vendaweb-api/src/app/DRE/teste.tsx

1180 lines
43 KiB
TypeScript
Raw Normal View History

2025-10-20 20:35:24 +00:00
"use client";
2025-10-08 04:19:15 +00:00
2025-10-21 03:49:20 +00:00
import { LoaderPinwheel, ChevronDown, ChevronRight, Filter } from "lucide-react";
2025-10-20 20:35:24 +00:00
import { useEffect, useState } from "react";
import AnaliticoComponent from "./analitico";
2025-10-21 03:49:20 +00:00
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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;
codgrupo?: string;
isCalculado?: boolean;
2025-10-08 04:19:15 +00:00
}
interface HierarchicalRow {
2025-10-20 20:35:24 +00:00
type: "grupo" | "subgrupo" | "centro_custo" | "conta";
2025-10-08 04:19:15 +00:00
level: number;
grupo?: string;
subgrupo?: string;
centro_custo?: string;
conta?: string;
codigo_conta?: number;
total?: number;
isExpanded?: boolean;
valoresPorMes?: Record<string, number>;
percentuaisPorMes?: Record<string, number>;
2025-10-08 04:19:15 +00:00
}
export default function Teste() {
const [data, setData] = useState<DREItem[]>([]);
2025-10-21 03:49:20 +00:00
const [loading, setLoading] = useState(false);
2025-10-08 04:19:15 +00:00
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 [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
2025-10-21 03:49:20 +00:00
// Estados para filtros
const [filtros, setFiltros] = useState({
periodoDe: "",
periodoAte: "",
grupo: "Todos",
subgrupo: "Todos",
centroCusto: "Todos",
conta: "Todas",
valorMin: "",
valorMax: "",
buscaTextual: ""
});
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[]>([]);
const [opcoesSubgrupos, setOpcoesSubgrupos] = useState<string[]>([]);
const [opcoesCentrosCusto, setOpcoesCentrosCusto] = useState<string[]>([]);
const [opcoesContas, setOpcoesContas] = useState<string[]>([]);
// Estados para analítico
const [analiticoFiltros, setAnaliticoFiltros] = useState({
2025-10-20 20:35:24 +00:00
dataInicio: "",
dataFim: "",
centroCusto: "",
codigoGrupo: "",
codigoSubgrupo: "",
codigoConta: "",
});
2025-10-08 05:30:37 +00:00
const [linhaSelecionada, setLinhaSelecionada] = useState<string | null>(null);
2025-10-08 04:19:15 +00:00
useEffect(() => {
2025-10-21 03:49:20 +00:00
// Carregar períodos disponíveis da API
carregarPeriodosDisponiveis();
// Inicializar filtros com período atual
const agora = new Date();
const anoAtual = agora.getFullYear();
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
const periodoAtual = `${anoAtual}-${mesAtual}`;
setFiltros(prev => ({
...prev,
periodoDe: `${anoAtual}-01`,
periodoAte: periodoAtual
}));
2025-10-08 04:19:15 +00:00
}, []);
2025-10-21 03:49:20 +00:00
const carregarPeriodosDisponiveis = async () => {
try {
const response = await fetch("/api/dre-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();
setMesesDisponiveis(periodosUnicos);
// Extrair grupos únicos
const gruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.grupo))].sort();
setOpcoesGrupos(gruposUnicos);
// Extrair subgrupos únicos
const subgruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.subgrupo))].sort();
setOpcoesSubgrupos(subgruposUnicos);
// Extrair centros de custo únicos
const centrosCustoUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.centro_custo))].sort();
setOpcoesCentrosCusto(centrosCustoUnicos);
// Extrair contas únicas
const contasUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.conta))].sort();
setOpcoesContas(contasUnicas);
} catch (error) {
console.error("Erro ao carregar períodos:", error);
}
};
2025-10-08 04:19:15 +00:00
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch("/api/dre-oracle");
2025-10-08 04:19:15 +00:00
if (!response.ok) {
throw new Error(`Erro ao carregar dados: ${response.status}`);
2025-10-08 04:19:15 +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
2025-10-20 20:35:24 +00:00
).padStart(2, "0")}`;
2025-10-08 04:19:15 +00:00
})
),
].sort() as string[];
setMesesDisponiveis(meses);
} catch (err) {
2025-10-20 20:35:24 +00:00
setError(err instanceof Error ? err.message : "Erro desconhecido");
2025-10-08 04:19:15 +00:00
} finally {
setLoading(false);
}
};
const formatCurrency = (value: string | number) => {
2025-10-20 20:35:24 +00:00
const numValue = typeof value === "string" ? parseFloat(value) : value;
return numValue.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
2025-10-08 04:19:15 +00:00
});
};
2025-10-08 12:28:20 +00:00
const formatCurrencyWithColor = (value: string | number) => {
2025-10-20 20:35:24 +00:00
const numValue = typeof value === "string" ? parseFloat(value) : value;
2025-10-08 12:28:20 +00:00
const formatted = formatCurrency(value);
const isNegative = numValue < 0;
return { formatted, isNegative };
};
// Função para extrair códigos dos grupos e subgrupos
const extractCodes = (grupo: string, subgrupo?: string) => {
const grupoMatch = grupo.match(/^(\d+)/);
2025-10-20 20:35:24 +00:00
const codigoGrupo = grupoMatch ? grupoMatch[1] : "";
2025-10-20 20:35:24 +00:00
let codigoSubgrupo = "";
if (subgrupo) {
// Primeiro tenta extrair código numérico (ex: "001.008 - LOJA 8" -> "001.008")
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
if (subgrupoMatch) {
codigoSubgrupo = subgrupoMatch[1];
} else {
// Se não tem código numérico, usa a descrição completa (ex: "DESPESAS ADMINISTRATIVAS")
codigoSubgrupo = subgrupo;
}
}
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) => {
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(
2025-10-20 20:35:24 +00:00
row.grupo || "",
row.subgrupo
);
2025-10-08 05:30:37 +00:00
// Criar um identificador único para a linha
2025-10-20 20:35:24 +00:00
const linhaId = `${row.type}-${row.grupo || ""}-${row.subgrupo || ""}-${
row.centro_custo || ""
}-${row.codigo_conta || ""}`;
2025-10-08 05:30:37 +00:00
setLinhaSelecionada(linhaId);
// Se um mês específico foi selecionado, usar apenas esse mês
const dataInicioFiltro = mesSelecionado || dataInicioStr;
const dataFimFiltro = mesSelecionado || dataFimStr;
setAnaliticoFiltros({
2025-10-08 05:30:37 +00:00
dataInicio: dataInicioFiltro,
dataFim: dataFimFiltro,
2025-10-20 20:35:24 +00:00
centroCusto: row.centro_custo || "",
codigoGrupo,
codigoSubgrupo,
2025-10-20 20:35:24 +00:00
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);
};
2025-10-21 03:49:20 +00:00
const handleFiltroChange = (campo: string, valor: string) => {
setFiltros(prev => ({
...prev,
[campo]: valor
}));
};
const limparFiltros = () => {
const agora = new Date();
const anoAtual = agora.getFullYear();
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
const periodoAtual = `${anoAtual}-${mesAtual}`;
setFiltros({
periodoDe: `${anoAtual}-01`,
periodoAte: periodoAtual,
grupo: "Todos",
subgrupo: "Todos",
centroCusto: "Todos",
conta: "Todas",
valorMin: "",
valorMax: "",
buscaTextual: ""
});
// Limpar dados da tabela
setData([]);
setDadosFiltrados([]);
setFiltrosAplicados(false);
setMesesDisponiveis([]);
};
const aplicarFiltros = async () => {
// Fechar o Sheet primeiro
setIsFilterOpen(false);
// Aguardar um pouco para a animação de fechamento
setTimeout(async () => {
try {
setLoading(true);
setError(null);
// Carregar dados da API
const response = await fetch("/api/dre-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;
});
}
// Filtro por grupo
if (filtros.grupo !== "Todos") {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.grupo === filtros.grupo
);
}
// Filtro por subgrupo
if (filtros.subgrupo !== "Todos") {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.subgrupo === filtros.subgrupo
);
}
// Filtro por centro de custo
if (filtros.centroCusto !== "Todos") {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.centro_custo === filtros.centroCusto
);
}
// Filtro por conta
if (filtros.conta !== "Todas") {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.conta === filtros.conta
);
}
// Filtro por valor mínimo
if (filtros.valorMin) {
const valorMin = parseFloat(filtros.valorMin.replace(',', '.'));
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
parseFloat(item.valor) >= valorMin
);
}
// Filtro por valor máximo
if (filtros.valorMax) {
const valorMax = parseFloat(filtros.valorMax.replace(',', '.'));
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
parseFloat(item.valor) <= valorMax
);
}
// Filtro por busca textual
if (filtros.buscaTextual) {
const termoBusca = filtros.buscaTextual.toLowerCase();
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.grupo.toLowerCase().includes(termoBusca) ||
item.subgrupo.toLowerCase().includes(termoBusca) ||
item.centro_custo.toLowerCase().includes(termoBusca) ||
item.conta.toLowerCase().includes(termoBusca)
);
}
setData(dadosFiltrados);
setDadosFiltrados(dadosFiltrados);
setFiltrosAplicados(true);
// Extrair meses únicos dos dados filtrados
const mesesUnicos = [...new Set(dadosFiltrados.map(item => item.data_competencia))].sort();
setMesesDisponiveis(mesesUnicos);
} catch (error) {
console.error("Erro ao aplicar filtros:", error);
setError(error instanceof Error ? error.message : "Erro desconhecido");
} finally {
setLoading(false);
}
}, 300); // Aguardar 300ms para a animação de fechamento
};
2025-10-08 04:19:15 +00:00
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
2025-10-20 20:35:24 +00:00
).padStart(2, "0")}`;
2025-10-08 04:19:15 +00:00
if (!valoresPorMes[anoMes]) {
valoresPorMes[anoMes] = 0;
}
valoresPorMes[anoMes] += parseFloat(item.valor);
});
return valoresPorMes;
};
// Função para calcular percentuais baseado no grupo 03 como referência
const calcularPercentuaisPorMes = (
valoresPorMes: Record<string, number>,
grupo: string
): Record<string, number> => {
const percentuais: Record<string, number> = {};
// Se for o grupo 03, retorna 100% para todos os meses
2025-10-20 20:35:24 +00:00
if (grupo.includes("03")) {
Object.keys(valoresPorMes).forEach((mes) => {
percentuais[mes] = 100;
});
return percentuais;
}
// Para outros grupos, calcular percentual baseado no grupo 03
Object.keys(valoresPorMes).forEach((mes) => {
const valorAtual = valoresPorMes[mes];
// Encontrar o valor do grupo 03 para o mesmo mês
const grupo03Items = data.filter((item) => {
const dataCompetencia = new Date(item.data_competencia);
const anoMes = `${dataCompetencia.getFullYear()}-${String(
dataCompetencia.getMonth() + 1
2025-10-20 20:35:24 +00:00
).padStart(2, "0")}`;
return anoMes === mes && item.grupo.includes("03");
});
const valorGrupo03 = grupo03Items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
if (valorGrupo03 !== 0) {
percentuais[mes] = (valorAtual / valorGrupo03) * 100;
} else {
percentuais[mes] = 0;
}
});
return percentuais;
};
2025-10-08 04:19:15 +00:00
const buildHierarchicalData = (): HierarchicalRow[] => {
const rows: HierarchicalRow[] = [];
// Agrupar por grupo
2025-10-08 04:19:15 +00:00
const grupos = data.reduce((acc, item) => {
if (!acc[item.grupo]) {
acc[item.grupo] = [];
2025-10-08 04:19:15 +00:00
}
acc[item.grupo].push(item);
2025-10-08 04:19:15 +00:00
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar grupos pelo CODGRUPO numérico
const sortedGrupos = Object.entries(grupos).sort(([grupoA, itemsA], [grupoB, itemsB]) => {
// Pegar o CODGRUPO do primeiro item de cada grupo
const codgrupoA = itemsA[0]?.codgrupo || "";
const codgrupoB = itemsB[0]?.codgrupo || "";
// Se ambos têm CODGRUPO, ordenar numericamente
if (codgrupoA && codgrupoB) {
return parseInt(codgrupoA) - parseInt(codgrupoB);
}
// Se apenas um tem CODGRUPO, ele vem primeiro
if (codgrupoA && !codgrupoB) return -1;
if (!codgrupoA && codgrupoB) return 1;
// Se nenhum tem CODGRUPO, ordenar alfabeticamente
return grupoA.localeCompare(grupoB);
});
2025-10-08 04:19:15 +00:00
sortedGrupos.forEach(([grupo, items]) => {
const totalGrupo = items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do grupo
const valoresPorMes = calcularValoresPorMes(items);
2025-10-08 04:19:15 +00:00
rows.push({
2025-10-20 20:35:24 +00:00
type: "grupo",
2025-10-08 04:19:15 +00:00
level: 0,
grupo,
total: totalGrupo,
isExpanded: expandedGroups.has(grupo),
valoresPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresPorMes, grupo),
2025-10-08 04:19:15 +00:00
});
if (expandedGroups.has(grupo)) {
// Agrupar por subgrupo dentro do grupo
const subgrupos = items.reduce((acc, item) => {
// Se o item originalmente era do grupo 05, agrupar tudo sob o nome do grupo 05
2025-10-20 20:35:24 +00:00
if (item.grupo.includes("05")) {
const subgrupoKey = item.grupo; // Usar o nome completo do grupo 05
if (!acc[subgrupoKey]) {
acc[subgrupoKey] = [];
}
acc[subgrupoKey].push(item);
} else {
// Para outros itens, agrupar normalmente por subgrupo
if (!acc[item.subgrupo]) {
acc[item.subgrupo] = [];
}
acc[item.subgrupo].push(item);
2025-10-08 04:19:15 +00:00
}
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar subgrupos
2025-10-20 20:35:24 +00:00
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) =>
a.localeCompare(b)
);
2025-10-08 04:19:15 +00:00
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
const totalSubgrupo = subgrupoItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do subgrupo
const valoresSubgrupoPorMes = calcularValoresPorMes(subgrupoItems);
2025-10-08 04:19:15 +00:00
rows.push({
2025-10-20 20:35:24 +00:00
type: "subgrupo",
2025-10-08 04:19:15 +00:00
level: 1,
grupo,
subgrupo,
total: totalSubgrupo,
isExpanded: expandedSubgrupos.has(`${grupo}-${subgrupo}`),
valoresPorMes: valoresSubgrupoPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresSubgrupoPorMes,
grupo
),
2025-10-08 04:19:15 +00:00
});
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
2025-10-20 20:35:24 +00:00
const sortedCentros = Object.entries(centros).sort(([a], [b]) =>
a.localeCompare(b)
);
2025-10-08 04:19:15 +00:00
sortedCentros.forEach(([centro, centroItems]) => {
const totalCentro = centroItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do centro de custo
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
2025-10-08 04:19:15 +00:00
rows.push({
2025-10-20 20:35:24 +00:00
type: "centro_custo",
2025-10-08 04:19:15 +00:00
level: 2,
grupo,
subgrupo,
centro_custo: centro,
total: totalCentro,
isExpanded: expandedCentros.has(
`${grupo}-${subgrupo}-${centro}`
),
valoresPorMes: valoresCentroPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresCentroPorMes,
grupo
),
2025-10-08 04:19:15 +00:00
});
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
2025-10-20 20:35:24 +00:00
const sortedContas = Object.entries(contas).sort(([a], [b]) =>
a.localeCompare(b)
);
2025-10-08 04:19:15 +00:00
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha da conta (sem ano/mês no nome)
const valoresContaPorMes = calcularValoresPorMes(contaItems);
2025-10-08 04:19:15 +00:00
rows.push({
2025-10-20 20:35:24 +00:00
type: "conta",
2025-10-08 04:19:15 +00:00
level: 3,
grupo,
subgrupo,
centro_custo: centro,
conta,
codigo_conta: contaItems[0].codigo_conta,
total: totalConta,
valoresPorMes: valoresContaPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresContaPorMes,
grupo
),
2025-10-08 04:19:15 +00:00
});
});
}
});
}
});
}
});
return rows;
};
const getRowStyle = (row: HierarchicalRow) => {
2025-10-20 20:35:24 +00:00
const baseStyle =
"transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30";
2025-10-08 04:19:15 +00:00
2025-10-08 05:30:37 +00:00
// Criar identificador único para a linha
2025-10-20 20:35:24 +00:00
const linhaId = `${row.type}-${row.grupo || ""}-${row.subgrupo || ""}-${
row.centro_custo || ""
}-${row.codigo_conta || ""}`;
2025-10-08 05:30:37 +00:00
const isSelected = linhaSelecionada === linhaId;
let style = baseStyle;
if (isSelected) {
2025-10-20 20:35:24 +00:00
style +=
" bg-gradient-to-r from-blue-100 to-indigo-100 border-l-4 border-blue-500 shadow-lg";
2025-10-08 05:30:37 +00:00
}
2025-10-08 04:19:15 +00:00
switch (row.type) {
2025-10-20 20:35:24 +00:00
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-10-20 20:35:24 +00:00
case "subgrupo":
return `${style} bg-gradient-to-r from-gray-50/30 to-blue-50/20 font-semibold text-gray-800`;
2025-10-20 20:35:24 +00:00
case "centro_custo":
return `${style} bg-gradient-to-r from-gray-50/20 to-gray-100/10 font-medium text-gray-700`;
2025-10-20 20:35:24 +00:00
case "conta":
return `${style} bg-white font-normal text-gray-600`;
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) => {
// Verificar se é um grupo calculado
const isCalculado = row.grupo?.includes("CALCULADO") ||
row.grupo?.includes("FATURAMENTO LÍQUIDO") ||
row.grupo?.includes("LUCRO BRUTO") ||
row.grupo?.includes("MARGEM LOJA") ||
row.grupo?.includes("RESULTADO OPERACIONAL") ||
row.grupo?.includes("RESULTADO FINANCEIRO") ||
row.grupo?.includes("OUTRAS RECEITAS / DESPESAS") ||
row.grupo?.includes("LAIR") ||
row.grupo?.includes("IR") ||
row.grupo?.includes("CSLL") ||
row.grupo?.includes("LUCRO LÍQUIDO") ||
row.grupo?.includes("EBITDA");
2025-10-08 04:19:15 +00:00
switch (row.type) {
2025-10-20 20:35:24 +00:00
case "grupo":
2025-10-08 04:19:15 +00:00
return (
<div className="flex items-center gap-3 whitespace-nowrap">
2025-10-08 04:19:15 +00:00
<button
onClick={() => toggleGroup(row.grupo!)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
2025-10-08 04:19:15 +00:00
>
2025-10-20 21:09:03 +00:00
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600" />
)}
2025-10-08 04:19:15 +00:00
</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>
{isCalculado && (
<span className="text-blue-600 font-bold text-sm"></span>
)}
</div>
</button>
2025-10-08 04:19:15 +00:00
</div>
);
2025-10-20 20:35:24 +00:00
case "subgrupo":
2025-10-08 04:19:15 +00:00
return (
<div className="flex items-center gap-3 whitespace-nowrap">
2025-10-08 04:19:15 +00:00
<button
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
2025-10-08 04:19:15 +00:00
>
2025-10-20 21:09:03 +00:00
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600" />
)}
2025-10-08 04:19:15 +00:00
</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"
>
2025-10-20 20:35:24 +00:00
<span className="font-semibold text-gray-800">
{row.subgrupo}
</span>
</button>
2025-10-08 04:19:15 +00:00
</div>
);
2025-10-20 20:35:24 +00:00
case "centro_custo":
2025-10-08 04:19:15 +00:00
return (
<div className="flex items-center gap-3 whitespace-nowrap">
2025-10-08 04:19:15 +00:00
<button
onClick={() =>
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
2025-10-08 04:19:15 +00:00
>
2025-10-20 21:09:03 +00:00
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600" />
)}
2025-10-08 04:19:15 +00:00
</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"
>
2025-10-20 20:35:24 +00:00
<span className="font-medium text-gray-700">
{row.centro_custo}
</span>
</button>
2025-10-08 04:19:15 +00:00
</div>
);
2025-10-20 20:35:24 +00:00
case "conta":
2025-10-08 04:19:15 +00:00
return (
<div className="flex items-center gap-3 whitespace-nowrap">
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
<span className="text-gray-400 font-bold text-lg"></span>
</div>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<span className="font-normal text-gray-600">{row.conta}</span>
</button>
2025-10-08 04:19:15 +00:00
</div>
);
default:
return null;
}
};
2025-10-21 03:49:20 +00:00
// Loading será tratado dentro do componente principal
// Error será tratado dentro do componente principal
const hierarchicalData = buildHierarchicalData();
return (
<div className="w-full max-w-none mx-auto p-6">
{/* Header Section */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div>
2025-10-21 03:49:20 +00:00
<h1 className="text-2xl font-bold text-gray-900">DRE Gerencial</h1>
2025-10-20 20:35:24 +00:00
<p className="text-sm text-gray-500">
Demonstração do Resultado do Exercício
</p>
</div>
</div>
2025-10-21 03:49:20 +00:00
{/* Botão de Filtro */}
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
<SheetTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<Filter className="w-4 h-4" />
Filtros
</Button>
</SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px]">
<SheetHeader>
<SheetTitle>Filtros</SheetTitle>
<SheetDescription>
Ajuste os critérios e clique em Pesquisar para atualizar a visão.
</SheetDescription>
</SheetHeader>
<div className="grid flex-1 auto-rows-min gap-6 py-4">
{/* Período */}
<div className="grid gap-3">
<Label>Período</Label>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="periodo-de" className="text-xs text-gray-500">DE</Label>
<Select value={filtros.periodoDe} onValueChange={(value) => handleFiltroChange('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) => handleFiltroChange('periodoAte', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{mesesDisponiveis.map(mes => (
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
2025-10-20 20:35:24 +00:00
{/* Grupo
2025-10-21 03:49:20 +00:00
<div className="grid gap-3">
<Label htmlFor="grupo">GRUPO</Label>
<Select value={filtros.grupo} onValueChange={(value) => handleFiltroChange('grupo', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos</SelectItem>
{opcoesGrupos.map(grupo => (
<SelectItem key={grupo} value={grupo}>{grupo}</SelectItem>
))}
</SelectContent>
</Select>
</div>*/}
2025-10-21 03:49:20 +00:00
{/* Subgrupo
2025-10-21 03:49:20 +00:00
<div className="grid gap-3">
<Label htmlFor="subgrupo">SUBGRUPO</Label>
<Select value={filtros.subgrupo} onValueChange={(value) => handleFiltroChange('subgrupo', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos</SelectItem>
{opcoesSubgrupos.map(subgrupo => (
<SelectItem key={subgrupo} value={subgrupo}>{subgrupo}</SelectItem>
))}
</SelectContent>
</Select>
</div>*/}
2025-10-21 03:49:20 +00:00
{/* Centro de Custo
2025-10-21 03:49:20 +00:00
<div className="grid gap-3">
<Label htmlFor="centro-custo">CENTRO DE CUSTO</Label>
<Select value={filtros.centroCusto} onValueChange={(value) => handleFiltroChange('centroCusto', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos</SelectItem>
{opcoesCentrosCusto.map(centro => (
<SelectItem key={centro} value={centro}>{centro}</SelectItem>
))}
</SelectContent>
</Select>
</div>*/}
2025-10-21 03:49:20 +00:00
{/* Conta
2025-10-21 03:49:20 +00:00
<div className="grid gap-3">
<Label htmlFor="conta">CONTA</Label>
<Select value={filtros.conta} onValueChange={(value) => handleFiltroChange('conta', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todas">Todas</SelectItem>
{opcoesContas.map(conta => (
<SelectItem key={conta} value={conta}>{conta}</SelectItem>
))}
</SelectContent>
</Select>
</div>*/}
2025-10-21 03:49:20 +00:00
{/* Valor
2025-10-21 03:49:20 +00:00
<div className="grid gap-3">
<Label>Valor</Label>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="valor-min" className="text-xs text-gray-500">VALOR MÍN.</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">R$</span>
<Input
id="valor-min"
value={filtros.valorMin}
onChange={(e) => handleFiltroChange('valorMin', e.target.value)}
className="pl-8"
placeholder="0,00"
/>
</div>
</div>
<div>
<Label htmlFor="valor-max" className="text-xs text-gray-500">VALOR MÁX.</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">R$</span>
<Input
id="valor-max"
value={filtros.valorMax}
onChange={(e) => handleFiltroChange('valorMax', e.target.value)}
className="pl-8"
placeholder="0,00"
/>
</div>
</div>
</div>
</div>*/}
2025-10-21 03:49:20 +00:00
{/* Busca Textual
2025-10-21 03:49:20 +00:00
<div className="grid gap-3">
<Label htmlFor="busca-textual">BUSCA TEXTUAL</Label>
<Input
id="busca-textual"
value={filtros.buscaTextual}
onChange={(e) => handleFiltroChange('buscaTextual', e.target.value)}
placeholder="Pesquise por grupo, subgrupo, centro ou conta"
/>
</div>*/}
2025-10-21 03:49:20 +00:00
</div>
<SheetFooter className="flex gap-3">
{/* <Button variant="outline" onClick={limparFiltros} className="flex-1">
2025-10-21 03:49:20 +00:00
Limpar filtros
</Button> */}
2025-10-21 03:49:20 +00:00
<Button variant="outline" onClick={() => setIsFilterOpen(false)} className="flex-1">
Cancelar
</Button>
<Button onClick={aplicarFiltros} className="flex-1">
Pesquisar
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
2025-10-08 04:19:15 +00:00
</div>
</div>
2025-10-21 03:49:20 +00:00
{/* Loading quando aplicando filtros */}
{loading && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
<LoaderPinwheel className="w-8 h-8 text-blue-600 animate-spin" />
</div>
<div>
2025-10-21 03:49:20 +00:00
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Aplicando filtros...
</h3>
<p className="text-gray-500">
Aguarde enquanto processamos os dados.
2025-10-20 20:35:24 +00:00
</p>
</div>
</div>
</div>
2025-10-21 03:49:20 +00:00
)}
2025-10-20 20:35:24 +00:00
2025-10-21 03:49:20 +00:00
{/* Erro */}
{error && !loading && (
<div className="bg-white rounded-xl shadow-lg border border-red-200 p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="w-16 h-16 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>
2025-10-21 03:49:20 +00:00
<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-2">
Tentar novamente
</Button>
</div>
</div>
2025-10-08 04:19:15 +00:00
</div>
2025-10-21 03:49:20 +00:00
)}
2025-10-08 04:19:15 +00:00
2025-10-21 03:49:20 +00:00
{/* Mensagem quando não há dados */}
{!filtrosAplicados && !loading && !error && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="w-16 h-16 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-2">
Nenhum dado exibido
</h3>
<p className="text-gray-500 mb-4">
Clique no botão "Filtros" para definir os critérios de busca e visualizar os dados do DRE.
</p>
</div>
</div>
</div>
2025-10-21 03:49:20 +00:00
)}
2025-10-08 04:19:15 +00:00
{/* Table Container */}
2025-10-21 03:49:20 +00:00
{filtrosAplicados && !loading && !error && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
{/* Table Header */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
<div className="flex items-center gap-4 px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wide">
<div className="flex-1 min-w-[300px] max-w-[400px]">Descrição</div>
2025-10-08 04:19:15 +00:00
{mesesDisponiveis.map((mes) => (
2025-10-20 20:56:42 +00:00
<div key={mes} className="flex min-w-[240px] max-w-[300px] gap-2">
<div className="flex-1 min-w-[120px] text-right">{mes}</div>
<div className="flex-1 min-w-[100px] text-center text-xs text-gray-500">
2025-10-20 20:35:24 +00:00
%
</div>
2025-10-08 04:26:38 +00:00
</div>
2025-10-08 04:19:15 +00:00
))}
<div className="flex-1 min-w-[120px] text-right">Total</div>
2025-10-08 04:26:38 +00:00
</div>
</div>
{/* Table Body */}
<div className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
2025-10-08 04:45:26 +00:00
{hierarchicalData.map((row, index) => (
2025-10-20 20:35:24 +00:00
<div
key={index}
className={`flex items-center gap-4 px-4 py-3 text-sm border-b border-gray-100 hover:bg-gray-50 transition-colors ${getRowStyle(
row
)}`}
>
<div
className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden"
style={getIndentStyle(row.level)}
>
2025-10-08 04:45:26 +00:00
{renderCellContent(row)}
</div>
2025-10-08 04:19:15 +00:00
{mesesDisponiveis.map((mes) => (
2025-10-20 20:56:42 +00:00
<div key={mes} className="flex min-w-[240px] max-w-[300px] gap-2">
<div
2025-10-20 20:56:42 +00:00
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
onClick={() => handleRowClick(row, mes)}
2025-10-20 20:35:24 +00:00
title={
row.valoresPorMes && row.valoresPorMes[mes]
? formatCurrency(row.valoresPorMes[mes])
: "-"
}
>
{row.valoresPorMes && row.valoresPorMes[mes]
? (() => {
2025-10-20 20:35:24 +00:00
const { formatted, isNegative } =
formatCurrencyWithColor(row.valoresPorMes[mes]);
return (
2025-10-20 20:35:24 +00:00
<span
className={
isNegative
? "text-red-600 font-bold"
: "text-gray-900"
}
>
{formatted}
</span>
);
})()
2025-10-20 20:35:24 +00:00
: "-"}
</div>
<div
2025-10-20 20:56:42 +00:00
className="flex-1 min-w-[100px] text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
onClick={() => handleRowClick(row, mes)}
2025-10-20 20:35:24 +00:00
title={
row.percentuaisPorMes &&
row.percentuaisPorMes[mes] !== undefined
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
: "-"
}
>
2025-10-20 20:35:24 +00:00
{row.percentuaisPorMes &&
row.percentuaisPorMes[mes] !== undefined
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
2025-10-20 20:35:24 +00:00
: "-"}
</div>
2025-10-08 04:45:26 +00:00
</div>
))}
2025-10-08 05:30:37 +00:00
<div
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
2025-10-08 05:30:37 +00:00
onClick={() => handleRowClick(row)}
2025-10-20 20:35:24 +00:00
title={row.total ? formatCurrency(row.total) : "-"}
2025-10-08 05:30:37 +00:00
>
2025-10-08 12:28:20 +00:00
{(() => {
2025-10-20 20:35:24 +00:00
const { formatted, isNegative } = formatCurrencyWithColor(
row.total!
);
2025-10-08 12:28:20 +00:00
return (
2025-10-20 20:35:24 +00:00
<span
className={
isNegative ? "text-red-600 font-bold" : "text-gray-900"
}
>
2025-10-08 12:28:20 +00:00
{formatted}
</span>
);
})()}
2025-10-08 04:45:26 +00:00
</div>
</div>
))}
</div>
2025-10-08 04:26:38 +00:00
</div>
2025-10-21 03:49:20 +00:00
)}
{/* Componente Analítico */}
{!loading && data.length > 0 && (
<AnaliticoComponent filtros={analiticoFiltros} />
)}
2025-10-08 04:19:15 +00:00
</div>
);
}