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

442 lines
14 KiB
TypeScript
Raw Normal View History

'use client';
import { Button } from '@/components/ui/button';
2025-10-08 11:59:57 +00:00
import { ArrowDown, ArrowUp, ArrowUpDown, Download } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
2025-10-08 11:59:57 +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;
}
type SortField =
| 'data_competencia'
| 'data_vencimento'
| 'data_caixa'
| 'codigo_fornecedor'
| 'nome_fornecedor'
| 'codigo_centrocusto'
| 'codigo_conta'
| 'conta'
| 'valor'
| 'historico'
| 'historico2'
| 'recnum';
type SortDirection = 'asc' | 'desc';
interface SortConfig {
field: SortField;
direction: SortDirection;
}
interface AnaliticoProps {
filtros: {
dataInicio: string;
dataFim: string;
centroCusto?: string;
codigoGrupo?: string;
codigoSubgrupo?: string;
codigoConta?: string;
};
}
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [data, setData] = useState<AnaliticoItem[]>([]);
const [loading, setLoading] = useState(false);
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: 'data_competencia',
direction: 'desc',
});
const fetchData = useCallback(async () => {
// Só faz a requisição se tiver dataInicio e dataFim
if (!filtros.dataInicio || !filtros.dataFim) {
setData([]);
return;
}
setLoading(true);
try {
const params = new URLSearchParams({
dataInicio: filtros.dataInicio,
dataFim: filtros.dataFim,
...(filtros.centroCusto && { centroCusto: filtros.centroCusto }),
...(filtros.codigoGrupo && { codigoGrupo: filtros.codigoGrupo }),
...(filtros.codigoSubgrupo && {
codigoSubgrupo: filtros.codigoSubgrupo,
}),
...(filtros.codigoConta && { codigoConta: filtros.codigoConta }),
});
const response = await fetch(`/api/analitico?${params}`);
if (response.ok) {
const result = await response.json();
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);
}
}, [filtros]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleSort = (field: SortField) => {
setSortConfig((prev) => ({
field,
direction:
prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc',
}));
};
const getSortIcon = (field: SortField) => {
if (sortConfig.field !== field) {
return <ArrowUpDown className="ml-1 h-3 w-3" />;
}
return sortConfig.direction === 'asc' ? (
<ArrowUp className="ml-1 h-3 w-3" />
) : (
<ArrowDown className="ml-1 h-3 w-3" />
);
};
const sortedData = [...data].sort((a, b) => {
const aValue = a[sortConfig.field];
const bValue = b[sortConfig.field];
if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortConfig.direction === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue;
}
return 0;
});
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('pt-BR');
};
const totalValor = data.reduce((sum, item) => {
const valor =
typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor;
return sum + (isNaN(valor) ? 0 : valor);
}, 0);
2025-10-08 11:59:57 +00:00
const exportToExcel = () => {
if (data.length === 0) return;
// Preparar dados para exportação
const exportData = data.map((item) => ({
'Data Competência': new Date(item.data_competencia).toLocaleDateString(
'pt-BR'
),
2025-10-08 11:59:57 +00:00
'Data Vencimento': new Date(item.data_vencimento).toLocaleDateString(
'pt-BR'
),
'Data Caixa': new Date(item.data_caixa).toLocaleDateString('pt-BR'),
'Código Fornecedor': item.codigo_fornecedor,
Fornecedor: item.nome_fornecedor,
'Código Centro Custo': item.codigo_centrocusto,
'Centro Custo': item.codigo_centrocusto, // Assumindo que é o mesmo valor
2025-10-08 11:59:57 +00:00
'Código Conta': item.codigo_conta,
Conta: item.conta,
Valor:
typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor,
Histórico: item.historico,
'Histórico 2': item.historico2,
Recnum: item.recnum,
2025-10-08 11:59:57 +00:00
}));
// Criar workbook
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(exportData);
// Adicionar resumo na segunda aba
const resumoData = [
{ Métrica: 'Total de Registros', Valor: data.length },
{ Métrica: 'Valor Total', Valor: totalValor },
];
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
// Adicionar abas ao workbook
XLSX.utils.book_append_sheet(wb, ws, 'Dados Analíticos');
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
// Gerar nome do arquivo com data e hora
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
const fileName = `analitico_${timestamp}.xlsx`;
// Fazer download
XLSX.writeFile(wb, fileName);
};
return (
2025-10-08 11:57:42 +00:00
<div className="w-full mt-2 border-t pt-1">
2025-10-08 11:59:57 +00:00
<div className="flex justify-between items-center mb-1">
<h2 className="text-lg font-bold">Análise Analítica</h2>
2025-10-08 11:59:57 +00:00
{data.length > 0 && (
<Button
onClick={exportToExcel}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
)}
</div>
{/* Filtros aplicados */}
{/* <div className="mb-4 p-3 bg-gray-50 rounded-md">
<div className="text-sm">
<strong>Filtros aplicados:</strong>
<div className="flex flex-wrap gap-2 mt-1">
{filtros.centroCusto && (
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
Centro: {filtros.centroCusto}
</span>
)}
{filtros.codigoGrupo && (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">
Grupo: {filtros.codigoGrupo}
</span>
)}
{filtros.codigoSubgrupo && (
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
Subgrupo: {filtros.codigoSubgrupo}
</span>
)}
{filtros.codigoConta && (
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded text-xs">
Conta: {filtros.codigoConta}
</span>
)}
</div>
</div>
</div> */}
{/* Resumo */}
{/* Tabela */}
2025-10-08 11:57:42 +00:00
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative mx-auto">
{/* Header fixo */}
<div
className="sticky top-0 z-30 border-b shadow-sm"
style={{ backgroundColor: 'white', opacity: 1 }}
>
<div
className="flex p-2 font-semibold text-xs"
style={{ backgroundColor: 'white', opacity: 1 }}
>
<div className="flex-1 min-w-[100px] max-w-[120px]">
<Button
variant="ghost"
onClick={() => handleSort('data_competencia')}
className="h-auto p-0 font-semibold"
>
Data Comp.
{getSortIcon('data_competencia')}
</Button>
</div>
<div className="flex-1 min-w-[100px] max-w-[120px]">
<Button
variant="ghost"
onClick={() => handleSort('data_vencimento')}
className="h-auto p-0 font-semibold"
>
Data Venc.
{getSortIcon('data_vencimento')}
</Button>
</div>
<div className="flex-1 min-w-[100px] max-w-[120px]">
<Button
variant="ghost"
onClick={() => handleSort('data_caixa')}
className="h-auto p-0 font-semibold"
>
Data Caixa
{getSortIcon('data_caixa')}
</Button>
</div>
<div className="flex-1 min-w-[100px] max-w-[120px]">
<Button
variant="ghost"
onClick={() => handleSort('codigo_fornecedor')}
className="h-auto p-0 font-semibold"
>
Cód. Fornec.
{getSortIcon('codigo_fornecedor')}
</Button>
</div>
<div className="flex-1 min-w-[150px] max-w-[200px]">
<Button
variant="ghost"
onClick={() => handleSort('nome_fornecedor')}
className="h-auto p-0 font-semibold"
>
Fornecedor
{getSortIcon('nome_fornecedor')}
</Button>
</div>
<div className="flex-1 min-w-[100px] max-w-[120px]">
<Button
variant="ghost"
onClick={() => handleSort('codigo_centrocusto')}
className="h-auto p-0 font-semibold"
>
Cód. Centro
{getSortIcon('codigo_centrocusto')}
</Button>
</div>
<div className="flex-1 min-w-[100px] max-w-[120px]">
<Button
variant="ghost"
onClick={() => handleSort('codigo_conta')}
className="h-auto p-0 font-semibold"
>
Cód. Conta
{getSortIcon('codigo_conta')}
</Button>
</div>
<div className="flex-1 min-w-[120px] max-w-[150px]">
<Button
variant="ghost"
onClick={() => handleSort('conta')}
className="h-auto p-0 font-semibold"
>
Conta
{getSortIcon('conta')}
</Button>
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] text-right">
<Button
variant="ghost"
onClick={() => handleSort('valor')}
className="h-auto p-0 font-semibold"
>
Valor
{getSortIcon('valor')}
</Button>
</div>
<div className="flex-1 min-w-[150px] max-w-[200px]">Histórico</div>
<div className="flex-1 min-w-[150px] max-w-[200px]">
Histórico 2
</div>
<div className="flex-1 min-w-[100px] max-w-[120px]">Recnum</div>
</div>
</div>
<div className="flex flex-col">
{loading ? (
<div className="p-8 text-center text-sm text-gray-500">
Carregando dados analíticos...
</div>
) : sortedData.length === 0 ? (
<div className="p-8 text-center text-sm text-gray-500">
Nenhum dado analítico encontrado para os filtros aplicados.
</div>
) : (
sortedData.map((row, index) => (
<div key={index} className="flex border-b hover:bg-gray-50">
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{formatDate(row.data_competencia)}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{formatDate(row.data_vencimento)}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{formatDate(row.data_caixa)}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{row.codigo_fornecedor || '-'}
</div>
<div className="flex-1 min-w-[150px] max-w-[200px] p-1 text-xs">
{row.nome_fornecedor || '-'}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{row.codigo_centrocusto || '-'}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{row.codigo_conta || '-'}
</div>
<div className="flex-1 min-w-[120px] max-w-[150px] p-1 text-xs">
{row.conta || '-'}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] text-right p-1 text-xs font-medium">
{formatCurrency(
typeof row.valor === 'string'
? parseFloat(row.valor)
: row.valor
)}
</div>
<div className="flex-1 min-w-[150px] max-w-[200px] p-1 text-xs">
{row.historico || '-'}
</div>
<div className="flex-1 min-w-[150px] max-w-[200px] p-1 text-xs">
{row.historico2 || '-'}
</div>
<div className="flex-1 min-w-[100px] max-w-[120px] p-1 text-xs">
{row.recnum || '-'}
</div>
</div>
))
)}
</div>
</div>
{data.length > 0 && (
2025-10-08 11:57:42 +00:00
<div className="w-[95%] mb-4 p-4 bg-blue-50 border rounded-md mx-auto">
<div className="flex justify-between items-center">
<div>
<h3 className="text-sm font-semibold">
Total de Registros: {data.length}
</h3>
</div>
<div>
<h3 className="text-sm font-semibold">
Valor Total: {formatCurrency(totalValor)}
</h3>
</div>
</div>
</div>
)}
</div>
);
}