Add internal purchases products page

This commit is contained in:
Tiago Yamamoto 2025-12-23 14:46:18 -03:00
parent 69ec17cd91
commit 949398b867

View file

@ -0,0 +1,261 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import Header from "@/components/Header";
import { empresaApiService } from "@/services/empresaApiService";
import { produtosVendaService, ProdutoCompleto } from "@/services/produtosVendaService";
import { MapPinIcon } from "@heroicons/react/24/outline";
interface Coordenadas {
lat: number;
lng: number;
}
interface GrupoProdutos {
chave: string;
nome: string;
descricao?: string;
categoria?: string;
laboratorio?: string;
itens: ProdutoCompleto[];
}
const calcularDistanciaKm = (origem: Coordenadas, destino: Coordenadas): number => {
const raioTerraKm = 6371;
const dLat = ((destino.lat - origem.lat) * Math.PI) / 180;
const dLng = ((destino.lng - origem.lng) * Math.PI) / 180;
const lat1 = (origem.lat * Math.PI) / 180;
const lat2 = (destino.lat * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLng / 2) * Math.sin(dLng / 2) * Math.cos(lat1) * Math.cos(lat2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return raioTerraKm * c;
};
const normalizarCoordenadas = (valor?: string | number | null): number | null => {
if (valor === undefined || valor === null) {
return null;
}
const numero = Number(valor);
if (!Number.isFinite(numero)) {
return null;
}
if (numero === 0) {
return null;
}
return numero;
};
const ComprasInternasPage = () => {
const [produtos, setProdutos] = useState<ProdutoCompleto[]>([]);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState<string | null>(null);
const [clienteCoords, setClienteCoords] = useState<Coordenadas | null>(null);
const [erroGeolocalizacao, setErroGeolocalizacao] = useState<string | null>(null);
const [coordenadasEmpresas, setCoordenadasEmpresas] = useState<Record<string, Coordenadas>>({});
useEffect(() => {
const carregarProdutos = async () => {
setLoading(true);
setErro(null);
try {
const produtosCarregados = await produtosVendaService.buscarTodosProdutosCompletos();
setProdutos(produtosCarregados);
const empresaIds = Array.from(
new Set(produtosCarregados.map((produto) => produto.empresa_id).filter(Boolean))
) as string[];
if (empresaIds.length) {
const empresas = await Promise.all(
empresaIds.map(async (empresaId) => ({
empresaId,
dados: await empresaApiService.buscarPorId(empresaId),
}))
);
const mapaCoordenadas: Record<string, Coordenadas> = {};
empresas.forEach(({ empresaId, dados }) => {
const lat = normalizarCoordenadas(dados?.latitude ?? null);
const lng = normalizarCoordenadas(dados?.longitude ?? null);
if (lat !== null && lng !== null) {
mapaCoordenadas[empresaId] = { lat, lng };
}
});
setCoordenadasEmpresas(mapaCoordenadas);
}
} catch (error) {
console.error("Erro ao carregar produtos para compra:", error);
setErro("Não foi possível carregar os produtos no momento.");
} finally {
setLoading(false);
}
};
carregarProdutos();
}, []);
useEffect(() => {
if (!navigator.geolocation) {
setErroGeolocalizacao("Geolocalização não suportada neste navegador.");
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
setClienteCoords({
lat: position.coords.latitude,
lng: position.coords.longitude,
});
},
(error) => {
console.warn("Erro ao obter geolocalização:", error);
setErroGeolocalizacao("Não foi possível obter sua localização.");
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000,
}
);
}, []);
const grupos = useMemo<GrupoProdutos[]>(() => {
const mapa = new Map<string, GrupoProdutos>();
produtos.forEach((produto) => {
const chave = produto.catalogo_id || produto.codigo_ean || produto.nome;
const grupoExistente = mapa.get(chave);
if (grupoExistente) {
grupoExistente.itens.push(produto);
} else {
mapa.set(chave, {
chave,
nome: produto.nome || "Produto sem nome",
descricao: produto.descricao,
categoria: produto.cat_nome || produto.categoria,
laboratorio: produto.lab_nome || produto.laboratorio,
itens: [produto],
});
}
});
return Array.from(mapa.values()).map((grupo) => ({
...grupo,
itens: [...grupo.itens].sort(
(a, b) => (a.preco_final || a.preco_venda) - (b.preco_final || b.preco_venda)
),
}));
}, [produtos]);
const obterDistancia = (produto: ProdutoCompleto): number | null => {
if (!clienteCoords || !produto.empresa_id) {
return null;
}
const coordenadasEmpresa = coordenadasEmpresas[produto.empresa_id];
if (!coordenadasEmpresa) {
return null;
}
return calcularDistanciaKm(clienteCoords, coordenadasEmpresa);
};
return (
<div className="min-h-screen bg-gray-50">
<Header />
<main className="max-w-6xl mx-auto px-4 py-8">
<div className="flex flex-col gap-3 mb-8">
<h1 className="text-2xl font-semibold text-gray-900">Produtos para compra interna</h1>
<p className="text-sm text-gray-600">
Lista interna de produtos para compra, agrupados por similaridade e com a distância estimada
até você.
</p>
{erroGeolocalizacao && (
<div className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-md px-3 py-2">
{erroGeolocalizacao}
</div>
)}
</div>
{loading && <div className="text-gray-500 text-sm">Carregando produtos...</div>}
{!loading && erro && (
<div className="text-sm text-red-700 bg-red-50 border border-red-200 rounded-md px-4 py-3">
{erro}
</div>
)}
{!loading && !erro && grupos.length === 0 && (
<div className="text-sm text-gray-600">Nenhum produto disponível para compra no momento.</div>
)}
<div className="space-y-6">
{grupos.map((grupo) => (
<section key={grupo.chave} className="bg-white border border-gray-200 rounded-lg p-5 shadow-sm">
<div className="flex flex-col gap-2 mb-4">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold text-gray-900">{grupo.nome}</h2>
<span className="text-xs text-gray-500">({grupo.itens.length} oferta(s))</span>
</div>
{grupo.descricao && <p className="text-sm text-gray-600">{grupo.descricao}</p>}
<div className="flex flex-wrap gap-3 text-xs text-gray-500">
{grupo.categoria && <span>Categoria: {grupo.categoria}</span>}
{grupo.laboratorio && <span>Laboratório: {grupo.laboratorio}</span>}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{grupo.itens.map((produto) => {
const distanciaKm = obterDistancia(produto);
const preco = produto.preco_final || produto.preco_venda;
return (
<div
key={produto.id}
className="border border-gray-100 rounded-lg p-4 flex flex-col gap-3 bg-gray-50"
>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700 font-medium">Estoque</div>
<div className="text-sm font-semibold text-gray-900">
{produto.quantidade_estoque ?? 0} un.
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700 font-medium">Preço</div>
<div className="text-sm font-semibold text-green-700">
R$ {preco.toFixed(2).replace(".", ",")}
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-sm text-gray-700 font-medium">
<MapPinIcon className="w-4 h-4 text-blue-600" />
Distância
</div>
<div className="text-sm font-semibold text-blue-700">
{distanciaKm !== null ? `${distanciaKm.toFixed(1)} km` : "Indisponível"}
</div>
</div>
{produto.data_validade && (
<div className="text-xs text-gray-500">
Validade: {new Date(produto.data_validade).toLocaleDateString("pt-BR")}
</div>
)}
</div>
);
})}
</div>
</section>
))}
</div>
</main>
</div>
);
};
export default ComprasInternasPage;