vendaweb-api/src/app/DRE/analitico.tsx

738 lines
25 KiB
TypeScript
Raw Normal View History

2025-10-20 20:35:24 +00:00
"use client";
import * as React from "react";
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
2025-10-20 20:26:14 +00:00
import { Download, Filter, X } from "lucide-react";
2025-10-20 20:35:24 +00:00
import * as XLSX from "xlsx";
interface AnaliticoItem {
codigo_grupo: string;
codigo_subgrupo: string;
codigo_fornecedor: string;
nome_fornecedor: string;
id: number;
codfilial: string;
recnum: number;
data_competencia: string;
data_vencimento: string;
data_pagamento: string;
data_caixa: string;
codigo_conta: string;
conta: string;
codigo_centrocusto: string;
valor: number;
historico: string;
historico2: string;
created_at: string;
updated_at: string;
// Campos adicionais do Oracle
entidade?: string;
tipo_parceiro?: string;
valor_previsto?: number;
valor_confirmado?: number;
valor_pago?: number;
numero_lancamento?: number;
ano_mes_comp?: string;
codgrupo?: string;
}
interface AnaliticoProps {
filtros: {
dataInicio: string;
dataFim: string;
centroCusto?: string;
codigoGrupo?: string;
codigoSubgrupo?: string;
codigoConta?: string;
linhaSelecionada?: string;
};
}
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [data, setData] = React.useState<AnaliticoItem[]>([]);
const [loading, setLoading] = React.useState(false);
const [globalFilter, setGlobalFilter] = React.useState("");
const [open, setOpen] = React.useState(false);
2025-10-20 20:35:24 +00:00
const [conditions, setConditions] = React.useState([
{ column: "", operator: "contains", value: "" },
]);
// Estado para armazenar filtros externos (vindos do teste.tsx)
const [filtrosExternos, setFiltrosExternos] = React.useState(filtros);
// Atualizar filtros externos quando os props mudarem
React.useEffect(() => {
console.log('🔄 Analítico - useEffect dos filtros chamado');
console.log('📋 Filtros recebidos via props:', filtros);
setFiltrosExternos(filtros);
}, [filtros]);
const fetchData = React.useCallback(async () => {
console.log('🔄 Analítico - fetchData chamado');
console.log('📋 Filtros externos recebidos:', filtrosExternos);
if (!filtrosExternos.dataInicio || !filtrosExternos.dataFim) {
console.log('⚠️ Sem dataInicio ou dataFim, limpando dados');
setData([]);
return;
}
setLoading(true);
try {
const params = new URLSearchParams();
if (filtrosExternos.dataInicio) {
params.append('dataInicio', filtrosExternos.dataInicio);
}
if (filtrosExternos.dataFim) {
params.append('dataFim', filtrosExternos.dataFim);
}
if (filtrosExternos.centroCusto) {
params.append('centroCusto', filtrosExternos.centroCusto);
}
if (filtrosExternos.codigoGrupo) {
params.append('codigoGrupo', filtrosExternos.codigoGrupo);
}
if (filtrosExternos.codigoConta) {
params.append('codigoConta', filtrosExternos.codigoConta);
}
const url = `/api/analitico-oracle?${params.toString()}`;
console.log('🌐 Fazendo requisição para:', url);
const response = await fetch(url);
if (response.ok) {
const result = await response.json();
console.log('✅ Resposta da API recebida:', result.length, 'registros');
console.log('📝 Primeiros 2 registros:', result.slice(0, 2));
console.log('🔍 Verificando campos específicos:', {
data_vencimento: result[0]?.data_vencimento,
data_caixa: result[0]?.data_caixa,
entidade: result[0]?.entidade,
valor: result[0]?.valor,
tipo_valor: typeof result[0]?.valor
});
setData(result as AnaliticoItem[]);
} else {
console.error("❌ Erro ao buscar dados:", await response.text());
}
} catch (error) {
console.error("❌ Erro ao buscar dados:", error);
} finally {
setLoading(false);
}
}, [filtrosExternos]);
React.useEffect(() => {
fetchData();
}, [fetchData]);
// Definir colunas do DataGridPro
const columns = React.useMemo(() => [
{
field: "data_vencimento",
headerName: "Data de Vencimento",
width: 150,
sortable: true,
resizable: true,
renderCell: (params: any) => {
if (!params.value) return "-";
try {
return new Date(params.value).toLocaleDateString("pt-BR");
} catch (error) {
return params.value;
}
2025-10-20 20:35:24 +00:00
},
},
{
field: "data_caixa",
headerName: "Data de Caixa",
width: 130,
sortable: true,
resizable: true,
renderCell: (params: any) => {
if (!params.value) return "-";
try {
return new Date(params.value).toLocaleDateString("pt-BR");
} catch (error) {
return params.value;
}
2025-10-20 20:35:24 +00:00
},
},
{
field: "entidade",
headerName: "Entidade",
width: 100,
sortable: true,
resizable: true,
renderCell: (params: any) => params.value || "-",
},
{
field: "codigo_fornecedor",
headerName: "Cód. Fornecedor",
width: 140,
sortable: true,
resizable: true,
},
{
field: "nome_fornecedor",
headerName: "Nome do Fornecedor",
flex: 1,
minWidth: 200,
sortable: true,
resizable: true,
},
{
field: "codigo_centrocusto",
headerName: "Centro de Custo",
width: 130,
sortable: true,
resizable: true,
},
{
field: "codigo_conta",
headerName: "Código da Conta",
width: 150,
sortable: true,
resizable: true,
},
{
field: "conta",
headerName: "Nome da Conta",
flex: 1,
minWidth: 180,
sortable: true,
resizable: true,
},
{
field: "valor",
headerName: "Valor Realizado",
type: "number" as const,
width: 140,
sortable: true,
resizable: true,
renderCell: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "") return "-";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "-";
const formatted = new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(numValue);
return (
<span className={numValue < 0 ? "text-red-600" : "text-gray-900"}>
{formatted}
</span>
);
2025-10-20 20:35:24 +00:00
},
},
{
field: "valor_previsto",
headerName: "Valor Previsto",
type: "number" as const,
width: 130,
sortable: true,
resizable: true,
valueFormatter: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || value === 0) return "-";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "-";
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(numValue);
},
cellClassName: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || value === 0) return "text-gray-500";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "text-gray-500";
return numValue < 0 ? "text-red-600" : "text-gray-900";
2025-10-20 21:45:09 +00:00
},
},
{
field: "valor_confirmado",
headerName: "Valor Confirmado",
type: "number" as const,
width: 140,
sortable: true,
resizable: true,
valueFormatter: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || value === 0) return "-";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "-";
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(numValue);
2025-10-20 20:35:24 +00:00
},
cellClassName: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || value === 0) return "text-gray-500";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "text-gray-500";
return numValue < 0 ? "text-red-600" : "text-gray-900";
2025-10-20 21:45:09 +00:00
},
},
{
field: "valor_pago",
headerName: "Valor Pago",
type: "number" as const,
width: 130,
sortable: true,
resizable: true,
valueFormatter: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || value === 0) return "-";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "-";
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(numValue);
2025-10-20 21:45:09 +00:00
},
cellClassName: (params: any) => {
const value = params.value;
if (value === null || value === undefined || value === "" || value === 0) return "text-gray-500";
const numValue = typeof value === "string" ? parseFloat(value) : Number(value);
if (isNaN(numValue)) return "text-gray-500";
return numValue < 0 ? "text-red-600" : "text-gray-900";
},
},
{
field: "historico",
headerName: "Histórico",
flex: 1,
minWidth: 250,
sortable: true,
resizable: true,
},
{
field: "historico2",
headerName: "Histórico 2",
flex: 1,
minWidth: 300,
sortable: true,
resizable: true,
},
{
field: "numero_lancamento",
headerName: "Número do Lançamento",
width: 80,
sortable: true,
resizable: true,
valueFormatter: (params: any) => params.value || "-",
},
] as any, []);
// Calcular totais das colunas de valores
const columnTotals = React.useMemo(() => {
if (!data || data.length === 0) {
return {
valorRealizado: 0,
valorPrevisto: 0,
valorConfirmado: 0,
valorPago: 0,
};
}
const valorRealizado = data.reduce((sum, item) => {
const valor = typeof item.valor === "string" ? parseFloat(item.valor) : item.valor;
return sum + (isNaN(valor) ? 0 : valor);
}, 0);
const valorPrevisto = data.reduce((sum, item) => {
const valor = typeof item.valor_previsto === "string" ? parseFloat(item.valor_previsto) : (item.valor_previsto || 0);
return sum + (isNaN(valor) ? 0 : valor);
}, 0);
const valorConfirmado = data.reduce((sum, item) => {
const valor = typeof item.valor_confirmado === "string" ? parseFloat(item.valor_confirmado) : (item.valor_confirmado || 0);
return sum + (isNaN(valor) ? 0 : valor);
}, 0);
const valorPago = data.reduce((sum, item) => {
const valor = typeof item.valor_pago === "string" ? parseFloat(item.valor_pago) : (item.valor_pago || 0);
return sum + (isNaN(valor) ? 0 : valor);
}, 0);
return {
valorRealizado,
valorPrevisto,
valorConfirmado,
valorPago,
};
}, [data]);
// Exportação XLSX
2025-10-08 11:59:57 +00:00
const exportToExcel = () => {
if (data.length === 0) return;
const exportData = data.map((item) => ({
"Data Competência": item.data_competencia
? new Date(item.data_competencia).toLocaleDateString("pt-BR")
: "-",
"Data Vencimento": item.data_vencimento
? new Date(item.data_vencimento).toLocaleDateString("pt-BR")
: "-",
"Data Caixa": item.data_caixa
? new Date(item.data_caixa).toLocaleDateString("pt-BR")
: "-",
2025-10-20 20:35:24 +00:00
"Código Fornecedor": item.codigo_fornecedor,
Fornecedor: item.nome_fornecedor,
2025-10-20 20:35:24 +00:00
"Código Centro Custo": item.codigo_centrocusto,
"Centro Custo": item.codigo_centrocusto,
2025-10-20 20:35:24 +00:00
"Código Conta": item.codigo_conta,
2025-10-08 11:59:57 +00:00
Conta: item.conta,
Valor: typeof item.valor === "string" ? parseFloat(item.valor) : item.valor,
2025-10-08 11:59:57 +00:00
Histórico: item.historico,
2025-10-20 20:35:24 +00:00
"Histórico 2": item.historico2,
Recnum: item.recnum,
2025-10-08 11:59:57 +00:00
}));
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(exportData);
const resumoData = [
{ Métrica: "Total de Registros", Valor: data.length },
{ Métrica: "Valor Total", Valor: columnTotals.valorRealizado },
{ Métrica: "Filtros Aplicados", Valor: "Sim" },
2025-10-08 11:59:57 +00:00
];
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
2025-10-20 20:35:24 +00:00
XLSX.utils.book_append_sheet(wb, ws, "Dados Analíticos");
XLSX.utils.book_append_sheet(wb, wsResumo, "Resumo");
2025-10-08 11:59:57 +00:00
const now = new Date();
2025-10-20 20:35:24 +00:00
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, "-");
const fileName = `analitico_${timestamp}.xlsx`;
2025-10-08 11:59:57 +00:00
XLSX.writeFile(wb, fileName);
};
// Aplicar filtros avançados
const applyFilters = () => {
// Implementar lógica de filtros avançados se necessário
setOpen(false);
};
const clearFilters = () => {
setConditions([{ column: "", operator: "contains", value: "" }]);
setGlobalFilter("");
};
return (
2025-10-21 21:18:51 +00:00
<div className="w-full max-w-none mx-auto p-2">
{/* Header Section */}
2025-10-21 21:18:51 +00:00
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<div>
2025-10-20 20:35:24 +00:00
<h1 className="text-2xl font-bold text-gray-900">
2025-10-21 20:57:58 +00:00
Análise Analítica{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ""}
2025-10-20 20:35:24 +00:00
</h1>
<p className="text-sm text-gray-500">
Relatório detalhado de transações
</p>
</div>
2025-10-21 21:18:51 +00:00
{/* Controls */}
<div className="flex gap-2 flex-wrap">
<Input
placeholder="Filtrar tudo..."
value={globalFilter ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setGlobalFilter(e.target.value)
}
className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
<Button
variant="outline"
onClick={() => setOpen(true)}
className="bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
>
<Filter className="w-4 h-4 mr-2" />
Filtros Avançados
</Button>
{globalFilter && (
2025-10-21 21:18:51 +00:00
<Button
variant="outline"
onClick={clearFilters}
className="bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
>
<X className="w-4 h-4 mr-2" />
Limpar Filtros
2025-10-21 21:18:51 +00:00
</Button>
)}
{data.length > 0 && (
<Button
onClick={exportToExcel}
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700"
>
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
)}
</div>
</div>
2025-10-20 20:35:24 +00:00
{/* Filtros Externos Ativos */}
{(filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoConta) && (
2025-10-21 21:18:51 +00:00
<div className="mb-2 p-2 bg-transparent border-transparent rounded-lg">
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium text-blue-900">Filtros aplicados pela tabela DRE Gerencial:</span>
</div>
<div className="flex flex-wrap gap-2 text-xs text-blue-800">
{filtrosExternos.dataInicio && filtrosExternos.dataFim && (
<span className="px-2 py-1 bg-blue-100 rounded">
Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim}
2025-10-21 21:18:51 +00:00
</span>
)}
{filtrosExternos.centroCusto && (
<span className="px-2 py-1 bg-blue-100 rounded">
Centro: {filtrosExternos.centroCusto}
2025-10-21 21:18:51 +00:00
</span>
)}
{filtrosExternos.codigoGrupo && (
<span className="px-2 py-1 bg-blue-100 rounded">
Grupo: {filtrosExternos.codigoGrupo}
2025-10-21 21:18:51 +00:00
</span>
)}
{filtrosExternos.codigoConta && (
<span className="px-2 py-1 bg-blue-100 rounded">
Conta: {filtrosExternos.codigoConta}
2025-10-20 20:35:24 +00:00
</span>
)}
2025-10-21 21:18:51 +00:00
</div>
2025-10-20 20:35:24 +00:00
</div>
2025-10-21 21:18:51 +00:00
)}
2025-10-20 16:03:14 +00:00
</div>
{/* DataGridPro */}
<Card className="w-full h-[85vh] shadow-lg rounded-2xl">
<CardContent className="p-4 h-full">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">
Total de Registros: <span className="text-blue-600">{data.length}</span>
</h2>
<div className="text-sm text-gray-600">
Valor Total: <span className={`font-bold ${columnTotals.valorRealizado < 0 ? "text-red-600" : "text-green-600"}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valorRealizado)}
</span>
</div>
2025-10-21 21:18:51 +00:00
</div>
</div>
<div style={{ height: "calc(100% - 2rem)", width: "100%" }}>
<DataGrid
rows={data}
columns={columns}
loading={loading}
disableRowSelectionOnClick
density="compact"
checkboxSelection
slots={{ toolbar: GridToolbar }}
initialState={{
sorting: { sortModel: [{ field: "data_vencimento", sort: "desc" }] },
}}
sx={{
"& .MuiDataGrid-columnHeaders": {
position: "sticky",
top: 0,
backgroundColor: "#f9fafb",
zIndex: 2,
borderBottom: "1px solid #e5e7eb",
},
"& .MuiDataGrid-cell": {
borderBottom: "1px solid #f0f0f0",
fontSize: "0.875rem",
},
"& .MuiDataGrid-virtualScroller": { overflowY: "auto" },
"& .MuiDataGrid-toolbarContainer": {
backgroundColor: "#f8fafc",
borderBottom: "1px solid #e5e7eb",
padding: "8px 16px",
},
"& .MuiDataGrid-footerContainer": {
backgroundColor: "#f8fafc",
borderTop: "1px solid #e5e7eb",
},
}}
/>
</div>
</CardContent>
</Card>
{/* Advanced Filters Dialog */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
<DialogHeader className="pb-4">
2025-10-20 20:35:24 +00:00
<DialogTitle className="text-xl font-semibold text-gray-900">
Filtros Avançados
</DialogTitle>
<p className="text-sm text-gray-600">
Estes filtros são aplicados sobre os dados filtrados pela tabela DRE Gerencial.
</p>
</DialogHeader>
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
{conditions.map((cond, idx) => (
2025-10-20 20:35:24 +00:00
<div
key={idx}
2025-10-21 21:18:51 +00:00
className="flex gap-2 items-start p-2 bg-gray-50 rounded-lg border border-gray-200"
2025-10-20 20:35:24 +00:00
>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Coluna
</label>
<Select
value={cond.column}
onValueChange={(v: string) => {
const next = [...conditions];
next[idx].column = v;
setConditions(next);
}}
>
<SelectTrigger className="w-full bg-white border-gray-300">
<SelectValue placeholder="Selecione a coluna" />
</SelectTrigger>
<SelectContent>
{columns.map((col: any) => (
2025-10-20 20:35:24 +00:00
<SelectItem
key={col.field}
value={col.field}
2025-10-20 20:35:24 +00:00
>
{col.headerName}
2025-10-20 20:35:24 +00:00
</SelectItem>
))}
</SelectContent>
</Select>
2025-10-21 02:04:42 +00:00
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Operador
</label>
<Select
value={cond.operator}
onValueChange={(v: string) => {
const next = [...conditions];
next[idx].operator = v;
2025-10-20 20:35:24 +00:00
if (v === "empty" || v === "notEmpty")
next[idx].value = "";
setConditions(next);
}}
>
<SelectTrigger className="w-full bg-white border-gray-300">
<SelectValue placeholder="Selecione o operador" />
</SelectTrigger>
<SelectContent>
<SelectItem value="contains">contém</SelectItem>
<SelectItem value="equals">igual a</SelectItem>
<SelectItem value="startsWith">começa com</SelectItem>
<SelectItem value="endsWith">termina com</SelectItem>
<SelectItem value="empty">está vazio</SelectItem>
<SelectItem value="notEmpty">não está vazio</SelectItem>
</SelectContent>
</Select>
2025-10-21 02:04:42 +00:00
</div>
{!(cond.operator === "empty" || cond.operator === "notEmpty") && (
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Valor
</label>
<Input
value={cond.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const next = [...conditions];
next[idx].value = e.target.value;
setConditions(next);
}}
placeholder="Digite o valor"
className="w-full bg-white border-gray-300"
/>
</div>
)}
{conditions.length > 1 && (
<div className="flex items-end">
<Button
variant="outline"
size="sm"
onClick={() => {
const next = conditions.filter((_, i) => i !== idx);
setConditions(next);
}}
className="mt-6 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
>
</Button>
</div>
)}
</div>
))}
<div className="flex justify-center pt-2">
<Button
variant="outline"
onClick={() =>
setConditions((prev) => [
...prev,
{ column: "", operator: "contains", value: "" },
])
}
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200"
>
<span className="text-lg">+</span>
Adicionar condição
</Button>
</div>
</div>
2025-10-21 21:18:51 +00:00
<DialogFooter className="flex gap-2 pt-3 border-t border-gray-200">
<Button
2025-10-20 20:35:24 +00:00
variant="outline"
onClick={clearFilters}
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
>
Limpar filtros avançados
</Button>
<Button
onClick={applyFilters}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
>
Aplicar filtros
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}