540 lines
15 KiB
Markdown
540 lines
15 KiB
Markdown
# 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
|
|
```typescript
|
|
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()`
|
|
```typescript
|
|
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()`
|
|
```typescript
|
|
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()`
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
interface AnaliticoProps {
|
|
filtros: {
|
|
dataInicio: string;
|
|
dataFim: string;
|
|
centroCusto?: string;
|
|
codigoGrupo?: string;
|
|
codigoSubgrupo?: string;
|
|
codigoConta?: string;
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Estados
|
|
```typescript
|
|
const [data, setData] = useState<AnaliticoItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
|
field: 'data_competencia',
|
|
direction: 'desc',
|
|
});
|
|
```
|
|
|
|
#### Funções Principais
|
|
|
|
##### `fetchData()`
|
|
```typescript
|
|
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()`
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// 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 `useCallback` para performance
|
|
- Effects com `useEffect` para 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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
- `useCallback` para funções de fetch
|
|
- `useMemo` para 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Implementar Context API** para estado global
|
|
2. **Adicionar React Query** para cache de dados
|
|
3. **Implementar Error Boundaries** robustos
|
|
4. **Adicionar testes unitários** e de integração
|
|
5. **Implementar lazy loading** de componentes
|
|
6. **Adicionar acessibilidade** (ARIA labels)
|
|
7. **Implementar temas** dark/light
|
|
8. **Adicionar animações** e transições
|