fix: Correção da estilização da DRE Gerencial

This commit is contained in:
Alessandro Gonçaalves 2025-10-20 17:03:15 -03:00
parent 6a834f1118
commit 88c334959d
2 changed files with 398 additions and 530 deletions

View File

@ -6,10 +6,8 @@ import {
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
flexRender,
} from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
@ -26,7 +24,7 @@ import {
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { ChevronUp, ChevronDown, Download } from "lucide-react";
import { Download } from "lucide-react";
import * as XLSX from 'xlsx';
interface AnaliticoItem {
@ -69,7 +67,6 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [columnFilters, setColumnFilters] = React.useState<any[]>([]);
const [open, setOpen] = React.useState(false);
const [conditions, setConditions] = React.useState([{ column: "", operator: "contains", value: "" }]);
const [isScrolled, setIsScrolled] = React.useState(false);
const fetchData = React.useCallback(async () => {
// Só faz a requisição se tiver dataInicio e dataFim
@ -230,15 +227,6 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const virtualRows = rowVirtualizer.getVirtualItems();
React.useEffect(() => {
const handleScroll = () => {
if (!parentRef.current) return;
setIsScrolled(parentRef.current.scrollTop > 0);
};
const el = parentRef.current;
el?.addEventListener("scroll", handleScroll);
return () => el?.removeEventListener("scroll", handleScroll);
}, []);
const applyFilters = () => {
// Agrupar múltiplas condições por coluna
@ -325,322 +313,284 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
};
return (
<Card className="w-full h-[85vh] shadow-xl rounded-2xl border-0 bg-gradient-to-br from-white to-gray-50/30 flex flex-col">
<CardContent className="p-6 flex-1 flex flex-col">
<div className="flex justify-between mb-6 flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">Análise Analítica</h2>
<p className="text-sm text-gray-500">Relatório detalhado de transações</p>
</div>
<div className="w-full max-w-7xl mx-auto p-6">
{/* Header Section */}
<div className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div className="flex gap-3">
<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"
>
Filtros Avançados
</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>
<h1 className="text-2xl font-bold text-gray-900">Análise Analítica</h1>
<p className="text-sm text-gray-500">Relatório detalhado de transações</p>
</div>
</div>
{/* Controls */}
<div className="flex gap-3 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"
>
Filtros Avançados
</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>
<div className="relative flex-1 bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col">
<div
ref={parentRef}
className="flex-1 overflow-auto bg-white scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 hover:scrollbar-thumb-gray-400"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#cbd5e0 #f7fafc'
}}
>
<table className="min-w-full border-collapse">
<thead
className={`bg-gradient-to-r from-blue-50 to-indigo-50 sticky top-0 z-20 transition-all duration-200 ${isScrolled ? "shadow-lg" : "shadow-sm"}`}
>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((header) => {
const sorted = header.column.getIsSorted();
return (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className="text-left px-4 py-4 border-b border-gray-200 cursor-pointer select-none group hover:bg-blue-100/50 transition-colors duration-150 whitespace-nowrap min-w-[150px]"
>
<div className="flex items-center justify-between gap-2">
<span className="font-semibold text-gray-800 text-sm uppercase tracking-wide truncate">
{flexRender(header.column.columnDef.header, header.getContext())}
</span>
<div className="flex flex-col flex-shrink-0">
{sorted === "asc" ? (
<ChevronUp className="w-4 h-4 text-blue-600" />
) : sorted === "desc" ? (
<ChevronDown className="w-4 h-4 text-blue-600" />
) : (
<div className="flex flex-col opacity-30 group-hover:opacity-60 transition-opacity">
<ChevronUp className="w-3 h-3 -mb-1" />
<ChevronDown className="w-3 h-3" />
</div>
)}
</div>
</div>
</th>
);
})}
</tr>
))}
</thead>
<tbody
style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: "relative" }}
>
{loading ? (
<tr>
<td colSpan={columns.length} className="p-12 text-center">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="text-gray-500 font-medium">Carregando dados analíticos...</span>
</div>
</td>
</tr>
) : virtualRows.length === 0 ? (
<tr>
<td colSpan={columns.length} className="p-12 text-center">
<div className="flex flex-col items-center gap-3">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<span className="text-gray-500 font-medium">Nenhum dado analítico encontrado para os filtros aplicados.</span>
</div>
</td>
</tr>
) : (
virtualRows.map((virtualRow) => {
const row = table.getRowModel().rows[virtualRow.index];
return (
<tr
key={row.id}
className="hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30 transition-all duration-150 border-b border-gray-100"
style={{
position: "absolute",
top: 0,
left: 0,
transform: `translateY(${virtualRow.start}px)`,
display: "table",
width: "100%",
}}
>
{row.getVisibleCells().map((cell, cellIndex) => (
<td
key={cell.id}
className={`px-4 py-3 text-sm whitespace-nowrap overflow-hidden min-w-[150px] ${
cellIndex === 0 ? 'font-medium text-gray-900' : 'text-gray-700'
} ${
cell.column.id === 'valor' ? 'text-right font-semibold' : ''
}`}
title={String(cell.getValue())}
>
<div className="truncate">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</td>
))}
</tr>
);
})
)}
</tbody>
</table>
</div>
{data.length > 0 && (
<div className="flex-shrink-0 p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border-t border-blue-200">
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900">
Total de Registros: <span className="text-blue-600">{table.getRowModel().rows.length}</span>
</h3>
<p className="text-sm text-gray-600">Transações encontradas</p>
</div>
</div>
<div className="text-right">
<h3 className="text-lg font-bold">
<span className={totalValor < 0 ? 'text-red-600' : 'text-green-600'}>
Valor Total: {new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(totalValor)}
</span>
</h3>
<p className="text-sm text-gray-600">Soma de todos os valores</p>
</div>
</div>
</div>
)}
</div>
{/* Table Container */}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
{/* Table Header */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
<div className="grid grid-cols-12 gap-4 px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide">
<div className="col-span-1">Data Comp.</div>
<div className="col-span-1">Data Venc.</div>
<div className="col-span-1">Data Caixa</div>
<div className="col-span-1">Cód. Fornec.</div>
<div className="col-span-2">Fornecedor</div>
<div className="col-span-1">Cód. Centro</div>
<div className="col-span-1">Cód. Conta</div>
<div className="col-span-2">Conta</div>
<div className="col-span-1 text-right">Valor</div>
<div className="col-span-1">Recnum</div>
</div>
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
<DialogHeader className="pb-4">
<DialogTitle className="text-xl font-semibold text-gray-900">Filtros Avançados</DialogTitle>
</DialogHeader>
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
{conditions.map((cond, idx) => (
<div key={idx} className="flex gap-3 items-start p-4 bg-gray-50 rounded-lg border border-gray-200">
<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) => (
<SelectItem key={col.accessorKey} value={col.accessorKey}>
{col.header}
</SelectItem>
))}
</SelectContent>
</Select>
</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;
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>
</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>
{/* Table Body */}
<div
ref={parentRef}
className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"
>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div>
<p className="text-gray-500">Carregando dados...</p>
</div>
</div>
</div>
) : virtualRows.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<p className="text-gray-500">Nenhum dado encontrado</p>
</div>
</div>
) : (
<div className="relative" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
{virtualRows.map((virtualRow) => {
const row = table.getRowModel().rows[virtualRow.index];
return (
<div
key={row.id}
className="absolute top-0 left-0 w-full grid grid-cols-12 gap-4 px-4 py-3 text-sm border-b border-gray-100 hover:bg-gray-50 transition-colors"
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
<div className="col-span-1 text-gray-600">{new Date(row.original.data_competencia).toLocaleDateString('pt-BR')}</div>
<div className="col-span-1 text-gray-600">{new Date(row.original.data_vencimento).toLocaleDateString('pt-BR')}</div>
<div className="col-span-1 text-gray-600">{new Date(row.original.data_caixa).toLocaleDateString('pt-BR')}</div>
<div className="col-span-1 font-medium text-gray-900">{row.original.codigo_fornecedor}</div>
<div className="col-span-2 text-gray-700 truncate" title={row.original.nome_fornecedor}>{row.original.nome_fornecedor}</div>
<div className="col-span-1 text-gray-600">{row.original.codigo_centrocusto}</div>
<div className="col-span-1 text-gray-600">{row.original.codigo_conta}</div>
<div className="col-span-2 text-gray-700 truncate" title={row.original.conta}>{row.original.conta}</div>
<div className={`col-span-1 text-right font-semibold ${row.original.valor < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(row.original.valor)}
</div>
<div className="col-span-1 text-gray-500">{row.original.recnum}</div>
</div>
);
})}
</div>
)}
</div>
</div>
{/* Summary Footer */}
{data.length > 0 && (
<div className="mt-6 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200 p-6">
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900">
Total de Registros: <span className="text-blue-600">{table.getRowModel().rows.length}</span>
</h3>
<p className="text-sm text-gray-600">Transações encontradas</p>
</div>
</div>
<div className="text-right">
<h3 className="text-lg font-bold">
<span className={totalValor < 0 ? 'text-red-600' : 'text-green-600'}>
Valor Total: {new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(totalValor)}
</span>
</h3>
<p className="text-sm text-gray-600">Soma de todos os valores</p>
</div>
</div>
</div>
)}
<DialogFooter className="flex gap-3 pt-6 border-t border-gray-200">
<Button
variant="outline"
onClick={clearFilters}
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
{/* Advanced Filters Dialog */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
<DialogHeader className="pb-4">
<DialogTitle className="text-xl font-semibold text-gray-900">Filtros Avançados</DialogTitle>
</DialogHeader>
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
{conditions.map((cond, idx) => (
<div key={idx} className="flex gap-3 items-start p-4 bg-gray-50 rounded-lg border border-gray-200">
<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) => (
<SelectItem key={col.accessorKey} value={col.accessorKey}>
{col.header}
</SelectItem>
))}
</SelectContent>
</Select>
</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;
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>
</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"
>
Limpar todos
<span className="text-lg">+</span>
Adicionar condição
</Button>
<Button
onClick={applyFilters}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
>
Aplicar filtros
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</div>
</div>
<DialogFooter className="flex gap-3 pt-6 border-t border-gray-200">
<Button
variant="outline"
onClick={clearFilters}
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
>
Limpar todos
</Button>
<Button
onClick={applyFilters}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
>
Aplicar filtros
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,8 +1,6 @@
'use client';
import { Button } from '@/components/ui/button';
// Removed unused table imports
import { ArrowDown, ArrowUp, ArrowUpDown, LoaderPinwheel } from 'lucide-react';
import { LoaderPinwheel } from 'lucide-react';
import { useEffect, useState } from 'react';
import AnaliticoComponent from './analitico';
@ -32,13 +30,6 @@ interface HierarchicalRow {
percentuaisPorMes?: Record<string, number>;
}
type SortField = 'descricao' | 'valor';
type SortDirection = 'asc' | 'desc';
interface SortConfig {
field: SortField;
direction: SortDirection;
}
export default function Teste() {
const [data, setData] = useState<DREItem[]>([]);
@ -51,10 +42,6 @@ export default function Teste() {
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
new Set()
);
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: 'descricao',
direction: 'asc',
});
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
// Estados para analítico
@ -207,24 +194,6 @@ export default function Teste() {
setExpandedCentros(newExpanded);
};
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="h-4 w-4" />;
}
return sortConfig.direction === 'asc' ? (
<ArrowUp className="h-4 w-4" />
) : (
<ArrowDown className="h-4 w-4" />
);
};
const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
const valoresPorMes: Record<string, number> = {};
@ -319,25 +288,7 @@ export default function Teste() {
}, {} as Record<string, DREItem[]>);
// Ordenar grupos
const sortedGrupos = Object.entries(grupos).sort(([a], [b]) => {
if (sortConfig.field === 'descricao') {
return sortConfig.direction === 'asc'
? a.localeCompare(b)
: b.localeCompare(a);
} else {
const totalA = grupos[a].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const totalB = grupos[b].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
return sortConfig.direction === 'asc'
? totalA - totalB
: totalB - totalA;
}
});
const sortedGrupos = Object.entries(grupos).sort(([a], [b]) => a.localeCompare(b));
sortedGrupos.forEach(([grupo, items]) => {
const totalGrupo = items.reduce(
@ -378,25 +329,7 @@ export default function Teste() {
}, {} as Record<string, DREItem[]>);
// Ordenar subgrupos
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) => {
if (sortConfig.field === 'descricao') {
return sortConfig.direction === 'asc'
? a.localeCompare(b)
: b.localeCompare(a);
} else {
const totalA = subgrupos[a].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const totalB = subgrupos[b].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
return sortConfig.direction === 'asc'
? totalA - totalB
: totalB - totalA;
}
});
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) => a.localeCompare(b));
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
const totalSubgrupo = subgrupoItems.reduce(
@ -431,25 +364,7 @@ export default function Teste() {
}, {} as Record<string, DREItem[]>);
// Ordenar centros de custo
const sortedCentros = Object.entries(centros).sort(([a], [b]) => {
if (sortConfig.field === 'descricao') {
return sortConfig.direction === 'asc'
? a.localeCompare(b)
: b.localeCompare(a);
} else {
const totalA = centros[a].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const totalB = centros[b].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
return sortConfig.direction === 'asc'
? totalA - totalB
: totalB - totalA;
}
});
const sortedCentros = Object.entries(centros).sort(([a], [b]) => a.localeCompare(b));
sortedCentros.forEach(([centro, centroItems]) => {
const totalCentro = centroItems.reduce(
@ -487,25 +402,7 @@ export default function Teste() {
}, {} as Record<string, DREItem[]>);
// Ordenar contas
const sortedContas = Object.entries(contas).sort(([a], [b]) => {
if (sortConfig.field === 'descricao') {
return sortConfig.direction === 'asc'
? a.localeCompare(b)
: b.localeCompare(a);
} else {
const totalA = contas[a].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const totalB = contas[b].reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
return sortConfig.direction === 'asc'
? totalA - totalB
: totalB - totalA;
}
});
const sortedContas = Object.entries(contas).sort(([a], [b]) => a.localeCompare(b));
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
@ -542,7 +439,7 @@ export default function Teste() {
};
const getRowStyle = (row: HierarchicalRow) => {
const baseStyle = 'transition-colors hover:bg-muted/50';
const baseStyle = 'transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30';
// Criar identificador único para a linha
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
@ -553,18 +450,18 @@ export default function Teste() {
let style = baseStyle;
if (isSelected) {
style += ' bg-blue-100 border-l-4 border-blue-500 shadow-md';
style += ' bg-gradient-to-r from-blue-100 to-indigo-100 border-l-4 border-blue-500 shadow-lg';
}
switch (row.type) {
case 'grupo':
return `${style} bg-primary/5 font-semibold`;
return `${style} bg-gradient-to-r from-blue-50/20 to-indigo-50/20 font-bold text-gray-900 border-b-2 border-blue-200`;
case 'subgrupo':
return `${style} bg-primary/10 font-medium`;
return `${style} bg-gradient-to-r from-gray-50/30 to-blue-50/20 font-semibold text-gray-800`;
case 'centro_custo':
return `${style} bg-secondary/30 font-medium`;
return `${style} bg-gradient-to-r from-gray-50/20 to-gray-100/10 font-medium text-gray-700`;
case 'conta':
return `${style} bg-muted/20`;
return `${style} bg-white font-normal text-gray-600`;
default:
return style;
}
@ -578,66 +475,74 @@ export default function Teste() {
switch (row.type) {
case 'grupo':
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-3 whitespace-nowrap">
<button
onClick={() => toggleGroup(row.grupo!)}
className="p-1 hover:bg-muted rounded"
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
>
{row.isExpanded ? '▼' : '▶'}
<span className="text-blue-600 font-bold text-sm">
{row.isExpanded ? '▼' : '▶'}
</span>
</button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<span className="font-semibold">{row.grupo}</span>
<span className="font-bold text-gray-900">{row.grupo}</span>
</button>
</div>
);
case 'subgrupo':
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-3 whitespace-nowrap">
<button
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
className="p-1 hover:bg-muted rounded"
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
>
{row.isExpanded ? '▼' : '▶'}
<span className="text-blue-600 font-bold text-sm">
{row.isExpanded ? '▼' : '▶'}
</span>
</button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<span className="font-medium">{row.subgrupo}</span>
<span className="font-semibold text-gray-800">{row.subgrupo}</span>
</button>
</div>
);
case 'centro_custo':
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-3 whitespace-nowrap">
<button
onClick={() =>
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
}
className="p-1 hover:bg-muted rounded"
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
>
{row.isExpanded ? '▼' : '▶'}
<span className="text-blue-600 font-bold text-sm">
{row.isExpanded ? '▼' : '▶'}
</span>
</button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<span className="font-medium">{row.centro_custo}</span>
<span className="font-medium text-gray-700">{row.centro_custo}</span>
</button>
</div>
);
case 'conta':
return (
<div className="flex items-center gap-2">
<span className="text-muted-foreground"></span>
<div className="flex items-center gap-3 whitespace-nowrap">
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
<span className="text-gray-400 font-bold text-lg"></span>
</div>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<span>{row.conta}</span>
<span className="font-normal text-gray-600">{row.conta}</span>
</button>
</div>
);
@ -648,11 +553,28 @@ export default function Teste() {
if (loading) {
return (
<div className="p-6">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<LoaderPinwheel className="h-8 w-8 animate-spin mx-auto mb-2" />
<p>Carregando dados...</p>
<div className="w-full max-w-7xl mx-auto p-6">
<div className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">DRE Gerencial</h1>
<p className="text-sm text-gray-500">Demonstração do Resultado do Exercício</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12">
<div className="flex flex-col items-center justify-center">
<div className="w-16 h-16 bg-gradient-to-r from-blue-100 to-indigo-100 rounded-full flex items-center justify-center mb-4">
<LoaderPinwheel className="h-8 w-8 text-blue-600 animate-spin" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Carregando dados...</h3>
<p className="text-sm text-gray-500">Aguarde enquanto processamos as informações</p>
</div>
</div>
</div>
@ -661,12 +583,33 @@ export default function Teste() {
if (error) {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4 text-destructive">
Erro ao carregar DRE Gerencial
</h1>
<div className="bg-destructive/10 border border-destructive/20 rounded-md p-4">
<p className="text-destructive">{error}</p>
<div className="w-full max-w-7xl mx-auto p-6">
<div className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-red-600 to-red-500 rounded-xl flex items-center justify-center shadow-lg">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">DRE Gerencial</h1>
<p className="text-sm text-gray-500">Demonstração do Resultado do Exercício</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-xl border border-red-200 p-8">
<div className="flex flex-col items-center text-center">
<div className="w-16 h-16 bg-gradient-to-r from-red-100 to-red-50 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-red-900 mb-2">Erro ao carregar DRE Gerencial</h3>
<p className="text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg p-3 max-w-md">
{error}
</p>
</div>
</div>
</div>
);
@ -675,79 +618,57 @@ export default function Teste() {
const hierarchicalData = buildHierarchicalData();
return (
<div className="w-full flex flex-col items-center gap-2">
<div className="mb-1">
<h1 className="text-lg font-bold">DRE Gerencial</h1>
<div className="w-full max-w-7xl mx-auto p-6">
{/* Header Section */}
<div className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">DRE Gerencial</h1>
<p className="text-sm text-gray-500">Demonstração do Resultado do Exercício</p>
</div>
</div>
</div>
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative">
{/* Header fixo separado */}
<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-[200px] max-w-[300px]">
<Button
variant="ghost"
onClick={() => handleSort('descricao')}
className="h-auto p-0 font-semibold"
>
Descrição
{getSortIcon('descricao')}
</Button>
</div>
{/* Table Container */}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
{/* Table Header */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
<div className="flex items-center gap-4 px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide">
<div className="flex-1 min-w-[300px] max-w-[400px]">Descrição</div>
{mesesDisponiveis.map((mes) => (
<div key={mes} className="flex min-w-[240px] max-w-[300px]">
<div className="flex-1 min-w-[120px] max-w-[150px] text-right px-2">
{mes}
</div>
<div className="flex-1 min-w-[120px] max-w-[150px] text-left px-2 text-xs text-muted-foreground pl-2.5">
%
</div>
<div key={mes} className="flex min-w-[200px] max-w-[250px]">
<div className="flex-1 min-w-[100px] text-right">{mes}</div>
<div className="flex-1 min-w-[100px] text-left text-xs text-gray-500">%</div>
</div>
))}
<div className="flex-1 min-w-[120px] max-w-[150px] text-right px-2">
<Button
variant="ghost"
onClick={() => handleSort('valor')}
className="h-auto p-0 font-semibold"
>
Total
{getSortIcon('valor')}
</Button>
</div>
<div className="flex-1 min-w-[120px] text-right">Total</div>
</div>
</div>
<div className="flex flex-col">
{/* Table Body */}
<div className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
{hierarchicalData.map((row, index) => (
<div key={index} className={`flex ${getRowStyle(row)}`}>
<div
className="flex-1 min-w-[200px] max-w-[300px] p-1 border-b text-xs"
style={getIndentStyle(row.level)}
>
<div key={index} className={`flex items-center gap-4 px-4 py-3 text-sm border-b border-gray-100 hover:bg-gray-50 transition-colors ${getRowStyle(row)}`}>
<div className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden" style={getIndentStyle(row.level)}>
{renderCellContent(row)}
</div>
{mesesDisponiveis.map((mes) => (
<div key={mes} className="flex min-w-[240px] max-w-[300px]">
<div key={mes} className="flex min-w-[200px] max-w-[250px]">
<div
className="flex-1 min-w-[120px] max-w-[150px] text-right font-medium p-1 border-b px-1 text-xs cursor-pointer hover:bg-blue-50"
className="flex-1 min-w-[100px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
onClick={() => handleRowClick(row, mes)}
title={row.valoresPorMes && row.valoresPorMes[mes] ? formatCurrency(row.valoresPorMes[mes]) : '-'}
>
{row.valoresPorMes && row.valoresPorMes[mes]
? (() => {
const { formatted, isNegative } =
formatCurrencyWithColor(row.valoresPorMes[mes]);
const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]);
return (
<span
className={
isNegative ? 'text-red-600' : 'text-gray-900'
}
>
<span className={isNegative ? 'text-red-600 font-bold' : 'text-gray-900'}>
{formatted}
</span>
);
@ -755,28 +676,25 @@ export default function Teste() {
: '-'}
</div>
<div
className="flex-1 min-w-[120px] max-w-[150px] text-left font-medium p-1 border-b px-1 text-xs cursor-pointer hover:bg-blue-50 pl-2.5"
className="flex-1 min-w-[100px] text-left font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
onClick={() => handleRowClick(row, mes)}
title={row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined ? `${row.percentuaisPorMes[mes].toFixed(1)}%` : '-'}
>
{row.percentuaisPorMes &&
row.percentuaisPorMes[mes] !== undefined
{row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
: '-'}
</div>
</div>
))}
<div
className="flex-1 min-w-[120px] max-w-[150px] text-right font-medium p-1 border-b px-1 text-xs cursor-pointer hover:bg-blue-50"
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
onClick={() => handleRowClick(row)}
title={row.total ? formatCurrency(row.total) : '-'}
>
{(() => {
const { formatted, isNegative } = formatCurrencyWithColor(
row.total!
);
const { formatted, isNegative } = formatCurrencyWithColor(row.total!);
return (
<span
className={isNegative ? 'text-red-600' : 'text-gray-900'}
>
<span className={isNegative ? 'text-red-600 font-bold' : 'text-gray-900'}>
{formatted}
</span>
);