novas features, geracao pdf, regras de usuarios, novo perfil de acesso
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user