299 lines
10 KiB
TypeScript
299 lines
10 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";
|
||
|
|
|
||
|
|
interface SaleSupervisor {
|
||
|
|
supervisorId: number;
|
||
|
|
name: string;
|
||
|
|
sale: number;
|
||
|
|
cost: number;
|
||
|
|
devolution: number;
|
||
|
|
objetivo: number;
|
||
|
|
profit: number;
|
||
|
|
percentual: number;
|
||
|
|
nfs: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface DashboardSale {
|
||
|
|
sale: number;
|
||
|
|
cost: number;
|
||
|
|
devolution: number;
|
||
|
|
objetivo: number;
|
||
|
|
profit: number;
|
||
|
|
percentual: number;
|
||
|
|
nfs: number;
|
||
|
|
saleSupervisor: SaleSupervisor[];
|
||
|
|
}
|
||
|
|
|
||
|
|
const DashboardDayView: React.FC = () => {
|
||
|
|
const [data, setData] = useState<DashboardSale | 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 apiUrl = env.API_URL.replace(/\/$/, ""); // Remove trailing slash
|
||
|
|
|
||
|
|
const response = await fetch(`${apiUrl}/dashboard/sale`, {
|
||
|
|
method: "GET",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
...(token && { Authorization: `Basic ${token}` }),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Erro ao carregar dados: ${response.statusText}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
setData(result);
|
||
|
|
} catch (err) {
|
||
|
|
setError(
|
||
|
|
err instanceof Error
|
||
|
|
? err.message
|
||
|
|
: "Erro ao carregar dados do dashboard"
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
fetchDashboardData();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const formatCurrency = (value: number): string => {
|
||
|
|
return new Intl.NumberFormat("pt-BR", {
|
||
|
|
style: "currency",
|
||
|
|
currency: "BRL",
|
||
|
|
}).format(value);
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatNumber = (value: number, decimals: number = 2): string => {
|
||
|
|
return new Intl.NumberFormat("pt-BR", {
|
||
|
|
minimumFractionDigits: decimals,
|
||
|
|
maximumFractionDigits: decimals,
|
||
|
|
}).format(value);
|
||
|
|
};
|
||
|
|
|
||
|
|
const colors = [
|
||
|
|
{ to: 25, color: "#f31700" },
|
||
|
|
{ from: 25, to: 50, color: "#f31700" },
|
||
|
|
{ from: 50, to: 70, color: "#ffc000" },
|
||
|
|
{ from: 70, color: "#0aac25" }, // Verde mais vibrante como na imagem
|
||
|
|
];
|
||
|
|
|
||
|
|
const colorsProfit = [
|
||
|
|
{ to: 0, color: "#f31700" },
|
||
|
|
{ from: 0, to: 20, color: "#f31700" },
|
||
|
|
{ from: 20, to: 30, color: "#ffc000" },
|
||
|
|
{ from: 30, color: "#0aac25" }, // Verde mais vibrante como na imagem
|
||
|
|
];
|
||
|
|
|
||
|
|
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) {
|
||
|
|
return (
|
||
|
|
<div className="bg-white p-6 rounded-2xl border border-slate-100">
|
||
|
|
<p className="text-slate-400 text-sm italic">Nenhum dado disponível</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const ticketMedio = data.nfs > 0 ? data.sale / data.nfs : 0;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="bg-slate-50 border-b border-slate-200 p-6 -mx-6 -mt-6 mb-6">
|
||
|
|
<h1 className="text-2xl font-black text-slate-600">Dashboard</h1>
|
||
|
|
<small className="text-slate-500 text-sm font-medium">
|
||
|
|
Faturamento
|
||
|
|
</small>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Cards Analytics */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
|
|
{/* Card 1: 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.percentual, 100)}%` }}
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
<small className="text-slate-500 text-xs mt-1 block">
|
||
|
|
{formatCurrency(data.objetivo)} ({formatNumber(data.percentual)}%)
|
||
|
|
</small>
|
||
|
|
</div>
|
||
|
|
<div className="mt-4">
|
||
|
|
<small className="text-slate-600 text-xs font-semibold block mb-2">
|
||
|
|
Devolução
|
||
|
|
</small>
|
||
|
|
<small className="text-slate-500 text-xs">
|
||
|
|
{formatCurrency(data.devolution)}
|
||
|
|
</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Card 2: Realizado */}
|
||
|
|
<div className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-center">
|
||
|
|
<ArcGauge value={data.percentual} colors={colors} title="Realizado" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Card 3: Margem Líquida */}
|
||
|
|
<div className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-center">
|
||
|
|
<ArcGauge
|
||
|
|
value={data.profit}
|
||
|
|
max={40}
|
||
|
|
colors={colorsProfit}
|
||
|
|
title="Margem Líquida"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Card 4: Cupons e Ticket Médio */}
|
||
|
|
<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]">
|
||
|
|
{formatNumber(data.nfs, 0)}
|
||
|
|
</h2>
|
||
|
|
</div>
|
||
|
|
<div className="mb-4">
|
||
|
|
<small className="text-slate-600 text-xs font-semibold block mb-2">
|
||
|
|
Quantidade de cupons emitidos
|
||
|
|
</small>
|
||
|
|
<div className="h-2.5 bg-slate-100 rounded-md overflow-hidden">
|
||
|
|
<div
|
||
|
|
className="h-full bg-[#f6d433] rounded-md"
|
||
|
|
style={{ width: "65%" }}
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="mt-4">
|
||
|
|
<small className="text-slate-600 text-xs font-semibold block mb-2">
|
||
|
|
Ticket Médio
|
||
|
|
</small>
|
||
|
|
<small className="text-slate-500 text-xs">
|
||
|
|
{formatCurrency(ticketMedio)}
|
||
|
|
</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Tabela de Supervisores */}
|
||
|
|
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden shadow-sm">
|
||
|
|
<div className="overflow-x-auto">
|
||
|
|
<table className="w-full">
|
||
|
|
<thead className="bg-slate-50">
|
||
|
|
<tr>
|
||
|
|
<th className="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
#
|
||
|
|
</th>
|
||
|
|
<th className="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
Loja
|
||
|
|
</th>
|
||
|
|
<th className="px-6 py-4 text-right text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
Meta
|
||
|
|
</th>
|
||
|
|
<th className="px-6 py-4 text-right text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
Realizado
|
||
|
|
</th>
|
||
|
|
<th className="px-6 py-4 text-right text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
%
|
||
|
|
</th>
|
||
|
|
<th className="px-6 py-4 text-right text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
Margem
|
||
|
|
</th>
|
||
|
|
<th className="px-6 py-4 text-right text-xs font-black text-slate-600 uppercase tracking-wider">
|
||
|
|
Devolução
|
||
|
|
</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-slate-100">
|
||
|
|
{data.saleSupervisor.map((supervisor, idx) => (
|
||
|
|
<tr
|
||
|
|
key={supervisor.supervisorId}
|
||
|
|
className="hover:bg-slate-50/50 transition-colors"
|
||
|
|
>
|
||
|
|
<td className="px-6 py-4 text-sm font-bold text-[#22baa0]">
|
||
|
|
{supervisor.supervisorId}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-sm font-medium text-slate-800">
|
||
|
|
{supervisor.name}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-sm text-right font-medium text-slate-600">
|
||
|
|
{formatCurrency(supervisor.objetivo)}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-sm text-right font-medium text-slate-800">
|
||
|
|
{formatCurrency(supervisor.sale)}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4">
|
||
|
|
<div className="flex items-center justify-end gap-2">
|
||
|
|
<span className="text-sm font-medium text-slate-800">
|
||
|
|
{formatNumber(supervisor.percentual)}%
|
||
|
|
</span>
|
||
|
|
<div className="w-20 h-2.5 bg-slate-100 rounded-md overflow-hidden">
|
||
|
|
<div
|
||
|
|
className={`h-full rounded-md ${
|
||
|
|
supervisor.percentual >= 100
|
||
|
|
? "bg-emerald-500"
|
||
|
|
: supervisor.percentual >= 70
|
||
|
|
? "bg-[#22baa0]"
|
||
|
|
: supervisor.percentual >= 50
|
||
|
|
? "bg-[#ffc000]"
|
||
|
|
: "bg-[#f31700]"
|
||
|
|
}`}
|
||
|
|
style={{
|
||
|
|
width: `${Math.min(supervisor.percentual, 100)}%`,
|
||
|
|
}}
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-sm text-right font-medium text-slate-800">
|
||
|
|
{formatNumber(supervisor.profit)}%
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-sm text-right font-medium text-slate-800">
|
||
|
|
{formatCurrency(supervisor.devolution)}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default DashboardDayView;
|