fix: Correção da estilização da DRE Gerencial
This commit is contained in:
parent
6a834f1118
commit
88c334959d
|
|
@ -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,21 +313,23 @@ 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">
|
||||
<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>
|
||||
<h2 className="text-xl font-bold text-gray-900">Análise Analítica</h2>
|
||||
<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>
|
||||
<div className="flex gap-3">
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<Input
|
||||
placeholder="Filtrar tudo..."
|
||||
value={globalFilter ?? ""}
|
||||
|
|
@ -367,118 +357,79 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
|||
</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 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>
|
||||
|
||||
{/* Table Body */}
|
||||
<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" }}
|
||||
className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"
|
||||
>
|
||||
{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 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>
|
||||
</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">
|
||||
<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>
|
||||
<span className="text-gray-500 font-medium">Nenhum dado analítico encontrado para os filtros aplicados.</span>
|
||||
<p className="text-gray-500">Nenhum dado encontrado</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
virtualRows.map((virtualRow) => {
|
||||
<div className="relative" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = table.getRowModel().rows[virtualRow.index];
|
||||
return (
|
||||
<tr
|
||||
<div
|
||||
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%",
|
||||
}}
|
||||
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)` }}
|
||||
>
|
||||
{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 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>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
})}
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Summary Footer */}
|
||||
{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="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">
|
||||
|
|
@ -507,8 +458,8 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Advanced Filters Dialog */}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
|
||||
<DialogHeader className="pb-4">
|
||||
|
|
@ -640,7 +591,6 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue