2025-10-08 05:08:35 +00:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
|
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
|
|
|
|
|
|
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 = 'nome_fornecedor' | 'data_competencia' | 'valor' | 'conta';
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-08 11:57:42 +00:00
|
|
|
<div className="w-full mt-2 border-t pt-1">
|
|
|
|
|
<div className="flex justify-center items-center mb-1">
|
2025-10-08 05:08:35 +00:00
|
|
|
<h2 className="text-lg font-bold">Análise Analítica</h2>
|
|
|
|
|
</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">
|
2025-10-08 05:08:35 +00:00
|
|
|
{/* Header fixo */}
|
|
|
|
|
<div
|
|
|
|
|
className="sticky top-0 z-30 border-b shadow-sm"
|
|
|
|
|
style={{ backgroundColor: 'white', opacity: 1 }}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="flex p-3 font-semibold text-xs"
|
|
|
|
|
style={{ backgroundColor: 'white', opacity: 1 }}
|
|
|
|
|
>
|
|
|
|
|
<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] text-right">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => handleSort('data_competencia')}
|
|
|
|
|
className="h-auto p-0 font-semibold"
|
|
|
|
|
>
|
|
|
|
|
Data
|
|
|
|
|
{getSortIcon('data_competencia')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-[120px] max-w-[150px] text-right">
|
|
|
|
|
<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-[200px] max-w-[300px]">Histórico</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-[150px] max-w-[200px] p-1 text-xs">
|
|
|
|
|
{row.nome_fornecedor || '-'}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-[100px] max-w-[120px] text-right p-1 text-xs">
|
|
|
|
|
{formatDate(row.data_competencia)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-[120px] max-w-[150px] text-right 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-[200px] max-w-[300px] p-1 text-xs">
|
|
|
|
|
{row.historico || '-'}
|
|
|
|
|
</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">
|
2025-10-08 05:08:35 +00:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|