fix: Correção da estilização da DRE Gerencial
This commit is contained in:
parent
6a834f1118
commit
88c334959d
|
|
@ -6,10 +6,8 @@ import {
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
flexRender,
|
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -26,7 +24,7 @@ import {
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { ChevronUp, ChevronDown, Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
interface AnaliticoItem {
|
interface AnaliticoItem {
|
||||||
|
|
@ -69,7 +67,6 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
||||||
const [columnFilters, setColumnFilters] = React.useState<any[]>([]);
|
const [columnFilters, setColumnFilters] = React.useState<any[]>([]);
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [conditions, setConditions] = React.useState([{ column: "", operator: "contains", value: "" }]);
|
const [conditions, setConditions] = React.useState([{ column: "", operator: "contains", value: "" }]);
|
||||||
const [isScrolled, setIsScrolled] = React.useState(false);
|
|
||||||
|
|
||||||
const fetchData = React.useCallback(async () => {
|
const fetchData = React.useCallback(async () => {
|
||||||
// Só faz a requisição se tiver dataInicio e dataFim
|
// Só faz a requisição se tiver dataInicio e dataFim
|
||||||
|
|
@ -230,15 +227,6 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
||||||
|
|
||||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
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 = () => {
|
const applyFilters = () => {
|
||||||
// Agrupar múltiplas condições por coluna
|
// Agrupar múltiplas condições por coluna
|
||||||
|
|
@ -325,322 +313,284 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="w-full max-w-7xl mx-auto p-6">
|
||||||
<CardContent className="p-6 flex-1 flex flex-col">
|
{/* Header Section */}
|
||||||
<div className="flex justify-between mb-6 flex-wrap gap-4">
|
<div className="mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
<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-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</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>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div>
|
||||||
<Input
|
<h1 className="text-2xl font-bold text-gray-900">Análise Analítica</h1>
|
||||||
placeholder="Filtrar tudo..."
|
<p className="text-sm text-gray-500">Relatório detalhado de transações</p>
|
||||||
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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
<div className="relative flex-1 bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col">
|
{/* Table Container */}
|
||||||
<div
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
||||||
ref={parentRef}
|
{/* Table Header */}
|
||||||
className="flex-1 overflow-auto bg-white scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 hover:scrollbar-thumb-gray-400"
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
|
||||||
style={{
|
<div className="grid grid-cols-12 gap-4 px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide">
|
||||||
scrollbarWidth: 'thin',
|
<div className="col-span-1">Data Comp.</div>
|
||||||
scrollbarColor: '#cbd5e0 #f7fafc'
|
<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>
|
||||||
<table className="min-w-full border-collapse">
|
<div className="col-span-2">Fornecedor</div>
|
||||||
<thead
|
<div className="col-span-1">Cód. Centro</div>
|
||||||
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"}`}
|
<div className="col-span-1">Cód. Conta</div>
|
||||||
>
|
<div className="col-span-2">Conta</div>
|
||||||
{table.getHeaderGroups().map((hg) => (
|
<div className="col-span-1 text-right">Valor</div>
|
||||||
<tr key={hg.id}>
|
<div className="col-span-1">Recnum</div>
|
||||||
{hg.headers.map((header) => {
|
</div>
|
||||||
const sorted = header.column.getIsSorted();
|
</div>
|
||||||
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 && (
|
{/* Table Body */}
|
||||||
<div className="flex-shrink-0 p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border-t border-blue-200">
|
<div
|
||||||
<div className="flex justify-between items-center">
|
ref={parentRef}
|
||||||
<div className="flex items-center gap-4">
|
className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"
|
||||||
<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">
|
{loading ? (
|
||||||
<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" />
|
<div className="flex items-center justify-center h-64">
|
||||||
</svg>
|
<div className="text-center">
|
||||||
</div>
|
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div>
|
||||||
<div>
|
<p className="text-gray-500">Carregando dados...</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
</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">
|
{/* 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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={clearFilters}
|
onClick={() =>
|
||||||
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
|
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>
|
||||||
<Button
|
</div>
|
||||||
onClick={applyFilters}
|
</div>
|
||||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
|
||||||
>
|
<DialogFooter className="flex gap-3 pt-6 border-t border-gray-200">
|
||||||
Aplicar filtros
|
<Button
|
||||||
</Button>
|
variant="outline"
|
||||||
</DialogFooter>
|
onClick={clearFilters}
|
||||||
</DialogContent>
|
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||||
</Dialog>
|
>
|
||||||
</CardContent>
|
Limpar todos
|
||||||
</Card>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={applyFilters}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
Aplicar filtros
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { LoaderPinwheel } from 'lucide-react';
|
||||||
// Removed unused table imports
|
|
||||||
import { ArrowDown, ArrowUp, ArrowUpDown, LoaderPinwheel } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import AnaliticoComponent from './analitico';
|
import AnaliticoComponent from './analitico';
|
||||||
|
|
||||||
|
|
@ -32,13 +30,6 @@ interface HierarchicalRow {
|
||||||
percentuaisPorMes?: Record<string, number>;
|
percentuaisPorMes?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortField = 'descricao' | 'valor';
|
|
||||||
type SortDirection = 'asc' | 'desc';
|
|
||||||
|
|
||||||
interface SortConfig {
|
|
||||||
field: SortField;
|
|
||||||
direction: SortDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Teste() {
|
export default function Teste() {
|
||||||
const [data, setData] = useState<DREItem[]>([]);
|
const [data, setData] = useState<DREItem[]>([]);
|
||||||
|
|
@ -51,10 +42,6 @@ export default function Teste() {
|
||||||
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
|
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
|
||||||
new Set()
|
new Set()
|
||||||
);
|
);
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
|
||||||
field: 'descricao',
|
|
||||||
direction: 'asc',
|
|
||||||
});
|
|
||||||
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
|
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
|
||||||
|
|
||||||
// Estados para analítico
|
// Estados para analítico
|
||||||
|
|
@ -207,24 +194,6 @@ export default function Teste() {
|
||||||
setExpandedCentros(newExpanded);
|
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 calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
|
||||||
const valoresPorMes: Record<string, number> = {};
|
const valoresPorMes: Record<string, number> = {};
|
||||||
|
|
@ -319,25 +288,7 @@ export default function Teste() {
|
||||||
}, {} as Record<string, DREItem[]>);
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
// Ordenar grupos
|
// Ordenar grupos
|
||||||
const sortedGrupos = Object.entries(grupos).sort(([a], [b]) => {
|
const sortedGrupos = Object.entries(grupos).sort(([a], [b]) => a.localeCompare(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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sortedGrupos.forEach(([grupo, items]) => {
|
sortedGrupos.forEach(([grupo, items]) => {
|
||||||
const totalGrupo = items.reduce(
|
const totalGrupo = items.reduce(
|
||||||
|
|
@ -378,25 +329,7 @@ export default function Teste() {
|
||||||
}, {} as Record<string, DREItem[]>);
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
// Ordenar subgrupos
|
// Ordenar subgrupos
|
||||||
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) => {
|
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) => a.localeCompare(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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
|
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
|
||||||
const totalSubgrupo = subgrupoItems.reduce(
|
const totalSubgrupo = subgrupoItems.reduce(
|
||||||
|
|
@ -431,25 +364,7 @@ export default function Teste() {
|
||||||
}, {} as Record<string, DREItem[]>);
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
// Ordenar centros de custo
|
// Ordenar centros de custo
|
||||||
const sortedCentros = Object.entries(centros).sort(([a], [b]) => {
|
const sortedCentros = Object.entries(centros).sort(([a], [b]) => a.localeCompare(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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sortedCentros.forEach(([centro, centroItems]) => {
|
sortedCentros.forEach(([centro, centroItems]) => {
|
||||||
const totalCentro = centroItems.reduce(
|
const totalCentro = centroItems.reduce(
|
||||||
|
|
@ -487,25 +402,7 @@ export default function Teste() {
|
||||||
}, {} as Record<string, DREItem[]>);
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
// Ordenar contas
|
// Ordenar contas
|
||||||
const sortedContas = Object.entries(contas).sort(([a], [b]) => {
|
const sortedContas = Object.entries(contas).sort(([a], [b]) => a.localeCompare(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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sortedContas.forEach(([conta, contaItems]) => {
|
sortedContas.forEach(([conta, contaItems]) => {
|
||||||
const totalConta = contaItems.reduce(
|
const totalConta = contaItems.reduce(
|
||||||
|
|
@ -542,7 +439,7 @@ export default function Teste() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowStyle = (row: HierarchicalRow) => {
|
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
|
// Criar identificador único para a linha
|
||||||
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
|
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
|
||||||
|
|
@ -553,18 +450,18 @@ export default function Teste() {
|
||||||
let style = baseStyle;
|
let style = baseStyle;
|
||||||
|
|
||||||
if (isSelected) {
|
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) {
|
switch (row.type) {
|
||||||
case 'grupo':
|
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':
|
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':
|
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':
|
case 'conta':
|
||||||
return `${style} bg-muted/20`;
|
return `${style} bg-white font-normal text-gray-600`;
|
||||||
default:
|
default:
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
@ -578,66 +475,74 @@ export default function Teste() {
|
||||||
switch (row.type) {
|
switch (row.type) {
|
||||||
case 'grupo':
|
case 'grupo':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleGroup(row.grupo!)}
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRowClick(row)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'subgrupo':
|
case 'subgrupo':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRowClick(row)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'centro_custo':
|
case 'centro_custo':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRowClick(row)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'conta':
|
case 'conta':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
||||||
<span className="text-muted-foreground">•</span>
|
<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
|
<button
|
||||||
onClick={() => handleRowClick(row)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -648,11 +553,28 @@ export default function Teste() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="w-full max-w-7xl mx-auto p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="mb-6">
|
||||||
<div className="text-center">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<LoaderPinwheel className="h-8 w-8 animate-spin mx-auto mb-2" />
|
<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">
|
||||||
<p>Carregando dados...</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -661,12 +583,33 @@ export default function Teste() {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="w-full max-w-7xl mx-auto p-6">
|
||||||
<h1 className="text-2xl font-bold mb-4 text-destructive">
|
<div className="mb-6">
|
||||||
Erro ao carregar DRE Gerencial
|
<div className="flex items-center gap-3 mb-4">
|
||||||
</h1>
|
<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">
|
||||||
<div className="bg-destructive/10 border border-destructive/20 rounded-md p-4">
|
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<p className="text-destructive">{error}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -675,79 +618,57 @@ export default function Teste() {
|
||||||
const hierarchicalData = buildHierarchicalData();
|
const hierarchicalData = buildHierarchicalData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col items-center gap-2">
|
<div className="w-full max-w-7xl mx-auto p-6">
|
||||||
<div className="mb-1">
|
{/* Header Section */}
|
||||||
<h1 className="text-lg font-bold">DRE Gerencial</h1>
|
<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>
|
||||||
|
|
||||||
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative">
|
{/* Table Container */}
|
||||||
{/* Header fixo separado */}
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
||||||
<div
|
{/* Table Header */}
|
||||||
className="sticky top-0 z-30 border-b shadow-sm"
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
|
||||||
style={{ backgroundColor: 'white', opacity: 1 }}
|
<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>
|
||||||
<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>
|
|
||||||
{mesesDisponiveis.map((mes) => (
|
{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 px-2">
|
<div className="flex-1 min-w-[100px] text-right">{mes}</div>
|
||||||
{mes}
|
<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-left px-2 text-xs text-muted-foreground pl-2.5">
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex-1 min-w-[120px] max-w-[150px] text-right px-2">
|
<div className="flex-1 min-w-[120px] text-right">Total</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('valor')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Total
|
|
||||||
{getSortIcon('valor')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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) => (
|
{hierarchicalData.map((row, index) => (
|
||||||
<div key={index} className={`flex ${getRowStyle(row)}`}>
|
<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
|
<div className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden" style={getIndentStyle(row.level)}>
|
||||||
className="flex-1 min-w-[200px] max-w-[300px] p-1 border-b text-xs"
|
|
||||||
style={getIndentStyle(row.level)}
|
|
||||||
>
|
|
||||||
{renderCellContent(row)}
|
{renderCellContent(row)}
|
||||||
</div>
|
</div>
|
||||||
{mesesDisponiveis.map((mes) => (
|
{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
|
<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)}
|
onClick={() => handleRowClick(row, mes)}
|
||||||
|
title={row.valoresPorMes && row.valoresPorMes[mes] ? formatCurrency(row.valoresPorMes[mes]) : '-'}
|
||||||
>
|
>
|
||||||
{row.valoresPorMes && row.valoresPorMes[mes]
|
{row.valoresPorMes && row.valoresPorMes[mes]
|
||||||
? (() => {
|
? (() => {
|
||||||
const { formatted, isNegative } =
|
const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]);
|
||||||
formatCurrencyWithColor(row.valoresPorMes[mes]);
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={isNegative ? 'text-red-600 font-bold' : 'text-gray-900'}>
|
||||||
className={
|
|
||||||
isNegative ? 'text-red-600' : 'text-gray-900'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{formatted}
|
{formatted}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
@ -755,28 +676,25 @@ export default function Teste() {
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
<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)}
|
onClick={() => handleRowClick(row, mes)}
|
||||||
|
title={row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined ? `${row.percentuaisPorMes[mes].toFixed(1)}%` : '-'}
|
||||||
>
|
>
|
||||||
{row.percentuaisPorMes &&
|
{row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined
|
||||||
row.percentuaisPorMes[mes] !== undefined
|
|
||||||
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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)}
|
onClick={() => handleRowClick(row)}
|
||||||
|
title={row.total ? formatCurrency(row.total) : '-'}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { formatted, isNegative } = formatCurrencyWithColor(
|
const { formatted, isNegative } = formatCurrencyWithColor(row.total!);
|
||||||
row.total!
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={isNegative ? 'text-red-600 font-bold' : 'text-gray-900'}>
|
||||||
className={isNegative ? 'text-red-600' : 'text-gray-900'}
|
|
||||||
>
|
|
||||||
{formatted}
|
{formatted}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue