ajusteste feitos para analise analitica

This commit is contained in:
Felipe Batista 2025-10-08 02:08:35 -03:00
parent eb2398702a
commit eb7c6547fd
3 changed files with 500 additions and 5 deletions

293
src/app/DRE/analitico.tsx Normal file
View File

@ -0,0 +1,293 @@
'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 (
<div className="w-full mt-6 border-t pt-4">
<div className="flex justify-between items-center mb-4">
<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 */}
<div className="w-full max-h-[400px] overflow-y-auto border rounded-md relative">
{/* 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 && (
<div className="mb-4 p-4 bg-blue-50 border rounded-md">
<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>
);
}

View File

@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
// Removed unused table imports // Removed unused table imports
import { ArrowDown, ArrowUp, ArrowUpDown, BarChart3 } from 'lucide-react'; import { ArrowDown, ArrowUp, ArrowUpDown, BarChart3 } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import AnaliticoComponent from './analitico';
interface DREItem { interface DREItem {
codfilial: string; codfilial: string;
@ -55,6 +56,16 @@ export default function Teste() {
}); });
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]); const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
// Estados para analítico
const [analiticoFiltros, setAnaliticoFiltros] = useState({
dataInicio: '',
dataFim: '',
centroCusto: '',
codigoGrupo: '',
codigoSubgrupo: '',
codigoConta: '',
});
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, []); }, []);
@ -62,10 +73,13 @@ export default function Teste() {
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true); setLoading(true);
setError(null);
const response = await fetch('/api/dre'); const response = await fetch('/api/dre');
if (!response.ok) { if (!response.ok) {
throw new Error('Erro ao carregar dados'); throw new Error(`Erro ao carregar dados: ${response.status}`);
} }
const result = await response.json(); const result = await response.json();
setData(result); setData(result);
@ -97,6 +111,47 @@ export default function Teste() {
}); });
}; };
// Função para extrair códigos dos grupos e subgrupos
const extractCodes = (grupo: string, subgrupo?: string) => {
const grupoMatch = grupo.match(/^(\d+)/);
const codigoGrupo = grupoMatch ? grupoMatch[1] : '';
let codigoSubgrupo = '';
if (subgrupo) {
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
codigoSubgrupo = subgrupoMatch ? subgrupoMatch[1] : '';
}
return { codigoGrupo, codigoSubgrupo };
};
// Função para lidar com clique nas linhas
const handleRowClick = (row: HierarchicalRow) => {
if (!data.length) return;
// Pegar todas as datas disponíveis para definir o período
const datas = data.map((item) => item.data_competencia);
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7); // YYYY-MM
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7); // YYYY-MM
const { codigoGrupo, codigoSubgrupo } = extractCodes(
row.grupo || '',
row.subgrupo
);
setAnaliticoFiltros({
dataInicio: dataInicioStr,
dataFim: dataFimStr,
centroCusto: row.centro_custo || '',
codigoGrupo,
codigoSubgrupo,
codigoConta: row.codigo_conta?.toString() || '',
});
};
const toggleGroup = (grupo: string) => { const toggleGroup = (grupo: string) => {
const newExpanded = new Set(expandedGroups); const newExpanded = new Set(expandedGroups);
if (newExpanded.has(grupo)) { if (newExpanded.has(grupo)) {
@ -404,7 +459,12 @@ export default function Teste() {
> >
{row.isExpanded ? '▼' : '▶'} {row.isExpanded ? '▼' : '▶'}
</button> </button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
>
<span className="font-semibold">{row.grupo}</span> <span className="font-semibold">{row.grupo}</span>
</button>
</div> </div>
); );
case 'subgrupo': case 'subgrupo':
@ -416,7 +476,12 @@ export default function Teste() {
> >
{row.isExpanded ? '▼' : '▶'} {row.isExpanded ? '▼' : '▶'}
</button> </button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
>
<span className="font-medium">{row.subgrupo}</span> <span className="font-medium">{row.subgrupo}</span>
</button>
</div> </div>
); );
case 'centro_custo': case 'centro_custo':
@ -430,14 +495,24 @@ export default function Teste() {
> >
{row.isExpanded ? '▼' : '▶'} {row.isExpanded ? '▼' : '▶'}
</button> </button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
>
<span className="font-medium">{row.centro_custo}</span> <span className="font-medium">{row.centro_custo}</span>
</button>
</div> </div>
); );
case 'conta': case 'conta':
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-muted-foreground"></span> <span className="text-muted-foreground"></span>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
>
<span>{row.conta}</span> <span>{row.conta}</span>
</button>
</div> </div>
); );
default: default:
@ -546,6 +621,11 @@ export default function Teste() {
))} ))}
</div> </div>
</div> </div>
{/* Componente Analítico */}
{!loading && data.length > 0 && (
<AnaliticoComponent filtros={analiticoFiltros} />
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,122 @@
import db from '@/db';
import { sql } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const dataInicio = searchParams.get('dataInicio');
const dataFim = searchParams.get('dataFim');
const centroCusto = searchParams.get('centroCusto');
const codigoGrupo = searchParams.get('codigoGrupo');
const codigoSubgrupo = searchParams.get('codigoSubgrupo');
const codigoConta = searchParams.get('codigoConta');
if (!dataInicio || !dataFim) {
return NextResponse.json(
{ message: 'Parâmetros obrigatórios: dataInicio, dataFim' },
{ status: 400 }
);
}
let whereConditions = [];
let params = [dataInicio, dataFim];
if (centroCusto) {
whereConditions.push(`ffa.codigo_centrocusto = $${params.length + 1}`);
params.push(centroCusto);
}
if (codigoGrupo) {
whereConditions.push(`drec.codigo_grupo = $${params.length + 1}`);
params.push(codigoGrupo);
}
if (codigoSubgrupo) {
whereConditions.push(`dg.codigo_subgrupo = $${params.length + 1}`);
params.push(codigoSubgrupo);
}
if (codigoConta) {
whereConditions.push(`ffa.codigo_conta = $${params.length + 1}`);
params.push(codigoConta);
}
// Query com filtros aplicados
let query;
if (centroCusto || codigoGrupo || codigoSubgrupo || codigoConta) {
// Query com filtros específicos
const conditions = [];
if (centroCusto)
conditions.push(`ffa.codigo_centrocusto = '${centroCusto}'`);
if (codigoConta) conditions.push(`ffa.codigo_conta = '${codigoConta}'`);
const whereClause =
conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '';
query = sql`
SELECT
ffa.codigo_fornecedor,
ffa.nome_fornecedor,
ffa.id,
ffa.codfilial,
ffa.recnum,
ffa.data_competencia,
ffa.data_vencimento,
ffa.data_pagamento,
ffa.data_caixa,
ffa.codigo_conta,
ffa.conta,
ffa.codigo_centrocusto,
ffa.valor,
ffa.historico,
ffa.historico2,
ffa.created_at,
ffa.updated_at
FROM fato_financeiro_analitico AS ffa
WHERE to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN ${dataInicio} AND ${dataFim}
${sql.raw(whereClause)}
LIMIT 50
`;
} else {
// Query sem filtros específicos
query = sql`
SELECT
ffa.codigo_fornecedor,
ffa.nome_fornecedor,
ffa.id,
ffa.codfilial,
ffa.recnum,
ffa.data_competencia,
ffa.data_vencimento,
ffa.data_pagamento,
ffa.data_caixa,
ffa.codigo_conta,
ffa.conta,
ffa.codigo_centrocusto,
ffa.valor,
ffa.historico,
ffa.historico2,
ffa.created_at,
ffa.updated_at
FROM fato_financeiro_analitico AS ffa
WHERE to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN ${dataInicio} AND ${dataFim}
LIMIT 50
`;
}
const data = await db.execute(query);
return NextResponse.json(data.rows);
} catch (error) {
console.error('Erro ao buscar dados analíticos:', error);
return NextResponse.json(
{
message: 'Erro ao buscar dados analíticos',
error: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
);
}
}