Vendaweb-portal/components/dashboard/DashboardSellerView.tsx

410 lines
15 KiB
TypeScript

import React, { useState, useEffect } from "react";
import ArcGauge from "../ArcGauge";
import LoadingSpinner from "../LoadingSpinner";
import { env } from "../../src/config/env";
import { authService } from "../../src/services/auth.service";
import { formatCurrency, formatNumber } from "../../utils/formatters";
interface SaleSeller {
supervisorId: number;
store: string;
sellerId: number;
sellerName: string;
qtdeDaysMonth: number;
qtdeDays: number;
objetivo: number;
saleValue: number;
dif: number;
ObjetivoSale: number;
percentualObjective: number;
qtdeInvoice: number;
ticket: number;
listPrice: number;
discountValue: number;
percentOff: number;
mix: number;
saleToday: number;
devolution: number;
preSaleValue: number;
preSaleQtde: number;
objetiveHour?: number;
percentualObjectiveHour?: number;
}
interface DashboardSeller {
objetive: number;
sale: number;
percentualSale: number;
discount: number;
mix: number;
objetiveToday: number;
saleToday: number;
nfs: number;
devolution: number;
nfsToday: number;
objetiveHour: number;
percentualObjetiveHour: number;
saleSupervisor: SaleSeller[];
}
const DashboardSellerView: React.FC = () => {
const [data, setData] = useState<DashboardSeller | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
const token = authService.getToken();
const supervisorId = authService.getSupervisor();
const apiUrl = env.API_URL.replace(/\/$/, "");
if (!supervisorId) {
throw new Error("Supervisor ID não encontrado.");
}
const response = await fetch(
`${apiUrl}/dashboard/sale/${supervisorId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
...(token && { Authorization: `Basic ${token}` }),
},
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
"Erro ao buscar dados do dashboard do vendedor."
);
}
const result = await response.json();
setData(result);
} catch (err) {
console.error("Erro ao buscar dados do dashboard:", err);
setError(
err instanceof Error
? err.message
: "Não foi possível carregar os dados do dashboard."
);
} finally {
setLoading(false);
}
};
fetchDashboardData();
}, []);
const colors = [
{ to: 25, color: "#f31700" },
{ from: 25, to: 50, color: "#f31700" },
{ from: 50, to: 70, color: "#ffc000" },
{ from: 70, color: "#0aac25" },
];
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<LoadingSpinner />
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-2xl p-6">
<p className="text-red-600 text-sm font-medium">{error}</p>
</div>
);
}
if (!data || !data.saleSupervisor || data.saleSupervisor.length === 0) {
return (
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-6">
<p className="text-blue-600 text-sm font-medium">
Nenhum dado de vendas disponível.
</p>
</div>
);
}
const firstSeller = data.saleSupervisor[0];
const faturamentoDiaPercentual =
data.objetiveToday > 0 ? (data.saleToday / data.objetiveToday) * 100 : 0;
const ticketMedioDia = data.nfsToday > 0 ? data.saleToday / data.nfsToday : 0;
return (
<div className="space-y-6">
{/* Header */}
<header className="flex justify-between items-end">
<div>
<h2 className="text-2xl font-black text-[#002147]">
Dashboard - Venda mês
</h2>
<p className="text-slate-500 text-sm font-medium mt-1">
{firstSeller.supervisorId} - {firstSeller.store}
</p>
</div>
</header>
{/* Cards de Analytics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Card 1: Faturamento Dia */}
<div className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
<div className="mb-4">
<h2 className="text-3xl font-bold text-[#002147]">
{formatCurrency(data.saleToday)}
</h2>
</div>
<div className="mb-4">
<small className="text-slate-600 text-xs font-semibold block mb-2">
Faturamento Dia
</small>
<div className="h-2.5 bg-slate-100 rounded-md overflow-hidden">
<div
className="h-full bg-[#22baa0] rounded-md transition-all"
style={{ width: `${Math.min(faturamentoDiaPercentual, 100)}%` }}
></div>
</div>
<small className="text-slate-500 text-xs mt-1 block">
{formatCurrency(data.objetiveToday)} (
{formatNumber(faturamentoDiaPercentual)}%)
</small>
</div>
<div className="mt-4">
<small className="text-slate-600 text-xs font-semibold block mb-2">
Ticket Médio Dia
</small>
<h3 className="text-xl font-bold text-[#002147]">
{formatCurrency(ticketMedioDia)}
</h3>
</div>
</div>
{/* Card 2: Realizado Hora */}
<div className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-center">
<ArcGauge
value={data.percentualObjetiveHour}
colors={colors}
title="Realizado hora"
/>
</div>
{/* Card 3: Faturamento Líquido */}
<div className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
<div className="mb-4">
<h2 className="text-3xl font-bold text-[#002147]">
{formatCurrency(data.sale)}
</h2>
</div>
<div className="mb-4">
<small className="text-slate-600 text-xs font-semibold block mb-2">
Faturamento Líquido
</small>
<div className="h-2.5 bg-slate-100 rounded-md overflow-hidden">
<div
className="h-full bg-[#22baa0] rounded-md transition-all"
style={{ width: `${Math.min(data.percentualSale, 100)}%` }}
></div>
</div>
<small className="text-slate-500 text-xs mt-1 block">
{formatCurrency(data.objetive)} (
{formatNumber(data.percentualSale)}%)
</small>
</div>
<div className="mt-4">
<small className="text-slate-600 text-xs font-semibold block mb-2">
Devolução
</small>
<h3 className="text-xl font-bold text-red-500">
{formatCurrency(data.devolution)}
</h3>
</div>
</div>
{/* Card 4: Realizado */}
<div className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-center">
<ArcGauge
value={data.percentualSale}
colors={colors}
title="Realizado"
/>
</div>
</div>
{/* Tabela de Vendedores */}
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden">
<div className="p-5 border-b border-slate-50">
<h3 className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em]">
Vendedores
</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 text-left">
<tr>
{[
"Vendedor",
"Meta dia",
"Meta Hora",
"Venda dia",
"% Hora",
"% Dia",
"Meta",
"Realizado",
"Dif",
"%",
"Qtde NFs",
"Ticket Médio",
"Desconto",
"Mix",
"Qtde Orçamento",
"VL Orçamento",
].map((h) => (
<th
key={h}
className="px-4 py-3 text-[9px] font-black text-slate-400 uppercase tracking-widest whitespace-nowrap"
>
{h}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{data.saleSupervisor.map((seller, idx) => {
const percentualDia =
seller.ObjetivoSale > 0
? (seller.saleToday / seller.ObjetivoSale) * 100
: 0;
return (
<tr
key={idx}
className="hover:bg-slate-50/50 transition-colors"
>
<td className="px-4 py-4 text-slate-500 font-medium text-xs">
{seller.sellerName}
</td>
<td className="px-4 py-4 text-slate-500 font-medium text-xs text-right">
{formatCurrency(seller.ObjetivoSale)}
</td>
<td className="px-4 py-4 text-slate-500 font-medium text-xs text-right">
{formatCurrency(seller.objetiveHour || 0)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.saleToday)}
</td>
<td className="px-4 py-4">
<div className="flex items-center space-x-2">
<div className="w-20 h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full ${
(seller.percentualObjectiveHour || 0) >= 100
? "bg-emerald-500"
: (seller.percentualObjectiveHour || 0) >= 70
? "bg-orange-500"
: "bg-red-500"
}`}
style={{
width: `${Math.min(
seller.percentualObjectiveHour || 0,
100
)}%`,
}}
></div>
</div>
<span className="text-[10px] font-black text-slate-700 whitespace-nowrap">
{formatNumber(seller.percentualObjectiveHour || 0)}%
</span>
</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center space-x-2">
<div className="w-20 h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full ${
percentualDia >= 100
? "bg-emerald-500"
: percentualDia >= 70
? "bg-orange-500"
: "bg-red-500"
}`}
style={{
width: `${Math.min(percentualDia, 100)}%`,
}}
></div>
</div>
<span className="text-[10px] font-black text-slate-700 whitespace-nowrap">
{formatNumber(percentualDia)}%
</span>
</div>
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.objetivo)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.saleValue)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.dif)}
</td>
<td className="px-4 py-4">
<div className="flex items-center space-x-2">
<div className="w-20 h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full ${
seller.percentualObjective >= 100
? "bg-emerald-500"
: seller.percentualObjective >= 70
? "bg-orange-500"
: "bg-red-500"
}`}
style={{
width: `${Math.min(
seller.percentualObjective,
100
)}%`,
}}
></div>
</div>
<span className="text-[10px] font-black text-slate-700 whitespace-nowrap">
{formatNumber(seller.percentualObjective)}%
</span>
</div>
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatNumber(seller.qtdeInvoice, 0)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.ticket)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.discountValue)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatNumber(seller.mix, 0)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatNumber(seller.preSaleQtde, 0)}
</td>
<td className="px-4 py-4 font-bold text-slate-800 text-xs text-right">
{formatCurrency(seller.preSaleValue)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default DashboardSellerView;