novas features, geracao pdf, regras de usuarios, novo perfil de acesso

This commit is contained in:
Vitex Tecnologia
2026-05-13 17:43:25 -03:00
parent 343aae602f
commit 517c9751bc
22 changed files with 1945 additions and 253 deletions
@@ -0,0 +1,262 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LayoutDashboard, Loader2, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useAuthAccess } from "@/contexts/AuthAccessContext";
import {
dashboardPontosPorClienteService,
type DashboardPontosPorParceiro,
} from "@/services/fechamento/dashboardPontosPorCliente";
import { fechamentoParceirosService, type ParceiroItem } from "@/services/fechamento/parceiros";
function formatPontos(valor: number): string {
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 4 });
}
function primeiroDiaMesAtual(): string {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
return `${y}-${m}-01`;
}
function hojeIsoDate(): string {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
export default function DashboardPontos() {
const { papel, me } = useAuthAccess();
const [parceiros, setParceiros] = useState<ParceiroItem[]>([]);
const [dataInicio, setDataInicio] = useState(primeiroDiaMesAtual);
const [dataFim, setDataFim] = useState(hojeIsoDate);
const [parceiroId, setParceiroId] = useState<string>("all");
const [cliente, setCliente] = useState("");
const [loading, setLoading] = useState(true);
const [dados, setDados] = useState<DashboardPontosPorParceiro[]>([]);
const clienteRef = useRef(cliente);
clienteRef.current = cliente;
const parceiroFixoSupervisor = papel === "supervisor" && me?.parceiroId;
const loadParceiros = useCallback(async () => {
if (parceiroFixoSupervisor && me?.parceiroId) {
try {
const res = await fechamentoParceirosService.listarParceiros({
estaAtivo: "all",
page: 1,
perPage: 200,
});
const um = res.data.find((p) => p.id === me.parceiroId);
setParceiros(um ? [um] : []);
setParceiroId(me.parceiroId);
} catch (e) {
const message = e instanceof Error ? e.message : "Erro ao carregar parceiros.";
toast.error(message);
setParceiros([]);
setParceiroId(me.parceiroId);
}
return;
}
try {
const lista = await fechamentoParceirosService.listarParceirosAtivos();
setParceiros(lista);
} catch (e) {
const message = e instanceof Error ? e.message : "Erro ao carregar parceiros.";
toast.error(message);
setParceiros([]);
}
}, [me?.parceiroId, parceiroFixoSupervisor]);
const loadRelatorio = useCallback(async () => {
try {
setLoading(true);
const pid =
parceiroFixoSupervisor && me?.parceiroId
? me.parceiroId
: parceiroId === "all"
? undefined
: parceiroId;
const res = await dashboardPontosPorClienteService.relatorioPontosPorCliente({
dataInicio: dataInicio.trim() || undefined,
dataFim: dataFim.trim() || undefined,
parceiroId: pid,
cliente: clienteRef.current.trim() || undefined,
});
setDados(res);
} catch (e) {
const message = e instanceof Error ? e.message : "Erro ao carregar dashboard.";
toast.error(message);
setDados([]);
} finally {
setLoading(false);
}
}, [dataFim, dataInicio, me?.parceiroId, parceiroFixoSupervisor, parceiroId]);
useEffect(() => {
void loadParceiros();
}, [loadParceiros]);
useEffect(() => {
void loadRelatorio();
}, [dataFim, dataInicio, loadRelatorio, parceiroId, me?.parceiroId, papel]);
const totalGeral = useMemo(
() => dados.reduce((acc, p) => acc + p.totalParceiro, 0),
[dados],
);
return (
<div className="mx-auto max-w-6xl space-y-6 p-4 pb-10 lg:p-8">
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight">
<LayoutDashboard className="h-7 w-7 text-primary" />
Dashboard
</h1>
<p className="text-muted-foreground">
Pontos alocados por cliente (tarefas revisadas), por parceiro.
</p>
</div>
<Button type="button" variant="outline" size="sm" className="gap-2 self-start" onClick={() => void loadRelatorio()}>
<RefreshCw className="h-4 w-4" />
Atualizar
</Button>
</div>
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-lg">Filtros</CardTitle>
<CardDescription>Período por data de conclusão da tarefa, ou data de criação se ainda não concluída.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2">
<Label htmlFor="dash-data-ini">Data início</Label>
<Input
id="dash-data-ini"
type="date"
value={dataInicio}
onChange={(e) => setDataInicio(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="dash-data-fim">Data fim</Label>
<Input id="dash-data-fim" type="date" value={dataFim} onChange={(e) => setDataFim(e.target.value)} />
</div>
<div className="space-y-2">
<Label>Parceiro</Label>
<Select
value={parceiroId}
onValueChange={setParceiroId}
disabled={Boolean(parceiroFixoSupervisor)}
>
<SelectTrigger>
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
{!parceiroFixoSupervisor ? (
<SelectItem value="all">Todos os parceiros</SelectItem>
) : null}
{parceiroFixoSupervisor && parceiros.length === 0 && me?.parceiroId ? (
<SelectItem value={me.parceiroId}>Parceiro vinculado</SelectItem>
) : null}
{parceiros.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="dash-cliente">Cliente (contém)</Label>
<Input
id="dash-cliente"
placeholder="Nome do cliente"
value={cliente}
onChange={(e) => setCliente(e.target.value)}
/>
</div>
</CardContent>
<CardContent className="pt-0">
<Button type="button" onClick={() => void loadRelatorio()}>
Aplicar filtros
</Button>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle className="text-lg">Resumo</CardTitle>
<CardDescription>Soma dos pontos no período e filtros atuais.</CardDescription>
</div>
{loading ? (
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
) : (
<span className="text-lg font-semibold tabular-nums">{formatPontos(totalGeral)} pts</span>
)}
</CardHeader>
</Card>
{loading && dados.length === 0 ? (
<div className="flex justify-center py-16">
<Loader2 className="h-10 w-10 animate-spin text-muted-foreground" />
</div>
) : dados.length === 0 ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
Nenhum dado para os filtros selecionados.
</CardContent>
</Card>
) : (
<div className="space-y-6">
{dados.map((bloco) => (
<Card key={bloco.parceiroId}>
<CardHeader className="pb-2">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<CardTitle className="text-base">{bloco.parceiroNome}</CardTitle>
<span className="text-sm font-medium text-muted-foreground tabular-nums">
Total: {formatPontos(bloco.totalParceiro)} pts
</span>
</div>
</CardHeader>
<CardContent className="pt-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Cliente</TableHead>
<TableHead className="text-right">Pontos</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{bloco.linhas.map((linha) => (
<TableRow key={`${bloco.parceiroId}-${linha.cliente}`}>
<TableCell>{linha.cliente}</TableCell>
<TableCell className="text-right tabular-nums">{formatPontos(linha.pontos)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}