15 KiB
15 KiB
Componentes - DRE Gerencial
Visão Geral
O sistema DRE Gerencial é construído com componentes React funcionais em TypeScript, seguindo padrões modernos de desenvolvimento frontend.
Estrutura de Componentes
1. Componente Principal (src/app/DRE/teste.tsx)
Responsabilidades
- Orquestração da interface DRE hierárquica
- Gerenciamento de estado de expansão/colapso
- Controle de ordenação e filtros
- Integração com componente analítico
Estados Principais
const [data, setData] = useState<DREItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(new Set());
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(new Set());
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: 'descricao',
direction: 'asc',
});
const [analiticoFiltros, setAnaliticoFiltros] = useState({
dataInicio: '',
dataFim: '',
centroCusto: '',
codigoGrupo: '',
codigoSubgrupo: '',
codigoConta: '',
});
Funções Principais
fetchData()
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/dre');
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) => {
const dataCompetencia = new Date(item.data_competencia);
return `${dataCompetencia.getFullYear()}-${String(
dataCompetencia.getMonth() + 1
).padStart(2, '0')}`;
})
)].sort() as string[];
setMesesDisponiveis(meses);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setLoading(false);
}
};
buildHierarchicalData()
const buildHierarchicalData = (): HierarchicalRow[] => {
const rows: HierarchicalRow[] = [];
// Agrupar por grupo, tratando grupo 05 como subgrupo do grupo 04
const grupos = data.reduce((acc, item) => {
if (item.grupo.includes('05')) {
// Lógica especial para grupo 05
const grupo04Key = Object.keys(acc).find((key) => key.includes('04'));
if (grupo04Key) {
acc[grupo04Key].push(item);
} else {
const grupo04Nome = '04 - GRUPO 04';
if (!acc[grupo04Nome]) {
acc[grupo04Nome] = [];
}
acc[grupo04Nome].push(item);
}
} else {
if (!acc[item.grupo]) {
acc[item.grupo] = [];
}
acc[item.grupo].push(item);
}
return acc;
}, {} as Record<string, DREItem[]>);
// Construir hierarquia completa
// ... lógica de construção hierárquica
return rows;
};
handleRowClick()
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
if (!data.length) return;
// Calcular período baseado nos dados
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);
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7);
const { codigoGrupo, codigoSubgrupo } = extractCodes(
row.grupo || '',
row.subgrupo
);
// Criar identificador único para a linha
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
row.centro_custo || ''
}-${row.codigo_conta || ''}`;
setLinhaSelecionada(linhaId);
// Configurar filtros para análise analítica
const dataInicioFiltro = mesSelecionado || dataInicioStr;
const dataFimFiltro = mesSelecionado || dataFimStr;
setAnaliticoFiltros({
dataInicio: dataInicioFiltro,
dataFim: dataFimFiltro,
centroCusto: row.centro_custo || '',
codigoGrupo,
codigoSubgrupo,
codigoConta: row.codigo_conta?.toString() || '',
});
};
Renderização
return (
<div className="w-full flex flex-col items-center gap-2">
<div className="mb-1">
<h1 className="text-lg font-bold">DRE Gerencial</h1>
</div>
{/* Tabela hierárquica */}
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative">
{/* Header fixo */}
<div className="sticky top-0 z-30 border-b shadow-sm">
{/* ... header content */}
</div>
{/* Dados hierárquicos */}
<div className="flex flex-col">
{hierarchicalData.map((row, index) => (
<div key={index} className={`flex ${getRowStyle(row)}`}>
{/* ... row content */}
</div>
))}
</div>
</div>
{/* Componente Analítico */}
{!loading && data.length > 0 && (
<AnaliticoComponent filtros={analiticoFiltros} />
)}
</div>
);
2. Componente Analítico (src/app/DRE/analitico.tsx)
Responsabilidades
- Visualização detalhada de transações
- Ordenação de dados analíticos
- Exportação para Excel
- Aplicação de filtros dinâmicos
Props
interface AnaliticoProps {
filtros: {
dataInicio: string;
dataFim: string;
centroCusto?: string;
codigoGrupo?: string;
codigoSubgrupo?: string;
codigoConta?: string;
};
}
Estados
const [data, setData] = useState<AnaliticoItem[]>([]);
const [loading, setLoading] = useState(false);
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: 'data_competencia',
direction: 'desc',
});
Funções Principais
fetchData()
const fetchData = useCallback(async () => {
if (!filtros.dataInicio || !filtros.dataFim) {
setData([]);
return;
}
setLoading(true);
try {
const params = new URLSearchParams({
dataInicio: filtros.dataInicio,
dataFim: filtros.dataFim,
...(filtros.centroCusto && { centroCusto: filtros.centroCusto }),
...(filtros.codigoGrupo && { codigoGrupo: filtros.codigoGrupo }),
...(filtros.codigoSubgrupo && { codigoSubgrupo: filtros.codigoSubgrupo }),
...(filtros.codigoConta && { codigoConta: filtros.codigoConta }),
});
const response = await fetch(`/api/analitico?${params}`);
if (response.ok) {
const result = await response.json();
setData(result as AnaliticoItem[]);
} else {
console.error('Erro ao buscar dados:', await response.text());
}
} catch (error) {
console.error('Erro ao buscar dados:', error);
} finally {
setLoading(false);
}
}, [filtros]);
exportToExcel()
const exportToExcel = () => {
if (data.length === 0) return;
// Preparar dados para exportação
const exportData = data.map((item) => ({
'Data Competência': new Date(item.data_competencia).toLocaleDateString('pt-BR'),
'Data Vencimento': new Date(item.data_vencimento).toLocaleDateString('pt-BR'),
'Data Caixa': new Date(item.data_caixa).toLocaleDateString('pt-BR'),
'Código Fornecedor': item.codigo_fornecedor,
Fornecedor: item.nome_fornecedor,
'Código Centro Custo': item.codigo_centrocusto,
'Centro Custo': item.codigo_centrocusto,
'Código Conta': item.codigo_conta,
Conta: item.conta,
Valor: typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor,
Histórico: item.historico,
'Histórico 2': item.historico2,
Recnum: item.recnum,
}));
// Criar workbook
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(exportData);
// Adicionar resumo na segunda aba
const resumoData = [
{ Métrica: 'Total de Registros', Valor: data.length },
{ Métrica: 'Valor Total', Valor: totalValor },
];
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
// Adicionar abas ao workbook
XLSX.utils.book_append_sheet(wb, ws, 'Dados Analíticos');
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
// Gerar nome do arquivo com timestamp
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
const fileName = `analitico_${timestamp}.xlsx`;
// Fazer download
XLSX.writeFile(wb, fileName);
};
3. Componentes UI (src/components/ui/)
Button Component
// src/components/ui/button.tsx
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
Padrões de Design
1. Composition Pattern
- Componentes pequenos e focados
- Props tipadas com TypeScript
- Reutilização através de composition
2. State Management
- Estados locais com
useState - Callbacks com
useCallbackpara performance - Effects com
useEffectpara side effects
3. Styling
- Tailwind CSS para styling
- Class variance authority para variantes
- Responsive design mobile-first
4. Type Safety
- Interfaces TypeScript para props
- Tipos específicos para dados
- Validação de tipos em runtime
Utilitários
1. Formatação
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value);
};
const formatCurrencyWithColor = (value: number) => {
const formatted = formatCurrency(value);
const isNegative = value < 0;
return { formatted, isNegative };
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('pt-BR');
};
2. Extração de Códigos
const extractCodes = (grupo: string, subgrupo?: string) => {
const grupoMatch = grupo.match(/^(\d+)/);
const codigoGrupo = grupoMatch ? grupoMatch[1] : '';
let codigoSubgrupo = '';
if (subgrupo) {
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
if (subgrupoMatch) {
codigoSubgrupo = subgrupoMatch[1];
} else {
codigoSubgrupo = subgrupo;
}
}
return { codigoGrupo, codigoSubgrupo };
};
3. Cálculos
const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
const valoresPorMes: Record<string, number> = {};
items.forEach((item) => {
const dataCompetencia = new Date(item.data_competencia);
const anoMes = `${dataCompetencia.getFullYear()}-${String(
dataCompetencia.getMonth() + 1
).padStart(2, '0')}`;
if (!valoresPorMes[anoMes]) {
valoresPorMes[anoMes] = 0;
}
valoresPorMes[anoMes] += parseFloat(item.valor);
});
return valoresPorMes;
};
const 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
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
).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;
};
Performance
1. Otimizações Implementadas
useCallbackpara funções de fetchuseMemopara cálculos pesados (potencial)- Renderização condicional
2. Estratégias de Renderização
- Lazy loading de componentes
- Virtualização para listas grandes (potencial)
- Debounce para filtros (potencial)
Testes
1. Testes Unitários
// Exemplo de teste para componente
import { render, screen } from '@testing-library/react';
import Teste from './teste';
describe('Teste Component', () => {
it('renders DRE title', () => {
render(<Teste />);
expect(screen.getByText('DRE Gerencial')).toBeInTheDocument();
});
});
2. Testes de Integração
// Teste de interação com API
describe('DRE Integration', () => {
it('loads data from API', async () => {
render(<Teste />);
await waitFor(() => {
expect(screen.getByText('Carregando dados...')).toBeInTheDocument();
});
// Verificar se dados foram carregados
});
});
Próximos Passos
- Implementar Context API para estado global
- Adicionar React Query para cache de dados
- Implementar Error Boundaries robustos
- Adicionar testes unitários e de integração
- Implementar lazy loading de componentes
- Adicionar acessibilidade (ARIA labels)
- Implementar temas dark/light
- Adicionar animações e transições