2025-12-09 14:29:32 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2, Download } from "lucide-react";
|
|
|
|
|
import React, { useEffect, useState, useCallback, memo } from "react";
|
|
|
|
|
import AnaliticoComponent from "./analitico";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
import * as XLSX from "xlsx";
|
|
|
|
|
import {
|
|
|
|
|
Sheet,
|
|
|
|
|
SheetContent,
|
|
|
|
|
SheetDescription,
|
|
|
|
|
SheetFooter,
|
|
|
|
|
SheetHeader,
|
|
|
|
|
SheetTitle,
|
|
|
|
|
SheetTrigger,
|
|
|
|
|
} from "@/components/ui/sheet";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
|
|
|
|
|
interface DREItem {
|
|
|
|
|
codfilial: string;
|
|
|
|
|
data_competencia: string;
|
|
|
|
|
data_cai: string;
|
|
|
|
|
grupo: string;
|
|
|
|
|
codigo_grupo: string;
|
|
|
|
|
codigo_conta: number;
|
|
|
|
|
conta: string;
|
|
|
|
|
valor: string;
|
|
|
|
|
codgrupo?: string;
|
|
|
|
|
tipo?: string;
|
|
|
|
|
filial?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HierarchicalRow {
|
2025-12-09 19:19:55 +00:00
|
|
|
type: "grupo" | "conta" | "calculado";
|
2025-12-09 14:29:32 +00:00
|
|
|
level: number;
|
|
|
|
|
grupo?: string;
|
|
|
|
|
codigo_grupo?: string;
|
|
|
|
|
conta?: string;
|
|
|
|
|
codigo_conta?: number;
|
|
|
|
|
total?: number;
|
|
|
|
|
isExpanded?: boolean;
|
|
|
|
|
valoresPorMes?: Record<string, number>;
|
2025-12-09 20:41:58 +00:00
|
|
|
valoresPorMesPorFilial?: Record<string, Record<string, number>>; // mes -> filial -> valor
|
2025-12-09 14:29:32 +00:00
|
|
|
percentuaisPorMes?: Record<string, number>;
|
2025-12-09 20:41:58 +00:00
|
|
|
percentuaisPorMesPorFilial?: Record<string, Record<string, number>>; // mes -> filial -> percentual
|
2025-12-09 14:29:32 +00:00
|
|
|
percentualTotal?: number;
|
2025-12-09 19:19:55 +00:00
|
|
|
isCalculado?: boolean;
|
2025-12-09 14:29:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Componente memoizado para linhas da tabela
|
|
|
|
|
const TableRow = memo(({
|
|
|
|
|
row,
|
|
|
|
|
index,
|
|
|
|
|
handleRowClick,
|
|
|
|
|
getRowStyle,
|
|
|
|
|
getIndentStyle,
|
|
|
|
|
renderCellContent,
|
|
|
|
|
mesesDisponiveis,
|
2025-12-09 20:41:58 +00:00
|
|
|
opcoesFiliais,
|
2025-12-09 20:58:11 +00:00
|
|
|
filiaisSelecionadas,
|
|
|
|
|
filtrosAplicados,
|
2025-12-09 14:29:32 +00:00
|
|
|
formatCurrency,
|
|
|
|
|
formatCurrencyWithColor,
|
|
|
|
|
getFixedCellBackground
|
|
|
|
|
}: {
|
|
|
|
|
row: HierarchicalRow;
|
|
|
|
|
index: number;
|
2025-12-09 21:21:15 +00:00
|
|
|
handleRowClick: (row: HierarchicalRow, mes?: string, filial?: string) => void;
|
2025-12-09 14:29:32 +00:00
|
|
|
getRowStyle: (row: HierarchicalRow) => string;
|
|
|
|
|
getIndentStyle: (level: number) => React.CSSProperties;
|
|
|
|
|
renderCellContent: (row: HierarchicalRow) => React.ReactNode;
|
|
|
|
|
mesesDisponiveis: string[];
|
2025-12-09 20:41:58 +00:00
|
|
|
opcoesFiliais: string[];
|
2025-12-09 20:58:11 +00:00
|
|
|
filiaisSelecionadas: string[];
|
|
|
|
|
filtrosAplicados: boolean;
|
2025-12-09 14:29:32 +00:00
|
|
|
formatCurrency: (value: number) => string;
|
|
|
|
|
formatCurrencyWithColor: (value: number) => { formatted: string; isNegative: boolean };
|
|
|
|
|
getFixedCellBackground: (row: HierarchicalRow) => string;
|
|
|
|
|
}) => {
|
|
|
|
|
return (
|
|
|
|
|
<tr
|
|
|
|
|
className={`text-sm border-b border-gray-100 hover:bg-gray-50 transition-all duration-200 ease-in-out ${getRowStyle(
|
|
|
|
|
row
|
|
|
|
|
)}`}
|
|
|
|
|
>
|
|
|
|
|
<td
|
|
|
|
|
className={`px-4 py-1 w-[300px] min-w-[300px] whitespace-nowrap overflow-hidden sticky left-0 z-10 ${getFixedCellBackground(row)} shadow-[2px_0_4px_rgba(0,0,0,0.05)] cursor-pointer`}
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
>
|
|
|
|
|
<div style={getIndentStyle(row.level)}>
|
|
|
|
|
{renderCellContent(row)}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
|
2025-12-09 20:41:58 +00:00
|
|
|
{/* Colunas de valores por mês e por filial - cada filial tem suas próprias colunas */}
|
2025-12-10 12:00:06 +00:00
|
|
|
{mesesDisponiveis.map((mes) => {
|
|
|
|
|
const filiaisParaMes = (filtrosAplicados && filiaisSelecionadas.length > 0)
|
|
|
|
|
? filiaisSelecionadas
|
|
|
|
|
: (opcoesFiliais.length > 0 ? opcoesFiliais : ['']);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<React.Fragment key={mes}>
|
|
|
|
|
{/* Colunas de filiais para este mês */}
|
|
|
|
|
{filiaisParaMes.map((filial: string) => {
|
|
|
|
|
// Só exibir se a filial estiver selecionada ou se não houver filtros aplicados
|
|
|
|
|
if (filtrosAplicados && filiaisSelecionadas.length > 0 && !filiaisSelecionadas.includes(filial)) {
|
|
|
|
|
return null;
|
2025-12-09 20:41:58 +00:00
|
|
|
}
|
2025-12-10 12:00:06 +00:00
|
|
|
return (
|
|
|
|
|
<React.Fragment key={`${mes}-${filial || 'default'}`}>
|
|
|
|
|
<td
|
|
|
|
|
className="px-2 py-1 text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[120px] min-w-[120px]"
|
|
|
|
|
onClick={() => handleRowClick(row, mes, filial)}
|
|
|
|
|
title={
|
|
|
|
|
filial && row.valoresPorMesPorFilial?.[mes]?.[filial] !== undefined
|
|
|
|
|
? formatCurrency(row.valoresPorMesPorFilial[mes][filial])
|
|
|
|
|
: row.valoresPorMes?.[mes] !== undefined
|
|
|
|
|
? formatCurrency(row.valoresPorMes[mes])
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{filial && row.valoresPorMesPorFilial?.[mes]?.[filial] !== undefined && row.valoresPorMesPorFilial[mes][filial] !== 0 ? (
|
|
|
|
|
(() => {
|
|
|
|
|
const valor = row.valoresPorMesPorFilial[mes][filial];
|
|
|
|
|
const { formatted, isNegative } = formatCurrencyWithColor(valor);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative
|
|
|
|
|
? "text-red-600 font-bold"
|
|
|
|
|
: "text-gray-900"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()
|
|
|
|
|
) : !filial && row.valoresPorMes?.[mes] !== undefined ? (
|
|
|
|
|
(() => {
|
|
|
|
|
const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative
|
|
|
|
|
? "text-red-600 font-bold"
|
|
|
|
|
: "text-gray-900"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-400">-</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td
|
|
|
|
|
className="px-2 py-1 text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[100px] min-w-[100px]"
|
|
|
|
|
onClick={() => handleRowClick(row, mes, filial)}
|
|
|
|
|
title={
|
|
|
|
|
filial && row.percentuaisPorMesPorFilial?.[mes]?.[filial] !== undefined
|
|
|
|
|
? `${row.percentuaisPorMesPorFilial[mes][filial].toFixed(1)}%`
|
|
|
|
|
: row.percentuaisPorMes?.[mes] !== undefined
|
|
|
|
|
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{filial && row.percentuaisPorMesPorFilial?.[mes]?.[filial] !== undefined && row.percentuaisPorMesPorFilial[mes][filial] !== 0 ? (
|
|
|
|
|
`${row.percentuaisPorMesPorFilial[mes][filial].toFixed(1)}%`
|
|
|
|
|
) : !filial && row.percentuaisPorMes?.[mes] !== undefined ? (
|
|
|
|
|
`${row.percentuaisPorMes[mes].toFixed(1)}%`
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-400">-</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* Colunas de totalizador para este mês */}
|
|
|
|
|
<td
|
|
|
|
|
className="px-2 py-1 text-right font-bold cursor-pointer hover:bg-green-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[120px] min-w-[120px] bg-green-50/30"
|
|
|
|
|
onClick={() => handleRowClick(row, mes)}
|
|
|
|
|
title={row.valoresPorMes?.[mes] !== undefined ? formatCurrency(row.valoresPorMes[mes]) : "-"}
|
2025-12-09 20:41:58 +00:00
|
|
|
>
|
2025-12-10 12:00:06 +00:00
|
|
|
{row.valoresPorMes?.[mes] !== undefined ? (
|
2025-12-09 20:41:58 +00:00
|
|
|
(() => {
|
|
|
|
|
const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative
|
|
|
|
|
? "text-red-600 font-bold"
|
2025-12-10 12:00:06 +00:00
|
|
|
: "text-green-700 font-bold"
|
2025-12-09 20:41:58 +00:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-gray-400">-</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td
|
2025-12-10 12:00:06 +00:00
|
|
|
className="px-2 py-1 text-center font-bold cursor-pointer hover:bg-green-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[100px] min-w-[100px] bg-green-50/30"
|
|
|
|
|
onClick={() => handleRowClick(row, mes)}
|
2025-12-09 20:41:58 +00:00
|
|
|
title={
|
2025-12-10 12:00:06 +00:00
|
|
|
row.percentuaisPorMes?.[mes] !== undefined
|
2025-12-09 20:41:58 +00:00
|
|
|
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
|
|
|
|
>
|
2025-12-10 12:00:06 +00:00
|
|
|
{row.percentuaisPorMes?.[mes] !== undefined
|
|
|
|
|
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
|
|
|
|
: <span className="text-gray-400">-</span>}
|
2025-12-09 20:41:58 +00:00
|
|
|
</td>
|
|
|
|
|
</React.Fragment>
|
2025-12-09 20:58:11 +00:00
|
|
|
);
|
2025-12-10 12:00:06 +00:00
|
|
|
})}
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
{/* Coluna Total */}
|
|
|
|
|
<td
|
|
|
|
|
className="px-4 py-1 text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[120px] min-w-[120px]"
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
title={row.total ? formatCurrency(row.total) : "-"}
|
|
|
|
|
>
|
|
|
|
|
{(() => {
|
|
|
|
|
const { formatted, isNegative } = formatCurrencyWithColor(
|
|
|
|
|
row.total!
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative ? "text-red-600 font-bold" : "text-gray-900"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
{/* Coluna Percentual Total */}
|
|
|
|
|
<td
|
|
|
|
|
className="px-2 py-1 text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[100px] min-w-[100px]"
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
title={
|
|
|
|
|
row.percentualTotal !== undefined
|
|
|
|
|
? `${row.percentualTotal.toFixed(1)}%`
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{row.percentualTotal !== undefined
|
|
|
|
|
? `${row.percentualTotal.toFixed(1)}%`
|
|
|
|
|
: "-"}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
TableRow.displayName = 'TableRow';
|
|
|
|
|
|
|
|
|
|
export default function Teste() {
|
|
|
|
|
const [data, setData] = useState<DREItem[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [expandedGrupos, setExpandedGrupos] = useState<Set<string>>(new Set());
|
|
|
|
|
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
// Estados para filtros
|
|
|
|
|
const [filtros, setFiltros] = useState({
|
|
|
|
|
periodoDe: "",
|
|
|
|
|
periodoAte: "",
|
2025-12-09 20:22:48 +00:00
|
|
|
filial: "Todas",
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Estados para multi-seleção
|
2025-12-09 20:22:48 +00:00
|
|
|
const [filiaisSelecionadas, setFiliaisSelecionadas] = useState<string[]>([]);
|
2025-12-09 14:29:32 +00:00
|
|
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
|
|
|
const [dadosFiltrados, setDadosFiltrados] = useState<DREItem[]>([]);
|
|
|
|
|
const [filtrosAplicados, setFiltrosAplicados] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Estados para opções dos filtros
|
|
|
|
|
const [opcoesGrupos, setOpcoesGrupos] = useState<string[]>([]);
|
2025-12-09 20:22:48 +00:00
|
|
|
const [opcoesFiliais, setOpcoesFiliais] = useState<string[]>([]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
// Estados para filtros de busca nos campos de seleção
|
2025-12-09 20:22:48 +00:00
|
|
|
const [filtroFilial, setFiltroFilial] = useState<string>("");
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
// Estados para analítico
|
|
|
|
|
const [analiticoFiltros, setAnaliticoFiltros] = useState({
|
|
|
|
|
dataInicio: "",
|
|
|
|
|
dataFim: "",
|
|
|
|
|
centroCusto: "",
|
|
|
|
|
codigoGrupo: "",
|
|
|
|
|
codigoSubgrupo: "",
|
|
|
|
|
codigoConta: "",
|
2025-12-09 21:21:15 +00:00
|
|
|
codFilial: "",
|
2025-12-09 14:29:32 +00:00
|
|
|
linhaSelecionada: "",
|
|
|
|
|
excluirCentroCusto: "",
|
|
|
|
|
excluirCodigoConta: "",
|
|
|
|
|
codigosCentrosCustoSelecionados: "",
|
|
|
|
|
codigosContasSelecionadas: "",
|
|
|
|
|
});
|
|
|
|
|
const [linhaSelecionada, setLinhaSelecionada] = useState<string | null>(null);
|
|
|
|
|
const [isAllExpanded, setIsAllExpanded] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// Carregar períodos disponíveis da API
|
|
|
|
|
carregarPeriodosDisponiveis();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const carregarPeriodosDisponiveis = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/dre-filial-oracle");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro HTTP: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dadosCompletos = await response.json();
|
|
|
|
|
|
|
|
|
|
// Extrair períodos únicos dos dados
|
|
|
|
|
const periodosUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.data_competencia))].sort() as string[];
|
|
|
|
|
setMesesDisponiveis(periodosUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair grupos únicos
|
|
|
|
|
const gruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.grupo))].sort() as string[];
|
|
|
|
|
setOpcoesGrupos(gruposUnicos);
|
|
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
// Extrair filiais únicas
|
|
|
|
|
const filiaisUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.filial || item.codfilial).filter(Boolean))].sort() as string[];
|
|
|
|
|
setOpcoesFiliais(filiaisUnicas);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
// Inicializar com todas as filiais selecionadas
|
|
|
|
|
setFiliaisSelecionadas(filiaisUnicas);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 14:46:20 +00:00
|
|
|
// Inicializar filtros de período com o ano corrente
|
|
|
|
|
const agora = new Date();
|
|
|
|
|
const anoAtual = agora.getFullYear();
|
|
|
|
|
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const periodoAtual = `${anoAtual}-${mesAtual}`;
|
|
|
|
|
const primeiroMesAno = `${anoAtual}-01`;
|
|
|
|
|
|
|
|
|
|
// Verificar se os períodos existem nos dados disponíveis
|
|
|
|
|
const periodoDeValido = periodosUnicos.includes(primeiroMesAno) ? primeiroMesAno : (periodosUnicos[0] || primeiroMesAno);
|
|
|
|
|
const periodoAteValido = periodosUnicos.includes(periodoAtual) ? periodoAtual : (periodosUnicos[periodosUnicos.length - 1] || periodoAtual);
|
|
|
|
|
|
|
|
|
|
setFiltros(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
periodoDe: periodoDeValido,
|
|
|
|
|
periodoAte: periodoAteValido
|
|
|
|
|
}));
|
|
|
|
|
|
2025-12-09 19:30:55 +00:00
|
|
|
// NÃO inicializar filtros do analítico - só serão definidos após clique em célula
|
2025-12-09 14:46:20 +00:00
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Erro ao carregar períodos:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
const response = await fetch("/api/dre-filial-oracle");
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro ao carregar dados: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
setData(result);
|
|
|
|
|
|
|
|
|
|
// Extrair meses únicos dos dados
|
|
|
|
|
const meses = [
|
|
|
|
|
...new Set(
|
|
|
|
|
result.map((item: DREItem) => {
|
|
|
|
|
return item.data_competencia;
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
].sort() as string[];
|
|
|
|
|
|
|
|
|
|
setMesesDisponiveis(meses);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "Erro desconhecido");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const formatCurrency = React.useCallback((value: string | number) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
|
|
|
|
return numValue.toLocaleString("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
});
|
2025-12-09 20:58:11 +00:00
|
|
|
}, []);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const formatCurrencyWithColor = React.useCallback((value: string | number) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
|
|
|
|
const formatted = formatCurrency(value);
|
|
|
|
|
const isNegative = numValue < 0;
|
|
|
|
|
return { formatted, isNegative };
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [formatCurrency]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
// Função para lidar com clique nas linhas
|
2025-12-09 21:21:15 +00:00
|
|
|
const handleRowClick = React.useCallback((row: HierarchicalRow, mesSelecionado?: string, filialSelecionada?: string) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
console.log('🖱️ Clique na linha:', row);
|
|
|
|
|
console.log('📅 Mês selecionado:', mesSelecionado);
|
2025-12-10 12:30:21 +00:00
|
|
|
console.log('🏢 Filial selecionada:', filialSelecionada);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
// Linhas calculadas não devem abrir o componente analítico
|
|
|
|
|
if (row.type === "calculado") {
|
|
|
|
|
console.log('⚠️ Linha calculada - não abre componente analítico');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
if (!data.length) {
|
|
|
|
|
console.log('⚠️ Sem dados disponíveis');
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
// Se um mês específico foi selecionado, usar apenas esse mês
|
|
|
|
|
const dataInicioFiltro = mesSelecionado || dataInicioStr;
|
|
|
|
|
const dataFimFiltro = mesSelecionado || dataFimStr;
|
|
|
|
|
|
|
|
|
|
// Determinar filtros baseado na hierarquia [grupo, conta]
|
|
|
|
|
let codigoGrupoFiltro = "";
|
|
|
|
|
let codigoContaFiltro = "";
|
|
|
|
|
|
|
|
|
|
if (row.type === "grupo" || row.type === "conta") {
|
|
|
|
|
// Buscar o CODGRUPO dos dados originais que correspondem a esta linha
|
|
|
|
|
const itemsCorrespondentes = data.filter((item: DREItem) => {
|
|
|
|
|
// Filtrar por período se um mês específico foi selecionado
|
|
|
|
|
if (mesSelecionado && item.data_competencia !== mesSelecionado) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 12:30:21 +00:00
|
|
|
// Se é um totalizador (mesSelecionado presente mas filialSelecionada não), não filtrar por filial
|
|
|
|
|
// Caso contrário, filtrar por filial se especificada
|
|
|
|
|
if (filialSelecionada) {
|
|
|
|
|
const itemFilial = item.filial || item.codfilial || "";
|
|
|
|
|
if (itemFilial !== filialSelecionada) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
if (row.type === "grupo") {
|
|
|
|
|
return item.codigo_grupo === row.codigo_grupo || item.codgrupo === row.codigo_grupo;
|
|
|
|
|
} else if (row.type === "conta") {
|
|
|
|
|
return (item.codigo_grupo === row.codigo_grupo || item.codgrupo === row.codigo_grupo) &&
|
|
|
|
|
item.codigo_conta === row.codigo_conta;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Pegar o CODGRUPO do primeiro item encontrado
|
|
|
|
|
if (itemsCorrespondentes.length > 0) {
|
|
|
|
|
const primeiroItem = itemsCorrespondentes[0];
|
|
|
|
|
codigoGrupoFiltro = primeiroItem.codgrupo || primeiroItem.codigo_grupo || "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtrar por conta se for nível conta
|
|
|
|
|
if (row.type === "conta") {
|
|
|
|
|
codigoContaFiltro = row.codigo_conta?.toString() || "";
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 21:21:15 +00:00
|
|
|
// Determinar CODFILIAL baseado na filial selecionada
|
2025-12-10 12:34:49 +00:00
|
|
|
// Se é um totalizador (mesSelecionado presente mas filialSelecionada não), incluir todas as filiais selecionadas
|
2025-12-09 21:21:15 +00:00
|
|
|
let codFilialFiltro = "";
|
|
|
|
|
if (filialSelecionada) {
|
|
|
|
|
// Se a filial selecionada já é um código numérico, usar diretamente
|
|
|
|
|
// Caso contrário, buscar o CODFILIAL correspondente ao nome da filial nos dados
|
|
|
|
|
if (/^\d+$/.test(filialSelecionada)) {
|
|
|
|
|
codFilialFiltro = filialSelecionada;
|
|
|
|
|
} else {
|
|
|
|
|
// Buscar o CODFILIAL correspondente ao nome da filial nos dados
|
|
|
|
|
const itemComFilial = data.find((item: DREItem) => {
|
|
|
|
|
const itemFilial = item.filial || item.codfilial || "";
|
|
|
|
|
return itemFilial === filialSelecionada;
|
|
|
|
|
});
|
|
|
|
|
if (itemComFilial) {
|
|
|
|
|
codFilialFiltro = itemComFilial.codfilial || filialSelecionada;
|
|
|
|
|
} else {
|
|
|
|
|
// Se não encontrar, tentar usar o próprio valor como código
|
|
|
|
|
codFilialFiltro = filialSelecionada;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-10 12:34:49 +00:00
|
|
|
} else if (mesSelecionado) {
|
|
|
|
|
// Se mesSelecionado está presente mas filialSelecionada não, é um totalizador
|
|
|
|
|
// Incluir todas as filiais selecionadas (ou todas disponíveis se nenhuma estiver selecionada)
|
|
|
|
|
const filiaisParaTotalizador = filiaisSelecionadas.length > 0
|
|
|
|
|
? filiaisSelecionadas
|
|
|
|
|
: opcoesFiliais;
|
|
|
|
|
|
|
|
|
|
// Converter nomes de filiais para códigos CODFILIAL
|
|
|
|
|
const codigosFiliais = filiaisParaTotalizador.map(filial => {
|
|
|
|
|
if (/^\d+$/.test(filial)) {
|
|
|
|
|
return filial;
|
|
|
|
|
} else {
|
|
|
|
|
const itemComFilial = data.find((item: DREItem) => {
|
|
|
|
|
const itemFilial = item.filial || item.codfilial || "";
|
|
|
|
|
return itemFilial === filial;
|
|
|
|
|
});
|
|
|
|
|
return itemComFilial?.codfilial || filial;
|
|
|
|
|
}
|
|
|
|
|
}).filter(Boolean);
|
|
|
|
|
|
|
|
|
|
// Passar como string separada por vírgula para a API processar com IN
|
|
|
|
|
codFilialFiltro = codigosFiliais.join(',');
|
|
|
|
|
console.log('📊 Totalizador: incluindo todas as filiais selecionadas:', codigosFiliais);
|
2025-12-09 21:21:15 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
const novosFiltros = {
|
|
|
|
|
dataInicio: dataInicioFiltro,
|
|
|
|
|
dataFim: dataFimFiltro,
|
|
|
|
|
centroCusto: "", // Não aplicável na hierarquia filial
|
|
|
|
|
codigoGrupo: codigoGrupoFiltro,
|
|
|
|
|
codigoSubgrupo: "", // Não aplicável na hierarquia filial
|
|
|
|
|
codigoConta: codigoContaFiltro,
|
2025-12-10 12:30:21 +00:00
|
|
|
codFilial: codFilialFiltro, // Vazio para totalizador = todas as filiais do mês
|
2025-12-09 14:29:32 +00:00
|
|
|
linhaSelecionada: row.grupo || row.conta || "",
|
|
|
|
|
excluirCentroCusto: "",
|
|
|
|
|
excluirCodigoConta: "",
|
|
|
|
|
codigosCentrosCustoSelecionados: "",
|
2025-12-09 20:22:48 +00:00
|
|
|
codigosContasSelecionadas: "",
|
2025-12-09 14:29:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('🎯 Novos filtros para analítico:', novosFiltros);
|
2025-12-10 12:30:21 +00:00
|
|
|
console.log('📊 É totalizador?', mesSelecionado && !filialSelecionada ? 'SIM' : 'NÃO');
|
2025-12-09 14:29:32 +00:00
|
|
|
setAnaliticoFiltros(novosFiltros);
|
2025-12-10 12:34:49 +00:00
|
|
|
}, [data, filiaisSelecionadas, opcoesFiliais]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
const toggleGrupo = useCallback((codigoGrupo: string) => {
|
|
|
|
|
setExpandedGrupos(prev => {
|
|
|
|
|
const newExpanded = new Set(prev);
|
|
|
|
|
if (newExpanded.has(codigoGrupo)) {
|
|
|
|
|
newExpanded.delete(codigoGrupo);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(codigoGrupo);
|
|
|
|
|
}
|
|
|
|
|
return newExpanded;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
const toggleFilial = (filial: string) => {
|
|
|
|
|
setFiliaisSelecionadas(prev => {
|
|
|
|
|
if (prev.includes(filial)) {
|
|
|
|
|
return prev.filter(f => f !== filial);
|
2025-12-09 14:29:32 +00:00
|
|
|
} else {
|
2025-12-09 20:22:48 +00:00
|
|
|
return [...prev, filial];
|
2025-12-09 14:29:32 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
const selecionarTodasFiliais = () => {
|
|
|
|
|
setFiliaisSelecionadas(opcoesFiliais);
|
2025-12-09 14:29:32 +00:00
|
|
|
};
|
|
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
const limparFiliais = () => {
|
|
|
|
|
setFiliaisSelecionadas([]);
|
2025-12-09 14:29:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função auxiliar para calcular valores por mês
|
2025-12-09 20:58:11 +00:00
|
|
|
const calcularValoresPorMes = React.useCallback((items: DREItem[]): Record<string, number> => {
|
2025-12-09 14:29:32 +00:00
|
|
|
const valoresPorMes: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
valoresPorMes[mes] = 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const anoMes = item.data_competencia;
|
|
|
|
|
if (anoMes && valoresPorMes[anoMes] !== undefined) {
|
|
|
|
|
valoresPorMes[anoMes] += parseFloat(item.valor);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return valoresPorMes;
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [mesesDisponiveis]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:41:58 +00:00
|
|
|
// Função auxiliar para calcular valores por mês e por filial
|
2025-12-09 20:58:11 +00:00
|
|
|
const calcularValoresPorMesPorFilial = React.useCallback((items: DREItem[]): Record<string, Record<string, number>> => {
|
2025-12-09 20:41:58 +00:00
|
|
|
const valoresPorMesPorFilial: Record<string, Record<string, number>> = {};
|
|
|
|
|
|
2025-12-10 12:45:06 +00:00
|
|
|
// Extrair filiais únicas dos próprios items para evitar dependência de arrays externos
|
|
|
|
|
const filiaisDisponiveis = [...new Set(items.map(item => item.filial || item.codfilial).filter(Boolean))] as string[];
|
2025-12-09 20:41:58 +00:00
|
|
|
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
valoresPorMesPorFilial[mes] = {};
|
|
|
|
|
filiaisDisponiveis.forEach(filial => {
|
|
|
|
|
valoresPorMesPorFilial[mes][filial] = 0;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const anoMes = item.data_competencia;
|
|
|
|
|
const filial = item.filial || item.codfilial || "";
|
2025-12-10 12:45:06 +00:00
|
|
|
if (anoMes && valoresPorMesPorFilial[anoMes] && filial) {
|
2025-12-09 20:41:58 +00:00
|
|
|
if (!valoresPorMesPorFilial[anoMes][filial]) {
|
|
|
|
|
valoresPorMesPorFilial[anoMes][filial] = 0;
|
|
|
|
|
}
|
|
|
|
|
valoresPorMesPorFilial[anoMes][filial] += parseFloat(item.valor);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return valoresPorMesPorFilial;
|
2025-12-10 12:45:06 +00:00
|
|
|
}, [mesesDisponiveis]);
|
2025-12-09 20:58:11 +00:00
|
|
|
|
|
|
|
|
// Memoizar valores do grupo 01 por mês para evitar recálculos repetidos
|
|
|
|
|
const valoresGrupo01PorMesMemo = React.useMemo(() => {
|
|
|
|
|
const valores: Record<string, number> = {};
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
valores[mes] = data
|
|
|
|
|
.filter(item => {
|
|
|
|
|
const codgrupo = item.codgrupo || item.codigo_grupo || "";
|
|
|
|
|
return codgrupo === "01" && item.data_competencia === mes;
|
|
|
|
|
})
|
|
|
|
|
.reduce((sum, item) => sum + parseFloat(item.valor), 0);
|
|
|
|
|
});
|
|
|
|
|
return valores;
|
|
|
|
|
}, [data, mesesDisponiveis]);
|
|
|
|
|
|
|
|
|
|
// Memoizar valores do grupo 01 por mês e por filial
|
|
|
|
|
const valoresGrupo01PorMesPorFilialMemo = React.useMemo(() => {
|
|
|
|
|
const valores: Record<string, Record<string, number>> = {};
|
2025-12-10 12:45:06 +00:00
|
|
|
// Extrair filiais únicas dos próprios dados para evitar dependência de arrays externos
|
|
|
|
|
const filiaisDisponiveis = [...new Set(data.map(item => item.filial || item.codfilial).filter(Boolean))] as string[];
|
2025-12-09 20:58:11 +00:00
|
|
|
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
valores[mes] = {};
|
|
|
|
|
filiaisDisponiveis.forEach(filial => {
|
|
|
|
|
valores[mes][filial] = data
|
|
|
|
|
.filter(item => {
|
|
|
|
|
const codgrupo = item.codgrupo || item.codigo_grupo || "";
|
|
|
|
|
const itemFilial = item.filial || item.codfilial || "";
|
|
|
|
|
return codgrupo === "01" && item.data_competencia === mes && itemFilial === filial;
|
|
|
|
|
})
|
|
|
|
|
.reduce((sum, item) => sum + parseFloat(item.valor), 0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return valores;
|
2025-12-10 12:45:06 +00:00
|
|
|
}, [data, mesesDisponiveis]);
|
2025-12-09 20:41:58 +00:00
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
// Função para calcular percentuais baseado no CODGRUPO 01 (FATURAMENTO LÍQUIDO)
|
2025-12-09 20:58:11 +00:00
|
|
|
const calcularPercentuaisPorMes = React.useCallback((
|
2025-12-09 19:19:55 +00:00
|
|
|
valoresPorMes: Record<string, number>,
|
|
|
|
|
codigoGrupo?: string
|
2025-12-09 14:29:32 +00:00
|
|
|
): Record<string, number> => {
|
|
|
|
|
const percentuais: Record<string, number> = {};
|
|
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
// Se for CODGRUPO 01, sempre retornar 100%
|
|
|
|
|
if (codigoGrupo === "01") {
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
percentuais[mes] = 100;
|
|
|
|
|
});
|
|
|
|
|
return percentuais;
|
|
|
|
|
}
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
// Usar valores memoizados do grupo 01
|
2025-12-09 14:29:32 +00:00
|
|
|
Object.keys(valoresPorMes).forEach((mes) => {
|
|
|
|
|
const valorAtual = valoresPorMes[mes];
|
2025-12-09 20:58:11 +00:00
|
|
|
const valorGrupo01 = valoresGrupo01PorMesMemo[mes] || 0;
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
if (valorGrupo01 !== 0) {
|
|
|
|
|
percentuais[mes] = (valorAtual / valorGrupo01) * 100;
|
2025-12-09 14:29:32 +00:00
|
|
|
} else {
|
|
|
|
|
percentuais[mes] = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return percentuais;
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [mesesDisponiveis, valoresGrupo01PorMesMemo]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:41:58 +00:00
|
|
|
// Função para calcular percentuais por mês e por filial baseado no CODGRUPO 01
|
2025-12-09 20:58:11 +00:00
|
|
|
const calcularPercentuaisPorMesPorFilial = React.useCallback((
|
2025-12-09 20:41:58 +00:00
|
|
|
valoresPorMesPorFilial: Record<string, Record<string, number>>,
|
|
|
|
|
codigoGrupo?: string
|
|
|
|
|
): Record<string, Record<string, number>> => {
|
|
|
|
|
const percentuaisPorMesPorFilial: Record<string, Record<string, number>> = {};
|
|
|
|
|
|
2025-12-10 12:45:06 +00:00
|
|
|
// Extrair filiais únicas dos próprios valoresPorMesPorFilial para evitar dependência de arrays externos
|
|
|
|
|
const primeiroMes = mesesDisponiveis[0] || "";
|
|
|
|
|
const filiaisDisponiveis = Object.keys(valoresPorMesPorFilial[primeiroMes] || {});
|
2025-12-09 20:41:58 +00:00
|
|
|
|
|
|
|
|
// Se for CODGRUPO 01, sempre retornar 100% para todas as filiais
|
|
|
|
|
if (codigoGrupo === "01") {
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
percentuaisPorMesPorFilial[mes] = {};
|
|
|
|
|
filiaisDisponiveis.forEach(filial => {
|
|
|
|
|
percentuaisPorMesPorFilial[mes][filial] = 100;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return percentuaisPorMesPorFilial;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
// Usar valores memoizados do grupo 01
|
2025-12-09 20:41:58 +00:00
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
percentuaisPorMesPorFilial[mes] = {};
|
|
|
|
|
filiaisDisponiveis.forEach(filial => {
|
|
|
|
|
const valorAtual = valoresPorMesPorFilial[mes]?.[filial] || 0;
|
2025-12-09 20:58:11 +00:00
|
|
|
const valorGrupo01 = valoresGrupo01PorMesPorFilialMemo[mes]?.[filial] || 0;
|
2025-12-09 20:41:58 +00:00
|
|
|
|
|
|
|
|
if (valorGrupo01 !== 0) {
|
|
|
|
|
percentuaisPorMesPorFilial[mes][filial] = (valorAtual / valorGrupo01) * 100;
|
|
|
|
|
} else {
|
|
|
|
|
percentuaisPorMesPorFilial[mes][filial] = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return percentuaisPorMesPorFilial;
|
2025-12-10 12:45:06 +00:00
|
|
|
}, [mesesDisponiveis, valoresGrupo01PorMesPorFilialMemo]);
|
2025-12-09 20:41:58 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
// Memoizar total do grupo 01
|
|
|
|
|
const totalGrupo01Memo = React.useMemo(() => {
|
|
|
|
|
return data
|
2025-12-09 19:19:55 +00:00
|
|
|
.filter(item => {
|
|
|
|
|
const codgrupo = item.codgrupo || item.codigo_grupo || "";
|
|
|
|
|
return codgrupo === "01";
|
|
|
|
|
})
|
|
|
|
|
.reduce((sum, item) => sum + parseFloat(item.valor), 0);
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [data]);
|
|
|
|
|
|
|
|
|
|
// Função para calcular percentual do total baseado no CODGRUPO 01
|
|
|
|
|
const calcularPercentualTotal = React.useCallback((total: number, codigoGrupo?: string): number => {
|
|
|
|
|
// Se for CODGRUPO 01, sempre retornar 100%
|
|
|
|
|
if (codigoGrupo === "01") {
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
2025-12-09 19:19:55 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
// Usar total memoizado do grupo 01
|
|
|
|
|
if (totalGrupo01Memo !== 0) {
|
|
|
|
|
return (total / totalGrupo01Memo) * 100;
|
2025-12-09 14:29:32 +00:00
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [totalGrupo01Memo]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const buildHierarchicalData = React.useCallback((): HierarchicalRow[] => {
|
2025-12-09 14:29:32 +00:00
|
|
|
const rows: HierarchicalRow[] = [];
|
|
|
|
|
|
|
|
|
|
// Hierarquia simplificada: [grupo, conta]
|
|
|
|
|
// Agrupar por CODGRUPO
|
|
|
|
|
const gruposPorCodigo = data.reduce((acc, item) => {
|
|
|
|
|
const codgrupo = item.codgrupo || item.codigo_grupo || "";
|
|
|
|
|
if (!codgrupo) return acc;
|
|
|
|
|
if (!acc[codgrupo]) {
|
|
|
|
|
acc[codgrupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[codgrupo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
// Calcular valores por grupo para linhas calculadas
|
|
|
|
|
const valoresPorGrupo: Record<string, Record<string, number>> = {};
|
|
|
|
|
Object.keys(gruposPorCodigo).forEach(codgrupo => {
|
|
|
|
|
valoresPorGrupo[codgrupo] = calcularValoresPorMes(gruposPorCodigo[codgrupo]);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
// Ordenar por CODGRUPO (numericamente)
|
|
|
|
|
const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => {
|
|
|
|
|
const numA = parseInt(codA) || 0;
|
|
|
|
|
const numB = parseInt(codB) || 0;
|
|
|
|
|
if (numA !== numB) {
|
|
|
|
|
return numA - numB;
|
|
|
|
|
}
|
|
|
|
|
return codA.localeCompare(codB);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
sortedGrupos.forEach(([codgrupo, items], index) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
// Calcular total do grupo
|
|
|
|
|
const totalGrupo = items.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresGrupoPorMes = calcularValoresPorMes(items);
|
2025-12-09 20:41:58 +00:00
|
|
|
const valoresGrupoPorMesPorFilial = calcularValoresPorMesPorFilial(items);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
// Linha do grupo (Level 0)
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "grupo",
|
|
|
|
|
level: 0,
|
|
|
|
|
grupo: items[0]?.grupo || codgrupo,
|
|
|
|
|
codigo_grupo: codgrupo,
|
|
|
|
|
total: totalGrupo,
|
|
|
|
|
isExpanded: expandedGrupos.has(codgrupo),
|
|
|
|
|
valoresPorMes: valoresGrupoPorMes,
|
2025-12-09 20:41:58 +00:00
|
|
|
valoresPorMesPorFilial: valoresGrupoPorMesPorFilial,
|
2025-12-09 19:19:55 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresGrupoPorMes, codgrupo),
|
2025-12-09 20:41:58 +00:00
|
|
|
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresGrupoPorMesPorFilial, codgrupo),
|
2025-12-09 19:19:55 +00:00
|
|
|
percentualTotal: calcularPercentualTotal(totalGrupo, codgrupo),
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedGrupos.has(codgrupo)) {
|
|
|
|
|
// Agrupar por conta dentro do grupo
|
|
|
|
|
const contas = items.reduce((acc, item) => {
|
|
|
|
|
const conta = item.conta || "";
|
|
|
|
|
if (!conta) return acc;
|
|
|
|
|
if (!acc[conta]) {
|
|
|
|
|
acc[conta] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[conta].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Ordenar contas por CODCONTA
|
|
|
|
|
const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => {
|
|
|
|
|
const codcontaA = itemsA[0]?.codigo_conta || 0;
|
|
|
|
|
const codcontaB = itemsB[0]?.codigo_conta || 0;
|
|
|
|
|
|
|
|
|
|
if (codcontaA && codcontaB) {
|
|
|
|
|
return codcontaA - codcontaB;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return contaA.localeCompare(contaB);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedContas.forEach(([conta, contaItems]) => {
|
|
|
|
|
const totalConta = contaItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresContaPorMes = calcularValoresPorMes(contaItems);
|
2025-12-09 20:41:58 +00:00
|
|
|
const valoresContaPorMesPorFilial = calcularValoresPorMesPorFilial(contaItems);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
// Linha da conta (Level 1)
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "conta",
|
|
|
|
|
level: 1,
|
|
|
|
|
grupo: items[0]?.grupo || codgrupo,
|
|
|
|
|
codigo_grupo: codgrupo,
|
|
|
|
|
conta,
|
|
|
|
|
codigo_conta: contaItems[0]?.codigo_conta,
|
|
|
|
|
total: totalConta,
|
|
|
|
|
isExpanded: false,
|
|
|
|
|
valoresPorMes: valoresContaPorMes,
|
2025-12-09 20:41:58 +00:00
|
|
|
valoresPorMesPorFilial: valoresContaPorMesPorFilial,
|
2025-12-09 19:19:55 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupo),
|
2025-12-09 20:41:58 +00:00
|
|
|
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresContaPorMesPorFilial, codgrupo),
|
2025-12-09 19:19:55 +00:00
|
|
|
percentualTotal: calcularPercentualTotal(totalConta, codgrupo),
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-09 19:19:55 +00:00
|
|
|
|
|
|
|
|
// Adicionar linha calculada "MARGEM DE LOJA" após o grupo 02
|
|
|
|
|
// Verificar se é o último grupo ou se o próximo grupo é maior que 02
|
|
|
|
|
const proximoCodigo = sortedGrupos[index + 1]?.[0];
|
|
|
|
|
const proximoNumero = proximoCodigo ? parseInt(proximoCodigo) : 999;
|
|
|
|
|
|
|
|
|
|
if (codgrupo === "02" || (parseInt(codgrupo) === 2 && proximoNumero > 2)) {
|
|
|
|
|
// Calcular MARGEM DE LOJA = CODGRUPO 01 - CODGRUPO 02
|
|
|
|
|
const valoresGrupo01 = valoresPorGrupo["01"] || {};
|
|
|
|
|
const valoresGrupo02 = valoresPorGrupo["02"] || {};
|
|
|
|
|
|
|
|
|
|
// Calcular valores por mês para MARGEM DE LOJA
|
|
|
|
|
const valoresMargemPorMes: Record<string, number> = {};
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
const valor01 = valoresGrupo01[mes] || 0;
|
|
|
|
|
const valor02 = valoresGrupo02[mes] || 0;
|
|
|
|
|
valoresMargemPorMes[mes] = valor01 - valor02;
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-09 20:41:58 +00:00
|
|
|
// Calcular valores por mês e por filial para MARGEM DE LOJA
|
|
|
|
|
const valoresMargemPorMesPorFilial: Record<string, Record<string, number>> = {};
|
|
|
|
|
const valoresGrupo01PorFilial = gruposPorCodigo["01"] ? calcularValoresPorMesPorFilial(gruposPorCodigo["01"]) : {};
|
|
|
|
|
const valoresGrupo02PorFilial = gruposPorCodigo["02"] ? calcularValoresPorMesPorFilial(gruposPorCodigo["02"]) : {};
|
|
|
|
|
|
2025-12-10 12:45:06 +00:00
|
|
|
// Extrair filiais únicas dos valores calculados
|
|
|
|
|
const primeiroMes = mesesDisponiveis[0] || "";
|
|
|
|
|
const filiaisDisponiveis = [...new Set([
|
|
|
|
|
...Object.keys(valoresGrupo01PorFilial[primeiroMes] || {}),
|
|
|
|
|
...Object.keys(valoresGrupo02PorFilial[primeiroMes] || {})
|
|
|
|
|
])];
|
|
|
|
|
|
2025-12-09 20:41:58 +00:00
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
valoresMargemPorMesPorFilial[mes] = {};
|
2025-12-10 12:45:06 +00:00
|
|
|
filiaisDisponiveis.forEach(filial => {
|
2025-12-09 20:41:58 +00:00
|
|
|
const valor01 = valoresGrupo01PorFilial[mes]?.[filial] || 0;
|
|
|
|
|
const valor02 = valoresGrupo02PorFilial[mes]?.[filial] || 0;
|
|
|
|
|
valoresMargemPorMesPorFilial[mes][filial] = valor01 - valor02;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-09 19:19:55 +00:00
|
|
|
// Calcular total
|
|
|
|
|
const totalMargem = Object.values(valoresMargemPorMes).reduce((sum, val) => sum + val, 0);
|
|
|
|
|
|
|
|
|
|
// Adicionar linha calculada
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "calculado",
|
|
|
|
|
level: 0,
|
|
|
|
|
grupo: "MARGEM DE LOJA",
|
|
|
|
|
codigo_grupo: "MARGEM",
|
|
|
|
|
total: totalMargem,
|
|
|
|
|
isExpanded: false,
|
|
|
|
|
valoresPorMes: valoresMargemPorMes,
|
2025-12-09 20:41:58 +00:00
|
|
|
valoresPorMesPorFilial: valoresMargemPorMesPorFilial,
|
2025-12-09 19:19:55 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresMargemPorMes, "MARGEM"),
|
2025-12-09 20:41:58 +00:00
|
|
|
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresMargemPorMesPorFilial, "MARGEM"),
|
2025-12-09 19:19:55 +00:00
|
|
|
percentualTotal: calcularPercentualTotal(totalMargem, "MARGEM"),
|
|
|
|
|
isCalculado: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows;
|
2025-12-10 12:45:06 +00:00
|
|
|
}, [data, mesesDisponiveis, expandedGrupos, calcularValoresPorMes, calcularValoresPorMesPorFilial, calcularPercentuaisPorMes, calcularPercentuaisPorMesPorFilial, calcularPercentualTotal]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const getRowStyle = React.useCallback((row: HierarchicalRow) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
const baseStyle =
|
|
|
|
|
"transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30";
|
|
|
|
|
|
|
|
|
|
const linhaId = `${row.type}-${row.codigo_grupo || ""}-${row.codigo_conta || ""}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
|
|
|
|
|
let style = baseStyle;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
style +=
|
|
|
|
|
" bg-gradient-to-r from-green-100 to-emerald-100 border-l-4 border-green-500 shadow-lg";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (row.type) {
|
|
|
|
|
case "grupo":
|
|
|
|
|
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`;
|
2025-12-09 19:19:55 +00:00
|
|
|
case "calculado":
|
|
|
|
|
return `${style} bg-gradient-to-r from-purple-50/30 to-pink-50/30 font-bold text-purple-900 border-b-2 border-purple-300 italic`;
|
2025-12-09 14:29:32 +00:00
|
|
|
case "conta":
|
|
|
|
|
return `${style} bg-white font-normal text-gray-600`;
|
|
|
|
|
default:
|
|
|
|
|
return style;
|
|
|
|
|
}
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [linhaSelecionada]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const getFixedCellBackground = React.useCallback((row: HierarchicalRow): string => {
|
2025-12-09 14:29:32 +00:00
|
|
|
const linhaId = `${row.type}-${row.codigo_grupo || ""}-${row.codigo_conta || ""}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
return "bg-gradient-to-r from-green-100 to-emerald-100";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (row.type) {
|
|
|
|
|
case "grupo":
|
|
|
|
|
return "bg-gradient-to-r from-blue-50 to-indigo-50";
|
2025-12-09 19:19:55 +00:00
|
|
|
case "calculado":
|
|
|
|
|
return "bg-gradient-to-r from-purple-50 to-pink-50";
|
2025-12-09 14:29:32 +00:00
|
|
|
case "conta":
|
|
|
|
|
return "bg-white";
|
|
|
|
|
default:
|
|
|
|
|
return "bg-white";
|
|
|
|
|
}
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [linhaSelecionada]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const getIndentStyle = React.useCallback((level: number) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
return { paddingLeft: `${level * 20}px` };
|
2025-12-09 20:58:11 +00:00
|
|
|
}, []);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
const renderCellContent = React.useCallback((row: HierarchicalRow) => {
|
2025-12-09 14:29:32 +00:00
|
|
|
switch (row.type) {
|
|
|
|
|
case "grupo":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleGrupo(row.codigo_grupo!)}
|
|
|
|
|
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-150 ease-in-out flex items-center justify-center w-8 h-8 flex-shrink-0 transform hover:scale-105"
|
|
|
|
|
>
|
|
|
|
|
{row.isExpanded ? (
|
|
|
|
|
<ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="font-bold text-gray-900">
|
|
|
|
|
{row.grupo}
|
|
|
|
|
</span>
|
|
|
|
|
{row.codigo_grupo && (
|
2025-12-09 19:44:22 +00:00
|
|
|
// <span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
|
|
|
|
// {row.codigo_grupo}
|
|
|
|
|
// </span>
|
|
|
|
|
<></>
|
2025-12-09 14:29:32 +00:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-12-09 19:19:55 +00:00
|
|
|
case "calculado":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
|
|
|
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
{/* <span className="text-purple-600 font-bold text-lg">=</span> */}
|
|
|
|
|
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 text-left p-2">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="font-bold text-purple-700 italic">
|
|
|
|
|
{row.grupo}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-12-09 14:29:32 +00:00
|
|
|
case "conta":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 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/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="font-semibold text-gray-800">
|
|
|
|
|
{row.conta}
|
|
|
|
|
</span>
|
|
|
|
|
{row.codigo_conta && (
|
|
|
|
|
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
|
|
|
|
{row.codigo_conta}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-12-09 20:58:11 +00:00
|
|
|
}, [toggleGrupo, handleRowClick]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
const toggleExpandAll = () => {
|
|
|
|
|
if (isAllExpanded) {
|
|
|
|
|
setExpandedGrupos(new Set());
|
|
|
|
|
setIsAllExpanded(false);
|
|
|
|
|
} else {
|
|
|
|
|
const todosGrupos = [...new Set(data.map(item => item.codgrupo || item.codigo_grupo).filter(Boolean))];
|
|
|
|
|
setExpandedGrupos(new Set(todosGrupos));
|
|
|
|
|
setIsAllExpanded(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const aplicarFiltros = async () => {
|
|
|
|
|
setIsFilterOpen(false);
|
|
|
|
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
const response = await fetch("/api/dre-filial-oracle");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro HTTP: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dadosCompletos = await response.json();
|
|
|
|
|
|
|
|
|
|
// Aplicar filtros nos dados
|
|
|
|
|
let dadosFiltrados = dadosCompletos;
|
|
|
|
|
|
|
|
|
|
// Filtro por período
|
|
|
|
|
if (filtros.periodoDe && filtros.periodoAte) {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
|
|
|
|
|
const dataItem = item.data_competencia;
|
|
|
|
|
return dataItem >= filtros.periodoDe && dataItem <= filtros.periodoAte;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
// Filtro por filial (multi-seleção)
|
|
|
|
|
if (filiaisSelecionadas.length > 0) {
|
2025-12-09 14:29:32 +00:00
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
|
2025-12-09 20:22:48 +00:00
|
|
|
const filialItem = item.filial || item.codfilial || "";
|
|
|
|
|
return filiaisSelecionadas.includes(filialItem);
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDadosFiltrados(dadosFiltrados);
|
|
|
|
|
setData(dadosFiltrados);
|
|
|
|
|
setFiltrosAplicados(true);
|
|
|
|
|
|
2025-12-09 19:30:55 +00:00
|
|
|
// Limpar filtros do analítico ao aplicar novos filtros na tabela
|
|
|
|
|
setAnaliticoFiltros({
|
|
|
|
|
dataInicio: "",
|
|
|
|
|
dataFim: "",
|
|
|
|
|
centroCusto: "",
|
|
|
|
|
codigoGrupo: "",
|
|
|
|
|
codigoSubgrupo: "",
|
|
|
|
|
codigoConta: "",
|
2025-12-09 21:21:15 +00:00
|
|
|
codFilial: "",
|
2025-12-09 19:30:55 +00:00
|
|
|
linhaSelecionada: "",
|
|
|
|
|
excluirCentroCusto: "",
|
|
|
|
|
excluirCodigoConta: "",
|
|
|
|
|
codigosCentrosCustoSelecionados: "",
|
|
|
|
|
codigosContasSelecionadas: "",
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
// Extrair meses únicos dos dados filtrados
|
|
|
|
|
const meses = [
|
|
|
|
|
...new Set(
|
|
|
|
|
dadosFiltrados.map((item: DREItem) => item.data_competencia)
|
|
|
|
|
),
|
|
|
|
|
].sort() as string[];
|
|
|
|
|
setMesesDisponiveis(meses);
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "Erro desconhecido");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limparFiltros = () => {
|
|
|
|
|
const agora = new Date();
|
|
|
|
|
const anoAtual = agora.getFullYear();
|
|
|
|
|
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const periodoAtual = `${anoAtual}-${mesAtual}`;
|
|
|
|
|
|
2025-12-09 14:43:12 +00:00
|
|
|
setFiltros({
|
|
|
|
|
periodoDe: `${anoAtual}-01`,
|
|
|
|
|
periodoAte: periodoAtual,
|
2025-12-09 20:22:48 +00:00
|
|
|
filial: "Todas",
|
2025-12-09 14:43:12 +00:00
|
|
|
});
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
setFiliaisSelecionadas([]);
|
2025-12-09 14:29:32 +00:00
|
|
|
setData([]);
|
|
|
|
|
setDadosFiltrados([]);
|
|
|
|
|
setFiltrosAplicados(false);
|
|
|
|
|
setMesesDisponiveis([]);
|
|
|
|
|
setIsAllExpanded(false);
|
|
|
|
|
setIsFilterOpen(false);
|
|
|
|
|
|
2025-12-09 19:30:55 +00:00
|
|
|
// Limpar filtros do analítico também
|
|
|
|
|
setAnaliticoFiltros({
|
|
|
|
|
dataInicio: "",
|
|
|
|
|
dataFim: "",
|
|
|
|
|
centroCusto: "",
|
|
|
|
|
codigoGrupo: "",
|
|
|
|
|
codigoSubgrupo: "",
|
|
|
|
|
codigoConta: "",
|
2025-12-09 21:21:15 +00:00
|
|
|
codFilial: "",
|
2025-12-09 19:30:55 +00:00
|
|
|
linhaSelecionada: "",
|
|
|
|
|
excluirCentroCusto: "",
|
|
|
|
|
excluirCodigoConta: "",
|
|
|
|
|
codigosCentrosCustoSelecionados: "",
|
|
|
|
|
codigosContasSelecionadas: "",
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
carregarPeriodosDisponiveis();
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-22 18:23:51 +00:00
|
|
|
// Função para construir dados hierárquicos completamente expandidos (para exportação XLSX)
|
|
|
|
|
// Usa a mesma lógica de buildHierarchicalData mas sempre expandido
|
|
|
|
|
const buildHierarchicalDataCompleta = (): HierarchicalRow[] => {
|
|
|
|
|
const rows: HierarchicalRow[] = [];
|
|
|
|
|
|
|
|
|
|
// Hierarquia simplificada: [grupo, conta]
|
|
|
|
|
// Agrupar por CODGRUPO
|
|
|
|
|
const gruposPorCodigo = data.reduce((acc, item) => {
|
|
|
|
|
const codgrupo = item.codgrupo || item.codigo_grupo || "";
|
|
|
|
|
if (!codgrupo) return acc;
|
|
|
|
|
if (!acc[codgrupo]) {
|
|
|
|
|
acc[codgrupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[codgrupo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Calcular valores por grupo para linhas calculadas
|
|
|
|
|
const valoresPorGrupo: Record<string, Record<string, number>> = {};
|
|
|
|
|
Object.keys(gruposPorCodigo).forEach(codgrupo => {
|
|
|
|
|
valoresPorGrupo[codgrupo] = calcularValoresPorMes(gruposPorCodigo[codgrupo]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Ordenar por CODGRUPO (numericamente)
|
|
|
|
|
const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => {
|
|
|
|
|
const numA = parseInt(codA) || 0;
|
|
|
|
|
const numB = parseInt(codB) || 0;
|
|
|
|
|
if (numA !== numB) {
|
|
|
|
|
return numA - numB;
|
|
|
|
|
}
|
|
|
|
|
return codA.localeCompare(codB);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedGrupos.forEach(([codgrupo, items], index) => {
|
|
|
|
|
// Calcular total do grupo
|
|
|
|
|
const totalGrupo = items.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresGrupoPorMes = calcularValoresPorMes(items);
|
|
|
|
|
const valoresGrupoPorMesPorFilial = calcularValoresPorMesPorFilial(items);
|
|
|
|
|
|
|
|
|
|
// Linha do grupo (Level 0) - sempre expandida
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "grupo",
|
|
|
|
|
level: 0,
|
|
|
|
|
grupo: items[0]?.grupo || codgrupo,
|
|
|
|
|
codigo_grupo: codgrupo,
|
|
|
|
|
total: totalGrupo,
|
|
|
|
|
isExpanded: true, // Sempre expandido na exportação
|
|
|
|
|
valoresPorMes: valoresGrupoPorMes,
|
|
|
|
|
valoresPorMesPorFilial: valoresGrupoPorMesPorFilial,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresGrupoPorMes, codgrupo),
|
|
|
|
|
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresGrupoPorMesPorFilial, codgrupo),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalGrupo, codgrupo),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Sempre expandir grupos na exportação (não verificar expandedGrupos)
|
|
|
|
|
// Agrupar por conta dentro do grupo
|
|
|
|
|
const contas = items.reduce((acc, item) => {
|
|
|
|
|
const conta = item.conta || "";
|
|
|
|
|
if (!conta) return acc;
|
|
|
|
|
if (!acc[conta]) {
|
|
|
|
|
acc[conta] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[conta].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Ordenar contas por CODCONTA
|
|
|
|
|
const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => {
|
|
|
|
|
const codcontaA = itemsA[0]?.codigo_conta || 0;
|
|
|
|
|
const codcontaB = itemsB[0]?.codigo_conta || 0;
|
|
|
|
|
|
|
|
|
|
if (codcontaA && codcontaB) {
|
|
|
|
|
return codcontaA - codcontaB;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return contaA.localeCompare(contaB);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedContas.forEach(([conta, contaItems]) => {
|
|
|
|
|
const totalConta = contaItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresContaPorMes = calcularValoresPorMes(contaItems);
|
|
|
|
|
const valoresContaPorMesPorFilial = calcularValoresPorMesPorFilial(contaItems);
|
|
|
|
|
|
|
|
|
|
// Linha da conta (Level 1) - sempre incluída na exportação
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "conta",
|
|
|
|
|
level: 1,
|
|
|
|
|
grupo: items[0]?.grupo || codgrupo,
|
|
|
|
|
codigo_grupo: codgrupo,
|
|
|
|
|
conta,
|
|
|
|
|
codigo_conta: contaItems[0]?.codigo_conta,
|
|
|
|
|
total: totalConta,
|
|
|
|
|
isExpanded: false,
|
|
|
|
|
valoresPorMes: valoresContaPorMes,
|
|
|
|
|
valoresPorMesPorFilial: valoresContaPorMesPorFilial,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupo),
|
|
|
|
|
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresContaPorMesPorFilial, codgrupo),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalConta, codgrupo),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Adicionar linha calculada "MARGEM DE LOJA" após o grupo 02
|
|
|
|
|
// Verificar se é o último grupo ou se o próximo grupo é maior que 02
|
|
|
|
|
const proximoCodigo = sortedGrupos[index + 1]?.[0];
|
|
|
|
|
const proximoNumero = proximoCodigo ? parseInt(proximoCodigo) : 999;
|
|
|
|
|
|
|
|
|
|
if (codgrupo === "02" || (parseInt(codgrupo) === 2 && proximoNumero > 2)) {
|
|
|
|
|
// Calcular MARGEM DE LOJA = CODGRUPO 01 - CODGRUPO 02
|
|
|
|
|
const valoresGrupo01 = valoresPorGrupo["01"] || {};
|
|
|
|
|
const valoresGrupo02 = valoresPorGrupo["02"] || {};
|
|
|
|
|
|
|
|
|
|
// Calcular valores por mês para MARGEM DE LOJA
|
|
|
|
|
const valoresMargemPorMes: Record<string, number> = {};
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
const valor01 = valoresGrupo01[mes] || 0;
|
|
|
|
|
const valor02 = valoresGrupo02[mes] || 0;
|
|
|
|
|
valoresMargemPorMes[mes] = valor01 - valor02;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calcular valores por mês e por filial para MARGEM DE LOJA
|
|
|
|
|
const valoresMargemPorMesPorFilial: Record<string, Record<string, number>> = {};
|
|
|
|
|
const valoresGrupo01PorFilial = gruposPorCodigo["01"] ? calcularValoresPorMesPorFilial(gruposPorCodigo["01"]) : {};
|
|
|
|
|
const valoresGrupo02PorFilial = gruposPorCodigo["02"] ? calcularValoresPorMesPorFilial(gruposPorCodigo["02"]) : {};
|
|
|
|
|
|
|
|
|
|
// Extrair filiais únicas dos valores calculados
|
|
|
|
|
const primeiroMes = mesesDisponiveis[0] || "";
|
|
|
|
|
const filiaisDisponiveis = [...new Set([
|
|
|
|
|
...Object.keys(valoresGrupo01PorFilial[primeiroMes] || {}),
|
|
|
|
|
...Object.keys(valoresGrupo02PorFilial[primeiroMes] || {})
|
|
|
|
|
])];
|
|
|
|
|
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
valoresMargemPorMesPorFilial[mes] = {};
|
|
|
|
|
filiaisDisponiveis.forEach(filial => {
|
|
|
|
|
const valor01 = valoresGrupo01PorFilial[mes]?.[filial] || 0;
|
|
|
|
|
const valor02 = valoresGrupo02PorFilial[mes]?.[filial] || 0;
|
|
|
|
|
valoresMargemPorMesPorFilial[mes][filial] = valor01 - valor02;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calcular total
|
|
|
|
|
const totalMargem = Object.values(valoresMargemPorMes).reduce((sum, val) => sum + val, 0);
|
|
|
|
|
|
|
|
|
|
// Adicionar linha calculada
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "calculado",
|
|
|
|
|
level: 0,
|
|
|
|
|
grupo: "MARGEM DE LOJA",
|
|
|
|
|
codigo_grupo: "MARGEM",
|
|
|
|
|
total: totalMargem,
|
|
|
|
|
isExpanded: false,
|
|
|
|
|
valoresPorMes: valoresMargemPorMes,
|
|
|
|
|
valoresPorMesPorFilial: valoresMargemPorMesPorFilial,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresMargemPorMes, "MARGEM"),
|
|
|
|
|
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresMargemPorMesPorFilial, "MARGEM"),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalMargem, "MARGEM"),
|
|
|
|
|
isCalculado: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-09 14:29:32 +00:00
|
|
|
const exportarXLSX = () => {
|
|
|
|
|
if (!data.length) {
|
|
|
|
|
console.log('⚠️ Nenhum dado para exportar');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 18:23:51 +00:00
|
|
|
console.log('📊 Exportando TODOS os dados expandidos para XLSX...');
|
|
|
|
|
|
|
|
|
|
// Criar uma versão completamente expandida dos dados hierárquicos
|
|
|
|
|
const dadosCompletosExpandidos = buildHierarchicalDataCompleta();
|
|
|
|
|
|
|
|
|
|
// Extrair filiais únicas para exportação
|
|
|
|
|
const filiaisParaExportacao = (filtrosAplicados && filiaisSelecionadas.length > 0)
|
|
|
|
|
? filiaisSelecionadas
|
|
|
|
|
: (opcoesFiliais.length > 0 ? opcoesFiliais : []);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
const dadosExportacao = dadosCompletosExpandidos.map((row, index) => {
|
|
|
|
|
const linha: any = {
|
|
|
|
|
'Linha': index + 1,
|
|
|
|
|
'Tipo': row.type,
|
|
|
|
|
'Nível': row.level,
|
|
|
|
|
'Grupo': row.grupo || '',
|
|
|
|
|
'Código Grupo': row.codigo_grupo || '',
|
|
|
|
|
'Conta': row.conta || '',
|
|
|
|
|
'Código Conta': row.codigo_conta || '',
|
|
|
|
|
'Total': row.total || 0,
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-22 18:23:51 +00:00
|
|
|
// Adicionar colunas por mês e por filial
|
2025-12-09 14:29:32 +00:00
|
|
|
mesesDisponiveis.forEach(mes => {
|
2025-12-22 18:23:51 +00:00
|
|
|
// Colunas por filial para este mês
|
|
|
|
|
filiaisParaExportacao.forEach((filial: string) => {
|
|
|
|
|
const valorFilial = row.valoresPorMesPorFilial?.[mes]?.[filial] || 0;
|
|
|
|
|
const percentualFilial = row.percentuaisPorMesPorFilial?.[mes]?.[filial] || 0;
|
|
|
|
|
linha[`${mes} FILIAL - ${filial} R$`] = valorFilial;
|
|
|
|
|
linha[`${mes} FILIAL - ${filial} %`] = percentualFilial;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Colunas de totalizador para este mês
|
|
|
|
|
const valorTotal = row.valoresPorMes?.[mes] || 0;
|
|
|
|
|
const percentualTotal = row.percentuaisPorMes?.[mes] || 0;
|
|
|
|
|
linha[`${mes} Total R$`] = valorTotal;
|
|
|
|
|
linha[`${mes} Total %`] = percentualTotal;
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return linha;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
|
|
|
const ws = XLSX.utils.json_to_sheet(dadosExportacao);
|
|
|
|
|
|
|
|
|
|
const colWidths = [
|
|
|
|
|
{ wch: 8 }, // Linha
|
|
|
|
|
{ wch: 15 }, // Tipo
|
|
|
|
|
{ wch: 8 }, // Nível
|
|
|
|
|
{ wch: 25 }, // Grupo
|
|
|
|
|
{ wch: 15 }, // Código Grupo
|
|
|
|
|
{ wch: 35 }, // Conta
|
|
|
|
|
{ wch: 12 }, // Código Conta
|
|
|
|
|
{ wch: 15 }, // Total
|
|
|
|
|
];
|
|
|
|
|
|
2025-12-22 18:23:51 +00:00
|
|
|
// Adicionar larguras para colunas por mês, filial e totalizador
|
2025-12-09 14:29:32 +00:00
|
|
|
mesesDisponiveis.forEach(() => {
|
2025-12-22 18:23:51 +00:00
|
|
|
// Colunas por filial (R$ e %)
|
|
|
|
|
filiaisParaExportacao.forEach(() => {
|
|
|
|
|
colWidths.push({ wch: 15 }); // Valor R$ por filial
|
|
|
|
|
colWidths.push({ wch: 10 }); // % por filial
|
|
|
|
|
});
|
|
|
|
|
// Colunas de totalizador (R$ e %)
|
|
|
|
|
colWidths.push({ wch: 15 }); // Valor R$ total
|
|
|
|
|
colWidths.push({ wch: 10 }); // % total
|
2025-12-09 14:29:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ws['!cols'] = colWidths;
|
|
|
|
|
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, 'DRE Filial Completo');
|
|
|
|
|
|
2025-12-09 14:43:12 +00:00
|
|
|
const resumoData = [
|
|
|
|
|
{ 'Informação': 'Período', 'Valor': `${filtros.periodoDe} a ${filtros.periodoAte}` },
|
2025-12-09 20:22:48 +00:00
|
|
|
{ 'Informação': 'Filial', 'Valor': filtros.filial },
|
2025-12-09 14:29:32 +00:00
|
|
|
{ 'Informação': 'Total de Registros', 'Valor': dadosCompletosExpandidos.length },
|
|
|
|
|
{ 'Informação': 'Data de Exportação', 'Valor': new Date().toLocaleString('pt-BR') },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
|
|
|
|
|
wsResumo['!cols'] = [{ wch: 20 }, { wch: 30 }];
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
|
|
|
|
|
|
|
|
|
|
const dataAtual = new Date().toISOString().split('T')[0];
|
|
|
|
|
const nomeArquivo = `DRE_Filial_Completo_${dataAtual}.xlsx`;
|
|
|
|
|
|
|
|
|
|
XLSX.writeFile(wb, nomeArquivo);
|
|
|
|
|
|
|
|
|
|
console.log('✅ Arquivo XLSX completo exportado:', nomeArquivo);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-09 20:58:11 +00:00
|
|
|
// Memoizar dados hierárquicos para evitar recálculos desnecessários
|
|
|
|
|
const hierarchicalData = React.useMemo(() => {
|
|
|
|
|
if (!data.length || !mesesDisponiveis.length) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return buildHierarchicalData();
|
|
|
|
|
}, [buildHierarchicalData]);
|
2025-12-09 14:29:32 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="w-full max-w-none mx-auto p-2">
|
|
|
|
|
{/* Header Section */}
|
|
|
|
|
<div className="mb-2">
|
|
|
|
|
<div className="flex items-center justify-between mb-1">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-bold text-gray-900">Despesa Filial</h1>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
Demonstração do Resultado do Exercício
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Controles */}
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={exportarXLSX}
|
|
|
|
|
disabled={!filtrosAplicados || hierarchicalData.length === 0}
|
|
|
|
|
className="flex items-center gap-2 text-xs h-8 px-3 transition-all duration-150 ease-in-out hover:scale-105 disabled:hover:scale-100"
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4" />
|
|
|
|
|
Exportar XLSX
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={toggleExpandAll}
|
|
|
|
|
disabled={!filtrosAplicados || hierarchicalData.length === 0}
|
|
|
|
|
className="flex items-center gap-2 text-xs h-8 px-3 transition-all duration-150 ease-in-out hover:scale-105 disabled:hover:scale-100"
|
|
|
|
|
>
|
|
|
|
|
{isAllExpanded ? (
|
|
|
|
|
<>
|
|
|
|
|
<Minimize2 className="w-4 h-4" />
|
|
|
|
|
Recolher Tudo
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Maximize2 className="w-4 h-4" />
|
|
|
|
|
Expandir Tudo
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
|
|
|
|
<SheetTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="flex items-center gap-2 text-xs h-8 px-3 transition-all duration-150 ease-in-out hover:scale-105"
|
|
|
|
|
>
|
|
|
|
|
<Filter className="w-4 h-4" />
|
|
|
|
|
Filtros
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetTrigger>
|
2025-12-09 14:41:39 +00:00
|
|
|
<SheetContent className="w-[400px] sm:w-[540px] flex flex-col">
|
2025-12-09 14:29:32 +00:00
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>Filtros</SheetTitle>
|
|
|
|
|
<SheetDescription>
|
2025-12-09 14:41:39 +00:00
|
|
|
Ajuste os critérios e clique em Pesquisar para atualizar a visão.
|
2025-12-09 14:29:32 +00:00
|
|
|
</SheetDescription>
|
|
|
|
|
</SheetHeader>
|
2025-12-09 14:41:39 +00:00
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto pr-2">
|
|
|
|
|
<div className="grid gap-3 py-2">
|
|
|
|
|
{/* Período */}
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<Label>Período</Label>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="periodo-de" className="text-xs text-gray-500">DE</Label>
|
|
|
|
|
<Select value={filtros.periodoDe} onValueChange={(value) => setFiltros(prev => ({ ...prev, periodoDe: value }))}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Selecione" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{mesesDisponiveis.map(mes => (
|
|
|
|
|
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="periodo-ate" className="text-xs text-gray-500">ATÉ</Label>
|
|
|
|
|
<Select value={filtros.periodoAte} onValueChange={(value) => setFiltros(prev => ({ ...prev, periodoAte: value }))}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Selecione" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{mesesDisponiveis.map(mes => (
|
|
|
|
|
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-09 14:29:32 +00:00
|
|
|
|
2025-12-09 20:22:48 +00:00
|
|
|
{/* Filial */}
|
2025-12-09 14:29:32 +00:00
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
2025-12-09 20:22:48 +00:00
|
|
|
<Label htmlFor="filial">FILIAL</Label>
|
2025-12-09 14:29:32 +00:00
|
|
|
<div className="flex gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-12-09 20:22:48 +00:00
|
|
|
onClick={selecionarTodasFiliais}
|
2025-12-09 14:29:32 +00:00
|
|
|
className="text-xs h-5 px-2"
|
|
|
|
|
>
|
|
|
|
|
Todas
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-12-09 20:22:48 +00:00
|
|
|
onClick={limparFiliais}
|
2025-12-09 14:29:32 +00:00
|
|
|
className="text-xs h-5 px-2"
|
|
|
|
|
>
|
|
|
|
|
Limpar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Input
|
2025-12-09 20:22:48 +00:00
|
|
|
placeholder="Filtrar filiais..."
|
|
|
|
|
value={filtroFilial}
|
|
|
|
|
onChange={(e) => setFiltroFilial(e.target.value)}
|
2025-12-09 14:29:32 +00:00
|
|
|
className="h-8 text-sm"
|
|
|
|
|
/>
|
|
|
|
|
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
|
2025-12-09 20:22:48 +00:00
|
|
|
{opcoesFiliais
|
|
|
|
|
.filter(filial => {
|
|
|
|
|
if (!filtroFilial) return true;
|
|
|
|
|
const termo = filtroFilial.toLowerCase();
|
|
|
|
|
return filial.toLowerCase().includes(termo);
|
2025-12-09 14:29:32 +00:00
|
|
|
})
|
2025-12-09 20:22:48 +00:00
|
|
|
.map(filial => (
|
|
|
|
|
<div key={filial} className="flex items-center space-x-1">
|
2025-12-09 14:29:32 +00:00
|
|
|
<Checkbox
|
2025-12-09 20:22:48 +00:00
|
|
|
id={`filial-${filial}`}
|
|
|
|
|
checked={filiaisSelecionadas.includes(filial)}
|
|
|
|
|
onCheckedChange={() => toggleFilial(filial)}
|
2025-12-09 14:29:32 +00:00
|
|
|
/>
|
|
|
|
|
<Label
|
2025-12-09 20:22:48 +00:00
|
|
|
htmlFor={`filial-${filial}`}
|
2025-12-09 14:29:32 +00:00
|
|
|
className="text-sm font-normal cursor-pointer flex-1"
|
|
|
|
|
>
|
2025-12-09 20:22:48 +00:00
|
|
|
{filial}
|
2025-12-09 14:29:32 +00:00
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-12-09 20:22:48 +00:00
|
|
|
{filiaisSelecionadas.length > 0 && (
|
2025-12-09 14:29:32 +00:00
|
|
|
<div className="text-xs text-gray-500">
|
2025-12-09 20:22:48 +00:00
|
|
|
{filiaisSelecionadas.length} filial(is) selecionada(s)
|
2025-12-09 14:29:32 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-09 14:41:39 +00:00
|
|
|
</div>
|
2025-12-09 14:29:32 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<SheetFooter className="flex gap-2 mt-4 border-t pt-4">
|
|
|
|
|
<Button variant="outline" onClick={limparFiltros} className="flex-1">
|
|
|
|
|
Limpar Filtros
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline" onClick={() => setIsFilterOpen(false)} className="flex-1">
|
|
|
|
|
Cancelar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={aplicarFiltros} className="flex-1">
|
|
|
|
|
Pesquisar
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Loading quando aplicando filtros */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-4 text-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-2">
|
|
|
|
|
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
|
|
|
|
|
<LoaderPinwheel className="w-8 h-8 text-blue-600 animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
|
|
|
|
Aplicando filtros...
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
Aguarde enquanto processamos os dados.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Erro */}
|
|
|
|
|
{error && !loading && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-red-200 p-4 text-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-2">
|
|
|
|
|
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
|
|
|
|
<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>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-red-900 mb-2">
|
|
|
|
|
Erro ao carregar dados
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-red-600 mb-4">{error}</p>
|
|
|
|
|
<Button onClick={() => aplicarFiltros()} className="flex items-center gap-1">
|
|
|
|
|
Tentar novamente
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Mensagem quando não há dados */}
|
|
|
|
|
{!filtrosAplicados && !loading && !error && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-4 text-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-2">
|
|
|
|
|
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
|
|
|
|
<Filter className="w-8 h-8 text-gray-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
|
|
|
|
Nenhum dado exibido
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-500 mb-2">
|
|
|
|
|
Clique no botão "Filtros" para definir os critérios de busca e visualizar os dados do DRE.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Table Container */}
|
|
|
|
|
{filtrosAplicados && !loading && !error && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
|
|
|
<div
|
|
|
|
|
className="overflow-x-auto overflow-y-auto max-h-[500px]"
|
|
|
|
|
style={{ scrollbarWidth: 'thin' }}
|
|
|
|
|
>
|
|
|
|
|
<table
|
|
|
|
|
className="w-full border-collapse"
|
|
|
|
|
style={{ minWidth: 'max-content' }}
|
|
|
|
|
>
|
|
|
|
|
{/* Table Header */}
|
|
|
|
|
<thead className="sticky top-0 z-10 bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
<tr className="border-b border-gray-200">
|
|
|
|
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wide w-[300px] min-w-[300px] bg-gradient-to-r from-blue-50 to-indigo-50 sticky left-0 z-20 shadow-[2px_0_4px_rgba(0,0,0,0.1)]">
|
|
|
|
|
Descrição
|
|
|
|
|
</th>
|
2025-12-10 12:00:06 +00:00
|
|
|
{mesesDisponiveis.map((mes) => {
|
|
|
|
|
const filiaisParaMes = (filtrosAplicados && filiaisSelecionadas.length > 0)
|
|
|
|
|
? filiaisSelecionadas
|
|
|
|
|
: (opcoesFiliais.length > 0 ? opcoesFiliais : ['']);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<React.Fragment key={mes}>
|
|
|
|
|
{/* Cabeçalhos de filiais para este mês */}
|
|
|
|
|
{filiaisParaMes.map((filial: string) => (
|
|
|
|
|
<React.Fragment key={`${mes}-${filial || 'default'}`}>
|
|
|
|
|
<th className="px-2 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide w-[120px] min-w-[120px] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
{mes}{filial && <><br/>
|
|
|
|
|
<span className="text-[10px] font-normal text-gray-600">Filial - {filial}</span></>}
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-2 py-2 text-center text-xs font-semibold text-gray-500 uppercase tracking-wide w-[100px] min-w-[100px] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
%{filial && <><br/>
|
|
|
|
|
<span className="text-[10px] font-normal text-gray-600">Filial - {filial}</span></>}
|
|
|
|
|
</th>
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{/* Cabeçalhos de totalizador para este mês */}
|
|
|
|
|
<th className="px-2 py-2 text-right text-xs font-bold text-green-700 uppercase tracking-wide w-[120px] min-w-[120px] bg-gradient-to-r from-green-50 to-emerald-50">
|
|
|
|
|
{mes}<br/>
|
|
|
|
|
<span className="text-[10px] font-normal text-green-600">Total</span>
|
2025-12-09 20:41:58 +00:00
|
|
|
</th>
|
2025-12-10 12:00:06 +00:00
|
|
|
<th className="px-2 py-2 text-center text-xs font-bold text-green-700 uppercase tracking-wide w-[100px] min-w-[100px] bg-gradient-to-r from-green-50 to-emerald-50">
|
|
|
|
|
%<br/>
|
|
|
|
|
<span className="text-[10px] font-normal text-green-600">Total</span>
|
2025-12-09 20:41:58 +00:00
|
|
|
</th>
|
|
|
|
|
</React.Fragment>
|
2025-12-10 12:00:06 +00:00
|
|
|
);
|
|
|
|
|
})}
|
2025-12-09 14:29:32 +00:00
|
|
|
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide w-[120px] min-w-[120px] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
Total
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-2 py-2 text-center text-xs font-semibold text-gray-500 uppercase tracking-wide w-[100px] min-w-[100px] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
%
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
|
|
{/* Table Body */}
|
|
|
|
|
<tbody>
|
|
|
|
|
{hierarchicalData.map((row, index) => {
|
|
|
|
|
const linhaId = `${row.type}-${row.codigo_grupo || ""}-${row.codigo_conta || ""}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
return (
|
|
|
|
|
<TableRow
|
|
|
|
|
key={index}
|
|
|
|
|
row={row}
|
|
|
|
|
index={index}
|
|
|
|
|
handleRowClick={handleRowClick}
|
|
|
|
|
getRowStyle={getRowStyle}
|
|
|
|
|
getIndentStyle={getIndentStyle}
|
|
|
|
|
renderCellContent={renderCellContent}
|
|
|
|
|
mesesDisponiveis={mesesDisponiveis}
|
2025-12-09 20:41:58 +00:00
|
|
|
opcoesFiliais={opcoesFiliais}
|
2025-12-09 20:58:11 +00:00
|
|
|
filiaisSelecionadas={filiaisSelecionadas}
|
|
|
|
|
filtrosAplicados={filtrosAplicados}
|
2025-12-09 14:29:32 +00:00
|
|
|
formatCurrency={formatCurrency}
|
|
|
|
|
formatCurrencyWithColor={formatCurrencyWithColor}
|
|
|
|
|
getFixedCellBackground={getFixedCellBackground}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-09 19:30:55 +00:00
|
|
|
{/* Componente Analítico - Sempre visível, mas só carrega dados após clique */}
|
2025-12-09 14:29:32 +00:00
|
|
|
<div className="mt-4">
|
|
|
|
|
<AnaliticoComponent filtros={analiticoFiltros} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|