410 lines
15 KiB
TypeScript
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;
|