2025-10-08 04:19:15 +00:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2025-10-08 04:45:26 +00:00
|
|
|
// Removed unused table imports
|
2025-10-08 20:21:10 +00:00
|
|
|
import { ArrowDown, ArrowUp, ArrowUpDown, LoaderPinwheel } from 'lucide-react';
|
2025-10-08 04:19:15 +00:00
|
|
|
import { useEffect, useState } from 'react';
|
2025-10-08 05:08:35 +00:00
|
|
|
import AnaliticoComponent from './analitico';
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
interface DREItem {
|
|
|
|
|
codfilial: string;
|
|
|
|
|
data_competencia: string;
|
|
|
|
|
data_cai: string;
|
|
|
|
|
grupo: string;
|
|
|
|
|
subgrupo: string;
|
|
|
|
|
centro_custo: string;
|
|
|
|
|
codigo_conta: number;
|
|
|
|
|
conta: string;
|
|
|
|
|
valor: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HierarchicalRow {
|
|
|
|
|
type: 'grupo' | 'subgrupo' | 'centro_custo' | 'conta';
|
|
|
|
|
level: number;
|
|
|
|
|
grupo?: string;
|
|
|
|
|
subgrupo?: string;
|
|
|
|
|
centro_custo?: string;
|
|
|
|
|
conta?: string;
|
|
|
|
|
codigo_conta?: number;
|
|
|
|
|
total?: number;
|
|
|
|
|
isExpanded?: boolean;
|
|
|
|
|
valoresPorMes?: 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[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
|
|
|
|
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(
|
|
|
|
|
new Set()
|
|
|
|
|
);
|
|
|
|
|
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
|
|
|
|
|
new Set()
|
|
|
|
|
);
|
|
|
|
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
|
|
|
|
field: 'descricao',
|
|
|
|
|
direction: 'asc',
|
|
|
|
|
});
|
|
|
|
|
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
// Estados para analítico
|
|
|
|
|
const [analiticoFiltros, setAnaliticoFiltros] = useState({
|
|
|
|
|
dataInicio: '',
|
|
|
|
|
dataFim: '',
|
|
|
|
|
centroCusto: '',
|
|
|
|
|
codigoGrupo: '',
|
|
|
|
|
codigoSubgrupo: '',
|
|
|
|
|
codigoConta: '',
|
|
|
|
|
});
|
2025-10-08 05:30:37 +00:00
|
|
|
const [linhaSelecionada, setLinhaSelecionada] = useState<string | null>(null);
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
fetchData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
2025-10-08 05:08:35 +00:00
|
|
|
setError(null);
|
2025-10-08 04:19:15 +00:00
|
|
|
const response = await fetch('/api/dre');
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
if (!response.ok) {
|
2025-10-08 05:08:35 +00:00
|
|
|
throw new Error(`Erro ao carregar dados: ${response.status}`);
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const result = await response.json();
|
|
|
|
|
setData(result);
|
|
|
|
|
|
|
|
|
|
// Extrair meses únicos dos dados
|
|
|
|
|
const meses = [
|
|
|
|
|
...new Set(
|
|
|
|
|
result.map((item: DREItem) => {
|
|
|
|
|
const dataCompetencia = new Date(item.data_competencia);
|
|
|
|
|
return `${dataCompetencia.getFullYear()}-${String(
|
|
|
|
|
dataCompetencia.getMonth() + 1
|
|
|
|
|
).padStart(2, '0')}`;
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
].sort() as string[];
|
|
|
|
|
|
|
|
|
|
setMesesDisponiveis(meses);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Erro desconhecido');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (value: string | number) => {
|
|
|
|
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
|
|
|
return numValue.toLocaleString('pt-BR', {
|
|
|
|
|
style: 'currency',
|
|
|
|
|
currency: 'BRL',
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 12:28:20 +00:00
|
|
|
const formatCurrencyWithColor = (value: string | number) => {
|
|
|
|
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
|
|
|
const formatted = formatCurrency(value);
|
|
|
|
|
const isNegative = numValue < 0;
|
|
|
|
|
return { formatted, isNegative };
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
// Função para extrair códigos dos grupos e subgrupos
|
|
|
|
|
const extractCodes = (grupo: string, subgrupo?: string) => {
|
|
|
|
|
const grupoMatch = grupo.match(/^(\d+)/);
|
|
|
|
|
const codigoGrupo = grupoMatch ? grupoMatch[1] : '';
|
|
|
|
|
|
|
|
|
|
let codigoSubgrupo = '';
|
|
|
|
|
if (subgrupo) {
|
2025-10-08 05:40:47 +00:00
|
|
|
// Primeiro tenta extrair código numérico (ex: "001.008 - LOJA 8" -> "001.008")
|
2025-10-08 05:08:35 +00:00
|
|
|
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
|
2025-10-08 05:40:47 +00:00
|
|
|
if (subgrupoMatch) {
|
|
|
|
|
codigoSubgrupo = subgrupoMatch[1];
|
|
|
|
|
} else {
|
|
|
|
|
// Se não tem código numérico, usa a descrição completa (ex: "DESPESAS ADMINISTRATIVAS")
|
|
|
|
|
codigoSubgrupo = subgrupo;
|
|
|
|
|
}
|
2025-10-08 05:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { codigoGrupo, codigoSubgrupo };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função para lidar com clique nas linhas
|
2025-10-08 05:30:37 +00:00
|
|
|
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
|
2025-10-08 05:08:35 +00:00
|
|
|
if (!data.length) return;
|
|
|
|
|
|
|
|
|
|
// Pegar todas as datas disponíveis para definir o período
|
|
|
|
|
const datas = data.map((item) => item.data_competencia);
|
|
|
|
|
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
|
|
|
|
|
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
|
|
|
|
|
|
|
|
|
|
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7); // YYYY-MM
|
|
|
|
|
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7); // YYYY-MM
|
|
|
|
|
|
|
|
|
|
const { codigoGrupo, codigoSubgrupo } = extractCodes(
|
|
|
|
|
row.grupo || '',
|
|
|
|
|
row.subgrupo
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-08 05:30:37 +00:00
|
|
|
// Criar um identificador único para a linha
|
|
|
|
|
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
|
|
|
|
|
row.centro_custo || ''
|
|
|
|
|
}-${row.codigo_conta || ''}`;
|
|
|
|
|
setLinhaSelecionada(linhaId);
|
|
|
|
|
|
|
|
|
|
// Se um mês específico foi selecionado, usar apenas esse mês
|
|
|
|
|
const dataInicioFiltro = mesSelecionado || dataInicioStr;
|
|
|
|
|
const dataFimFiltro = mesSelecionado || dataFimStr;
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
setAnaliticoFiltros({
|
2025-10-08 05:30:37 +00:00
|
|
|
dataInicio: dataInicioFiltro,
|
|
|
|
|
dataFim: dataFimFiltro,
|
2025-10-08 05:08:35 +00:00
|
|
|
centroCusto: row.centro_custo || '',
|
|
|
|
|
codigoGrupo,
|
|
|
|
|
codigoSubgrupo,
|
|
|
|
|
codigoConta: row.codigo_conta?.toString() || '',
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const toggleGroup = (grupo: string) => {
|
|
|
|
|
const newExpanded = new Set(expandedGroups);
|
|
|
|
|
if (newExpanded.has(grupo)) {
|
|
|
|
|
newExpanded.delete(grupo);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(grupo);
|
|
|
|
|
}
|
|
|
|
|
setExpandedGroups(newExpanded);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleSubgrupo = (subgrupo: string) => {
|
|
|
|
|
const newExpanded = new Set(expandedSubgrupos);
|
|
|
|
|
if (newExpanded.has(subgrupo)) {
|
|
|
|
|
newExpanded.delete(subgrupo);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(subgrupo);
|
|
|
|
|
}
|
|
|
|
|
setExpandedSubgrupos(newExpanded);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleCentro = (centro: string) => {
|
|
|
|
|
const newExpanded = new Set(expandedCentros);
|
|
|
|
|
if (newExpanded.has(centro)) {
|
|
|
|
|
newExpanded.delete(centro);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(centro);
|
|
|
|
|
}
|
|
|
|
|
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> = {};
|
|
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const dataCompetencia = new Date(item.data_competencia);
|
|
|
|
|
const anoMes = `${dataCompetencia.getFullYear()}-${String(
|
|
|
|
|
dataCompetencia.getMonth() + 1
|
|
|
|
|
).padStart(2, '0')}`;
|
|
|
|
|
|
|
|
|
|
if (!valoresPorMes[anoMes]) {
|
|
|
|
|
valoresPorMes[anoMes] = 0;
|
|
|
|
|
}
|
|
|
|
|
valoresPorMes[anoMes] += parseFloat(item.valor);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return valoresPorMes;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildHierarchicalData = (): HierarchicalRow[] => {
|
|
|
|
|
const rows: HierarchicalRow[] = [];
|
|
|
|
|
|
|
|
|
|
// Agrupar por grupo
|
|
|
|
|
const grupos = data.reduce((acc, item) => {
|
|
|
|
|
if (!acc[item.grupo]) {
|
|
|
|
|
acc[item.grupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.grupo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} 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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedGrupos.forEach(([grupo, items]) => {
|
|
|
|
|
const totalGrupo = items.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha do grupo
|
|
|
|
|
rows.push({
|
|
|
|
|
type: 'grupo',
|
|
|
|
|
level: 0,
|
|
|
|
|
grupo,
|
|
|
|
|
total: totalGrupo,
|
|
|
|
|
isExpanded: expandedGroups.has(grupo),
|
|
|
|
|
valoresPorMes: calcularValoresPorMes(items),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedGroups.has(grupo)) {
|
|
|
|
|
// Agrupar por subgrupo dentro do grupo
|
|
|
|
|
const subgrupos = items.reduce((acc, item) => {
|
|
|
|
|
if (!acc[item.subgrupo]) {
|
|
|
|
|
acc[item.subgrupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.subgrupo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} 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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
|
|
|
|
|
const totalSubgrupo = subgrupoItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha do subgrupo
|
|
|
|
|
rows.push({
|
|
|
|
|
type: 'subgrupo',
|
|
|
|
|
level: 1,
|
|
|
|
|
grupo,
|
|
|
|
|
subgrupo,
|
|
|
|
|
total: totalSubgrupo,
|
|
|
|
|
isExpanded: expandedSubgrupos.has(`${grupo}-${subgrupo}`),
|
|
|
|
|
valoresPorMes: calcularValoresPorMes(subgrupoItems),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedSubgrupos.has(`${grupo}-${subgrupo}`)) {
|
|
|
|
|
// Agrupar por centro de custo dentro do subgrupo
|
|
|
|
|
const centros = subgrupoItems.reduce((acc, item) => {
|
|
|
|
|
if (!acc[item.centro_custo]) {
|
|
|
|
|
acc[item.centro_custo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.centro_custo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} 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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedCentros.forEach(([centro, centroItems]) => {
|
|
|
|
|
const totalCentro = centroItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha do centro de custo
|
|
|
|
|
rows.push({
|
|
|
|
|
type: 'centro_custo',
|
|
|
|
|
level: 2,
|
|
|
|
|
grupo,
|
|
|
|
|
subgrupo,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
total: totalCentro,
|
|
|
|
|
isExpanded: expandedCentros.has(
|
|
|
|
|
`${grupo}-${subgrupo}-${centro}`
|
|
|
|
|
),
|
|
|
|
|
valoresPorMes: calcularValoresPorMes(centroItems),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedCentros.has(`${grupo}-${subgrupo}-${centro}`)) {
|
|
|
|
|
// Agrupar por conta dentro do centro de custo
|
|
|
|
|
const contas = centroItems.reduce((acc, item) => {
|
|
|
|
|
if (!acc[item.conta]) {
|
|
|
|
|
acc[item.conta] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.conta].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} 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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedContas.forEach(([conta, contaItems]) => {
|
|
|
|
|
const totalConta = contaItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha da conta (sem ano/mês no nome)
|
|
|
|
|
rows.push({
|
|
|
|
|
type: 'conta',
|
|
|
|
|
level: 3,
|
|
|
|
|
grupo,
|
|
|
|
|
subgrupo,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
conta,
|
|
|
|
|
codigo_conta: contaItems[0].codigo_conta,
|
|
|
|
|
total: totalConta,
|
|
|
|
|
valoresPorMes: calcularValoresPorMes(contaItems),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRowStyle = (row: HierarchicalRow) => {
|
|
|
|
|
const baseStyle = 'transition-colors hover:bg-muted/50';
|
|
|
|
|
|
2025-10-08 05:30:37 +00:00
|
|
|
// Criar identificador único para a linha
|
|
|
|
|
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
|
|
|
|
|
row.centro_custo || ''
|
|
|
|
|
}-${row.codigo_conta || ''}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
|
|
|
|
|
let style = baseStyle;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
style += ' bg-blue-100 border-l-4 border-blue-500 shadow-md';
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
switch (row.type) {
|
|
|
|
|
case 'grupo':
|
2025-10-08 05:30:37 +00:00
|
|
|
return `${style} bg-primary/5 font-semibold`;
|
2025-10-08 04:19:15 +00:00
|
|
|
case 'subgrupo':
|
2025-10-08 05:30:37 +00:00
|
|
|
return `${style} bg-primary/10 font-medium`;
|
2025-10-08 04:19:15 +00:00
|
|
|
case 'centro_custo':
|
2025-10-08 05:30:37 +00:00
|
|
|
return `${style} bg-secondary/30 font-medium`;
|
2025-10-08 04:19:15 +00:00
|
|
|
case 'conta':
|
2025-10-08 05:30:37 +00:00
|
|
|
return `${style} bg-muted/20`;
|
2025-10-08 04:19:15 +00:00
|
|
|
default:
|
2025-10-08 05:30:37 +00:00
|
|
|
return style;
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getIndentStyle = (level: number) => {
|
|
|
|
|
return { paddingLeft: `${level * 20}px` };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderCellContent = (row: HierarchicalRow) => {
|
|
|
|
|
switch (row.type) {
|
|
|
|
|
case 'grupo':
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleGroup(row.grupo!)}
|
|
|
|
|
className="p-1 hover:bg-muted rounded"
|
|
|
|
|
>
|
|
|
|
|
{row.isExpanded ? '▼' : '▶'}
|
|
|
|
|
</button>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<span className="font-semibold">{row.grupo}</span>
|
|
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
case 'subgrupo':
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
|
|
|
|
|
className="p-1 hover:bg-muted rounded"
|
|
|
|
|
>
|
|
|
|
|
{row.isExpanded ? '▼' : '▶'}
|
|
|
|
|
</button>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<span className="font-medium">{row.subgrupo}</span>
|
|
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
case 'centro_custo':
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
|
|
|
|
|
}
|
|
|
|
|
className="p-1 hover:bg-muted rounded"
|
|
|
|
|
>
|
|
|
|
|
{row.isExpanded ? '▼' : '▶'}
|
|
|
|
|
</button>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<span className="font-medium">{row.centro_custo}</span>
|
|
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
case 'conta':
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-muted-foreground">•</span>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
className="flex-1 text-left hover:bg-blue-50 p-1 rounded cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<span>{row.conta}</span>
|
|
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<div className="flex items-center justify-center h-64">
|
|
|
|
|
<div className="text-center">
|
2025-10-08 20:21:10 +00:00
|
|
|
<LoaderPinwheel className="h-8 w-8 animate-spin mx-auto mb-2" />
|
2025-10-08 04:19:15 +00:00
|
|
|
<p>Carregando dados...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hierarchicalData = buildHierarchicalData();
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-08 12:55:48 +00:00
|
|
|
<div className="w-full flex flex-col items-center gap-2">
|
|
|
|
|
<div className="mb-1">
|
|
|
|
|
<h1 className="text-lg font-bold">DRE Gerencial</h1>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-08 04:45:26 +00:00
|
|
|
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative">
|
2025-10-08 04:26:38 +00:00
|
|
|
{/* Header fixo separado */}
|
|
|
|
|
<div
|
|
|
|
|
className="sticky top-0 z-30 border-b shadow-sm"
|
|
|
|
|
style={{ backgroundColor: 'white', opacity: 1 }}
|
|
|
|
|
>
|
|
|
|
|
<div
|
2025-10-08 04:45:26 +00:00
|
|
|
className="flex p-3 font-semibold text-xs"
|
2025-10-08 04:26:38 +00:00
|
|
|
style={{ backgroundColor: 'white', opacity: 1 }}
|
|
|
|
|
>
|
2025-10-08 04:45:26 +00:00
|
|
|
<div className="flex-1 min-w-[200px] max-w-[300px]">
|
2025-10-08 04:19:15 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => handleSort('descricao')}
|
|
|
|
|
className="h-auto p-0 font-semibold"
|
|
|
|
|
>
|
|
|
|
|
Descrição
|
|
|
|
|
{getSortIcon('descricao')}
|
|
|
|
|
</Button>
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
2025-10-08 04:19:15 +00:00
|
|
|
{mesesDisponiveis.map((mes) => (
|
2025-10-08 04:45:26 +00:00
|
|
|
<div
|
|
|
|
|
key={mes}
|
|
|
|
|
className="flex-1 min-w-[120px] max-w-[150px] text-right px-2"
|
|
|
|
|
>
|
2025-10-08 04:19:15 +00:00
|
|
|
{mes}
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
2025-10-08 04:19:15 +00:00
|
|
|
))}
|
2025-10-08 04:45:26 +00:00
|
|
|
<div className="flex-1 min-w-[120px] max-w-[150px] text-right px-2">
|
2025-10-08 04:19:15 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => handleSort('valor')}
|
|
|
|
|
className="h-auto p-0 font-semibold"
|
|
|
|
|
>
|
|
|
|
|
Total
|
|
|
|
|
{getSortIcon('valor')}
|
|
|
|
|
</Button>
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-08 04:45:26 +00:00
|
|
|
<div className="flex flex-col">
|
|
|
|
|
{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)}
|
|
|
|
|
>
|
|
|
|
|
{renderCellContent(row)}
|
|
|
|
|
</div>
|
2025-10-08 04:19:15 +00:00
|
|
|
{mesesDisponiveis.map((mes) => (
|
2025-10-08 04:45:26 +00:00
|
|
|
<div
|
|
|
|
|
key={mes}
|
2025-10-08 05:30:37 +00:00
|
|
|
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"
|
|
|
|
|
onClick={() => handleRowClick(row, mes)}
|
2025-10-08 04:26:38 +00:00
|
|
|
>
|
2025-10-08 04:45:26 +00:00
|
|
|
{row.valoresPorMes && row.valoresPorMes[mes]
|
2025-10-08 12:28:20 +00:00
|
|
|
? (() => {
|
|
|
|
|
const { formatted, isNegative } =
|
|
|
|
|
formatCurrencyWithColor(row.valoresPorMes[mes]);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative ? 'text-red-600' : 'text-gray-900'
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()
|
2025-10-08 04:45:26 +00:00
|
|
|
: '-'}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2025-10-08 05:30:37 +00:00
|
|
|
<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"
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
>
|
2025-10-08 12:28:20 +00:00
|
|
|
{(() => {
|
|
|
|
|
const { formatted, isNegative } = formatCurrencyWithColor(
|
|
|
|
|
row.total!
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={isNegative ? 'text-red-600' : 'text-gray-900'}
|
|
|
|
|
>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
2025-10-08 04:45:26 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
2025-10-08 05:08:35 +00:00
|
|
|
|
|
|
|
|
{/* Componente Analítico */}
|
|
|
|
|
{!loading && data.length > 0 && (
|
|
|
|
|
<AnaliticoComponent filtros={analiticoFiltros} />
|
|
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|