From 7758ad5abeaf92bef16e65a754b675b105165e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alessandro=20Gon=C3=A7aalves?= Date: Mon, 20 Oct 2025 12:15:53 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20adicionados=20filtros=20e=20estiliza?= =?UTF-8?q?=C3=A7=C3=A3o=20na=20tabela=20de=20anal=C3=ADtico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.json | 6 + docs/README.md | 95 +++++ docs/api.md | 377 +++++++++++++++++ docs/architecture.md | 185 +++++++++ docs/components.md | 539 ++++++++++++++++++++++++ docs/database.md | 264 ++++++++++++ docs/deployment.md | 580 ++++++++++++++++++++++++++ docs/development.md | 529 ++++++++++++++++++++++++ docs/troubleshooting.md | 603 +++++++++++++++++++++++++++ package-lock.json | 766 +++++++++++++++++++++++++++++++++- package.json | 4 + src/app/DRE/analitico.tsx | 777 +++++++++++++++++++++-------------- src/components/ui/card.tsx | 27 ++ src/components/ui/dialog.tsx | 119 ++++++ src/components/ui/input.tsx | 24 ++ src/components/ui/select.tsx | 157 +++++++ 16 files changed, 4736 insertions(+), 316 deletions(-) create mode 100644 .eslintrc.json create mode 100644 docs/README.md create mode 100644 docs/api.md create mode 100644 docs/architecture.md create mode 100644 docs/components.md create mode 100644 docs/database.md create mode 100644 docs/deployment.md create mode 100644 docs/development.md create mode 100644 docs/troubleshooting.md create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/select.tsx diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..13015d6 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "next/core-web-vitals", + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f553c41 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,95 @@ +# DRE Gerencial - Documentação do Sistema + +## Visão Geral + +O **DRE Gerencial** é um sistema web desenvolvido em Next.js para análise e visualização de dados financeiros através de uma Demonstração do Resultado do Exercício (DRE) hierárquica e interativa. + +## Objetivo Principal + +O sistema tem como objetivo principal fornecer uma interface intuitiva para análise de dados financeiros empresariais, permitindo: + +- Visualização hierárquica de dados financeiros (Grupo → Subgrupo → Centro de Custo → Conta) +- Análise temporal por períodos mensais +- Drill-down analítico para detalhamento de transações +- Exportação de dados para Excel +- Cálculo de percentuais baseados em grupos de referência + +## Características Principais + +### 1. **Interface Hierárquica** +- Estrutura em árvore expansível (Grupo → Subgrupo → Centro de Custo → Conta) +- Visualização de valores e percentuais por mês +- Ordenação por descrição ou valor total +- Seleção de linhas para análise detalhada + +### 2. **Análise Analítica** +- Drill-down a partir de qualquer nível hierárquico +- Filtros por período, centro de custo, grupo, subgrupo e conta +- Visualização detalhada de transações individuais +- Exportação para Excel com múltiplas abas + +### 3. **Cálculos Automáticos** +- Percentuais baseados no Grupo 03 como referência +- Totais consolidados por nível hierárquico +- Valores por mês com formatação monetária brasileira + +## Estrutura do Projeto + +``` +src/ +├── app/ +│ ├── api/ +│ │ ├── analitico/route.ts # API para dados analíticos +│ │ └── dre/route.ts # API para dados DRE +│ ├── DRE/ +│ │ ├── analitico.tsx # Componente de análise analítica +│ │ ├── page.tsx # Página principal +│ │ └── teste.tsx # Componente principal DRE +│ └── layout.tsx # Layout da aplicação +├── components/ +│ └── ui/ # Componentes UI reutilizáveis +├── db/ +│ ├── index.ts # Configuração do banco +│ └── schema.ts # Schema do banco de dados +└── lib/ + └── utils.ts # Utilitários +``` + +## Tecnologias Utilizadas + +- **Frontend**: Next.js 15, React 19, TypeScript +- **Styling**: Tailwind CSS 4 +- **Database**: PostgreSQL com Drizzle ORM +- **UI Components**: Radix UI, Lucide React +- **Export**: XLSX para Excel + +## Documentação Detalhada + +- [Arquitetura do Sistema](./architecture.md) +- [Banco de Dados](./database.md) +- [APIs](./api.md) +- [Componentes](./components.md) +- [Guia de Desenvolvimento](./development.md) +- [Deploy e Configuração](./deployment.md) +- [Troubleshooting](./troubleshooting.md) + +## Quick Start + +1. Instalar dependências: `npm install` +2. Configurar variáveis de ambiente (ver [Deploy](./deployment.md)) +3. Executar: `npm run dev` +4. Acessar: `http://localhost:3000/DRE` + +## Manutenção + +Para manter o sistema sem perder suas características: + +1. **Preserve a hierarquia**: Grupo → Subgrupo → Centro de Custo → Conta +2. **Mantenha os cálculos**: Percentuais baseados no Grupo 03 +3. **Conserve a funcionalidade**: Drill-down e exportação Excel +4. **Atualize dados**: Mantenha sincronização com fonte de dados +5. **Teste filtros**: Valide todos os filtros analíticos + +--- + +*Última atualização: $(date)* diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..af48e04 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,377 @@ +# APIs - DRE Gerencial + +## Visão Geral + +O sistema possui duas APIs principais construídas com Next.js App Router, utilizando Drizzle ORM para interação com PostgreSQL. + +## Estrutura das APIs + +### 1. **API DRE Gerencial** (`/api/dre/route.ts`) + +#### Endpoint +``` +GET /api/dre +``` + +#### Descrição +Retorna dados consolidados da view `view_dre_gerencial` para construção da interface hierárquica. + +#### Implementação +```typescript +import db from '@/db'; +import { sql } from 'drizzle-orm'; +import { NextResponse } from 'next/server'; + +export async function GET() { + try { + const data = await db.execute(sql`SELECT * FROM view_dre_gerencial`); + return NextResponse.json(data.rows); + } catch (error) { + console.error('Erro ao buscar dados da view:', error); + return NextResponse.json( + { error: 'Erro ao carregar dados' }, + { status: 500 } + ); + } +} +``` + +#### Response +```typescript +interface DREItem { + codfilial: string; + data_competencia: string; + data_caixa: string; + grupo: string; + subgrupo: string; + centro_custo: string; + codigo_conta: number; + conta: string; + valor: string; +} +``` + +#### Casos de Uso +- Carregamento inicial da interface DRE +- Construção da hierarquia Grupo → Subgrupo → Centro de Custo → Conta +- Cálculo de totais e percentuais + +--- + +### 2. **API Analítica** (`/api/analitico/route.ts`) + +#### Endpoint +``` +GET /api/analitico?dataInicio=YYYY-MM&dataFim=YYYY-MM&[filtros] +``` + +#### Parâmetros + +| Parâmetro | Tipo | Obrigatório | Descrição | +|-----------|------|-------------|-----------| +| `dataInicio` | string | ✅ | Período inicial (YYYY-MM) | +| `dataFim` | string | ✅ | Período final (YYYY-MM) | +| `centroCusto` | string | ❌ | Filtro por centro de custo | +| `codigoGrupo` | string | ❌ | Filtro por código do grupo | +| `codigoSubgrupo` | string | ❌ | Filtro por código do subgrupo | +| `codigoConta` | string | ❌ | Filtro por código da conta | + +#### Implementação +```typescript +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const dataInicio = searchParams.get('dataInicio'); + const dataFim = searchParams.get('dataFim'); + const centroCusto = searchParams.get('centroCusto'); + const codigoGrupo = searchParams.get('codigoGrupo'); + const codigoSubgrupo = searchParams.get('codigoSubgrupo'); + const codigoConta = searchParams.get('codigoConta'); + + if (!dataInicio || !dataFim) { + return NextResponse.json( + { message: 'Parâmetros obrigatórios: dataInicio, dataFim' }, + { status: 400 } + ); + } + + // Construção dinâmica da query baseada nos filtros + let query; + + if (centroCusto || codigoGrupo || codigoSubgrupo || codigoConta) { + // Query com filtros específicos + query = buildFilteredQuery(dataInicio, dataFim, filtros); + } else { + // Query simples por período + query = buildSimpleQuery(dataInicio, dataFim); + } + + const data = await db.execute(query); + return NextResponse.json(data.rows); + } catch (error) { + console.error('Erro ao buscar dados analíticos:', error); + return NextResponse.json( + { + message: 'Erro ao buscar dados analíticos', + error: error instanceof Error ? error.message : String(error), + }, + { status: 500 } + ); + } +} +``` + +#### Response +```typescript +interface AnaliticoItem { + codigo_grupo: string; + codigo_subgrupo: string; + codigo_fornecedor: string; + nome_fornecedor: string; + id: number; + codfilial: string; + recnum: number; + data_competencia: string; + data_vencimento: string; + data_pagamento: string; + data_caixa: string; + codigo_conta: string; + conta: string; + codigo_centrocusto: string; + valor: number; + historico: string; + historico2: string; + created_at: string; + updated_at: string; +} +``` + +## Estratégias de Query + +### 1. **Query Simples (Sem Filtros Específicos)** +```sql +SELECT + ffa.codigo_fornecedor, + ffa.nome_fornecedor, + ffa.id, + ffa.codfilial, + ffa.recnum, + ffa.data_competencia, + ffa.data_vencimento, + ffa.data_pagamento, + ffa.data_caixa, + ffa.codigo_conta, + ffa.conta, + ffa.codigo_centrocusto, + ffa.valor, + ffa.historico, + ffa.historico2, + ffa.created_at, + ffa.updated_at +FROM fato_financeiro_analitico AS ffa +WHERE to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN $1 AND $2 +``` + +### 2. **Query com Filtros de Centro de Custo e Conta** +```sql +SELECT ffa.* +FROM fato_financeiro_analitico AS ffa +WHERE to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN $1 AND $2 + AND ffa.codigo_centrocusto = $3 + AND ffa.codigo_conta = $4 +``` + +### 3. **Query com Filtros de Grupo/Subgrupo** +```sql +SELECT ffa.* +FROM fato_financeiro_analitico AS ffa +WHERE EXISTS ( + SELECT 1 FROM public.view_dre_gerencial AS dre + WHERE ffa.codigo_conta = dre.codigo_conta::text + AND ffa.codigo_centrocusto = dre.centro_custo + AND to_char(ffa.data_competencia, 'YYYY-MM') = to_char(dre.data_competencia, 'YYYY-MM') + AND SUBSTRING(dre.grupo FROM '^\\s*(\\d+)\\s*\\.') = $1 + AND SUBSTRING(dre.subgrupo FROM '^\\s*(\\d+(?:\\.\\d+)+)\\s*-') = $2 +) +AND to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN $3 AND $4 +``` + +## Tratamento de Erros + +### 1. **Validação de Parâmetros** +```typescript +if (!dataInicio || !dataFim) { + return NextResponse.json( + { message: 'Parâmetros obrigatórios: dataInicio, dataFim' }, + { status: 400 } + ); +} +``` + +### 2. **Tratamento de Erros de Banco** +```typescript +try { + const data = await db.execute(query); + return NextResponse.json(data.rows); +} catch (error) { + console.error('Erro ao buscar dados:', error); + return NextResponse.json( + { + message: 'Erro ao buscar dados', + error: error instanceof Error ? error.message : String(error), + }, + { status: 500 } + ); +} +``` + +### 3. **Códigos de Status HTTP** + +| Status | Cenário | Response | +|--------|---------|----------| +| 200 | Sucesso | Dados solicitados | +| 400 | Parâmetros inválidos | Mensagem de erro | +| 500 | Erro interno | Detalhes do erro | + +## Performance e Otimização + +### 1. **Índices Recomendados** +```sql +-- Para filtros por data +CREATE INDEX idx_fato_financeiro_data_competencia +ON fato_financeiro_analitico (data_competencia); + +-- Para filtros por centro de custo +CREATE INDEX idx_fato_financeiro_centro_custo +ON fato_financeiro_analitico (codigo_centrocusto); + +-- Para filtros por conta +CREATE INDEX idx_fato_financeiro_conta +ON fato_financeiro_analitico (codigo_conta); +``` + +### 2. **Estratégias de Cache** +- **Client-side**: React Query para cache de dados +- **Server-side**: Cache de views materializadas +- **CDN**: Para assets estáticos + +### 3. **Paginação (Futuro)** +```typescript +interface PaginatedResponse { + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} +``` + +## Segurança + +### 1. **Validação de Input** +- Sanitização de parâmetros de query +- Validação de tipos de dados +- Escape de caracteres especiais + +### 2. **SQL Injection Prevention** +- Uso de prepared statements via Drizzle +- Parâmetros tipados +- Validação de entrada + +### 3. **Rate Limiting (Futuro)** +```typescript +// Implementação de rate limiting +const rateLimit = new Map(); + +export async function GET(request: NextRequest) { + const ip = request.ip; + const now = Date.now(); + const windowMs = 15 * 60 * 1000; // 15 minutos + const maxRequests = 100; + + // Lógica de rate limiting +} +``` + +## Monitoramento + +### 1. **Logs Estruturados** +```typescript +console.log({ + timestamp: new Date().toISOString(), + endpoint: '/api/analitico', + method: 'GET', + params: { dataInicio, dataFim, centroCusto }, + duration: Date.now() - startTime, + status: 'success' +}); +``` + +### 2. **Métricas de Performance** +- Tempo de resposta por endpoint +- Número de requisições por minuto +- Taxa de erro por endpoint +- Uso de memória e CPU + +### 3. **Health Check (Futuro)** +```typescript +// GET /api/health +export async function GET() { + try { + await db.execute(sql`SELECT 1`); + return NextResponse.json({ status: 'healthy', timestamp: new Date() }); + } catch (error) { + return NextResponse.json({ status: 'unhealthy', error: error.message }, { status: 500 }); + } +} +``` + +## Testes + +### 1. **Testes Unitários** +```typescript +// Exemplo de teste para API DRE +describe('/api/dre', () => { + it('should return DRE data', async () => { + const response = await fetch('/api/dre'); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(Array.isArray(data)).toBe(true); + }); +}); +``` + +### 2. **Testes de Integração** +```typescript +// Teste com filtros +describe('/api/analitico', () => { + it('should filter by date range', async () => { + const params = new URLSearchParams({ + dataInicio: '2024-01', + dataFim: '2024-12' + }); + + const response = await fetch(`/api/analitico?${params}`); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.every(item => + item.data_competencia.startsWith('2024') + )).toBe(true); + }); +}); +``` + +## Próximos Passos + +1. **Implementar Autenticação** JWT +2. **Adicionar Rate Limiting** por IP +3. **Implementar Cache Redis** para queries frequentes +4. **Adicionar Paginação** para grandes volumes +5. **Implementar Webhooks** para notificações +6. **Adicionar Documentação OpenAPI** (Swagger) +7. **Implementar Versionamento** de API +8. **Adicionar Monitoramento** com Prometheus/Grafana diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d6fc2ba --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,185 @@ +# Arquitetura do Sistema DRE Gerencial + +## Visão Geral da Arquitetura + +O sistema DRE Gerencial segue uma arquitetura moderna baseada em Next.js com App Router, utilizando uma abordagem de componentes React funcionais e TypeScript para type safety. + +## Padrões Arquiteturais + +### 1. **Arquitetura em Camadas** + +``` +┌─────────────────────────────────────┐ +│ Frontend Layer │ +│ (React Components + Tailwind CSS) │ +├─────────────────────────────────────┤ +│ API Layer │ +│ (Next.js API Routes) │ +├─────────────────────────────────────┤ +│ Database Layer │ +│ (PostgreSQL + Drizzle ORM) │ +└─────────────────────────────────────┘ +``` + +### 2. **Estrutura de Componentes** + +#### Componente Principal (`teste.tsx`) +- **Responsabilidade**: Orquestração da interface DRE hierárquica +- **Estado**: Gerencia expansão/colapso, ordenação, filtros analíticos +- **Padrão**: Container Component com lógica de negócio + +#### Componente Analítico (`analitico.tsx`) +- **Responsabilidade**: Visualização detalhada de transações +- **Estado**: Dados analíticos, ordenação, loading +- **Padrão**: Presentational Component com funcionalidades específicas + +### 3. **Gerenciamento de Estado** + +#### Estados Locais por Componente +```typescript +// Estados de expansão hierárquica +const [expandedGroups, setExpandedGroups] = useState>(new Set()); +const [expandedSubgrupos, setExpandedSubgrupos] = useState>(new Set()); +const [expandedCentros, setExpandedCentros] = useState>(new Set()); + +// Estados de ordenação +const [sortConfig, setSortConfig] = useState({ + field: 'descricao', + direction: 'asc', +}); + +// Estados de filtros analíticos +const [analiticoFiltros, setAnaliticoFiltros] = useState({ + dataInicio: '', + dataFim: '', + centroCusto: '', + codigoGrupo: '', + codigoSubgrupo: '', + codigoConta: '', +}); +``` + +### 4. **Padrões de Dados** + +#### Hierarquia de Dados +```typescript +interface HierarchicalRow { + type: 'grupo' | 'subgrupo' | 'centro_custo' | 'conta'; + level: number; + grupo?: string; + subgrupo?: string; + centro_custo?: string; + conta?: string; + codigo_conta?: number; + total?: number; + isExpanded?: boolean; + valoresPorMes?: Record; + percentuaisPorMes?: Record; +} +``` + +#### Transformação de Dados +- **Agregação**: Dados brutos → Hierarquia estruturada +- **Cálculos**: Valores por mês e percentuais automáticos +- **Ordenação**: Por descrição ou valor total + +## Fluxo de Dados + +### 1. **Carregamento Inicial** +``` +API /api/dre → Dados brutos → buildHierarchicalData() → Interface hierárquica +``` + +### 2. **Interação do Usuário** +``` +Clique em linha → handleRowClick() → setAnaliticoFiltros() → AnaliticoComponent +``` + +### 3. **Análise Analítica** +``` +Filtros → API /api/analitico → Dados detalhados → Tabela analítica +``` + +## Padrões de Design + +### 1. **Component Composition** +- Componentes pequenos e focados +- Props tipadas com TypeScript +- Separação de responsabilidades + +### 2. **Custom Hooks (Potencial)** +```typescript +// Exemplo de hook customizado para dados DRE +const useDREData = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchData = useCallback(async () => { + // Lógica de fetch + }, []); + + return { data, loading, fetchData }; +}; +``` + +### 3. **Error Boundaries** +- Tratamento de erros em componentes +- Estados de loading e error +- Fallbacks visuais + +## Performance + +### 1. **Otimizações Implementadas** +- `useCallback` para funções de fetch +- `useMemo` para cálculos pesados (potencial) +- Lazy loading de componentes (potencial) + +### 2. **Estratégias de Renderização** +- Renderização condicional baseada em estado +- Virtualização para listas grandes (potencial) +- Debounce para filtros (potencial) + +## Escalabilidade + +### 1. **Estrutura Modular** +- Componentes reutilizáveis em `/components/ui` +- APIs separadas por funcionalidade +- Schema de banco bem definido + +### 2. **Extensibilidade** +- Fácil adição de novos níveis hierárquicos +- Suporte a novos tipos de filtros +- Integração com outras fontes de dados + +## Segurança + +### 1. **Validação de Dados** +- TypeScript para type safety +- Validação de parâmetros nas APIs +- Sanitização de queries SQL + +### 2. **Controle de Acesso** +- Autenticação (a implementar) +- Autorização por níveis (a implementar) +- Logs de auditoria (a implementar) + +## Monitoramento + +### 1. **Logs** +- Console logs para debugging +- Error tracking (a implementar) +- Performance monitoring (a implementar) + +### 2. **Métricas** +- Tempo de carregamento de dados +- Uso de filtros +- Exportações realizadas + +## Próximos Passos Arquiteturais + +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 autenticação** e autorização +6. **Adicionar monitoramento** e analytics diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 0000000..bcdd732 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,539 @@ +# 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([]); +const [loading, setLoading] = useState(true); +const [error, setError] = useState(null); +const [expandedGroups, setExpandedGroups] = useState>(new Set()); +const [expandedSubgrupos, setExpandedSubgrupos] = useState>(new Set()); +const [expandedCentros, setExpandedCentros] = useState>(new Set()); +const [sortConfig, setSortConfig] = useState({ + 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); + + // 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 ( +
+
+

DRE Gerencial

+
+ + {/* Tabela hierárquica */} +
+ {/* Header fixo */} +
+ {/* ... header content */} +
+ + {/* Dados hierárquicos */} +
+ {hierarchicalData.map((row, index) => ( +
+ {/* ... row content */} +
+ ))} +
+
+ + {/* Componente Analítico */} + {!loading && data.length > 0 && ( + + )} +
+); +``` + +--- + +### 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([]); +const [loading, setLoading] = useState(false); +const [sortConfig, setSortConfig] = useState({ + 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, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +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 => { + const valoresPorMes: Record = {}; + + 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, + grupo: string +): Record => { + const percentuais: Record = {}; + + // 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(); + 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(); + + 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 diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..ccf2a0a --- /dev/null +++ b/docs/database.md @@ -0,0 +1,264 @@ +# Banco de Dados - DRE Gerencial + +## Visão Geral + +O sistema utiliza PostgreSQL como banco de dados principal, com Drizzle ORM para type-safe database operations. A estrutura é baseada em views materializadas para performance otimizada. + +## Configuração do Banco + +### Variáveis de Ambiente +```env +POSTGRES_DB=dre_gerencial +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=sua_senha +``` + +### Conexão (src/db/index.ts) +```typescript +import 'dotenv/config'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import * as schema from './schema'; + +const pool = new Pool({ + database: process.env.POSTGRES_DB, + host: process.env.POSTGRES_HOST, + port: Number(process.env.POSTGRES_PORT), + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, +}); + +const db = drizzle({ + client: pool, + schema, +}); +``` + +## Schema do Banco + +### View Principal (src/db/schema.ts) +```typescript +export const view = pgView('view_dre_gerencial', { + codfilial: text(), + data_competencia: date(), + data_caixa: date(), + grupo: text(), + subgrupo: text(), + centro_custo: text(), + codigo_conta: integer(), + conta: text(), + valor: numeric(), +}); +``` + +### Estrutura da View +A view `view_dre_gerencial` consolida dados de múltiplas tabelas para fornecer uma estrutura hierárquica otimizada: + +| Campo | Tipo | Descrição | +|-------|------|-----------| +| `codfilial` | text | Código da filial | +| `data_competencia` | date | Data de competência do lançamento | +| `data_caixa` | date | Data de caixa | +| `grupo` | text | Descrição do grupo (ex: "01 - RECEITAS") | +| `subgrupo` | text | Descrição do subgrupo | +| `centro_custo` | text | Centro de custo | +| `codigo_conta` | integer | Código numérico da conta | +| `conta` | text | Descrição da conta | +| `valor` | numeric | Valor do lançamento | + +## Tabelas Base (Inferidas) + +### Tabela Principal: `fato_financeiro_analitico` +Baseada na API analítica, esta tabela contém os dados detalhados: + +| Campo | Tipo | Descrição | +|-------|------|-----------| +| `id` | integer | ID único | +| `codfilial` | text | Código da filial | +| `recnum` | integer | Número do registro | +| `data_competencia` | date | Data de competência | +| `data_vencimento` | date | Data de vencimento | +| `data_pagamento` | date | Data de pagamento | +| `data_caixa` | date | Data de caixa | +| `codigo_conta` | text | Código da conta | +| `conta` | text | Descrição da conta | +| `codigo_centrocusto` | text | Código do centro de custo | +| `codigo_fornecedor` | text | Código do fornecedor | +| `nome_fornecedor` | text | Nome do fornecedor | +| `valor` | numeric | Valor do lançamento | +| `historico` | text | Histórico principal | +| `historico2` | text | Histórico secundário | +| `created_at` | timestamp | Data de criação | +| `updated_at` | timestamp | Data de atualização | + +## Queries Principais + +### 1. **Dados DRE Gerencial** +```sql +SELECT * FROM view_dre_gerencial +``` +- **Uso**: Carregamento inicial da interface hierárquica +- **Performance**: Otimizada via view materializada + +### 2. **Dados Analíticos com Filtros** +```sql +SELECT + ffa.codigo_fornecedor, + ffa.nome_fornecedor, + ffa.id, + ffa.codfilial, + ffa.recnum, + ffa.data_competencia, + ffa.data_vencimento, + ffa.data_pagamento, + ffa.data_caixa, + ffa.codigo_conta, + ffa.conta, + ffa.codigo_centrocusto, + ffa.valor, + ffa.historico, + ffa.historico2, + ffa.created_at, + ffa.updated_at +FROM fato_financeiro_analitico AS ffa +WHERE to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN $1 AND $2 + AND ffa.codigo_centrocusto = $3 -- Opcional + AND ffa.codigo_conta = $4 -- Opcional +``` + +### 3. **Query com Filtros de Grupo/Subgrupo** +```sql +SELECT ffa.* +FROM fato_financeiro_analitico AS ffa +WHERE EXISTS ( + SELECT 1 FROM public.view_dre_gerencial AS dre + WHERE ffa.codigo_conta = dre.codigo_conta::text + AND ffa.codigo_centrocusto = dre.centro_custo + AND to_char(ffa.data_competencia, 'YYYY-MM') = to_char(dre.data_competencia, 'YYYY-MM') + AND SUBSTRING(dre.grupo FROM '^\\s*(\\d+)\\s*\\.') = $1 + AND SUBSTRING(dre.subgrupo FROM '^\\s*(\\d+(?:\\.\\d+)+)\\s*-') = $2 +) +AND to_char(ffa.data_competencia, 'YYYY-MM') BETWEEN $3 AND $4 +``` + +## Índices Recomendados + +### Índices para Performance +```sql +-- Índice para filtros por data +CREATE INDEX idx_fato_financeiro_data_competencia +ON fato_financeiro_analitico (data_competencia); + +-- Índice para filtros por centro de custo +CREATE INDEX idx_fato_financeiro_centro_custo +ON fato_financeiro_analitico (codigo_centrocusto); + +-- Índice para filtros por conta +CREATE INDEX idx_fato_financeiro_conta +ON fato_financeiro_analitico (codigo_conta); + +-- Índice composto para queries analíticas +CREATE INDEX idx_fato_financeiro_analitico_composto +ON fato_financeiro_analitico (data_competencia, codigo_centrocusto, codigo_conta); +``` + +## Migrações e Versionamento + +### Drizzle Kit Configuration +```typescript +// drizzle.config.ts +export default defineConfig({ + out: './drizzle', + schema: './src/db/schema.ts', + dialect: 'postgresql', + dbCredentials: { + database: process.env.POSTGRES_DB || 'dre_gerencial', + host: process.env.POSTGRES_HOST || 'localhost', + port: Number(process.env.POSTGRES_PORT) || 5432, + user: process.env.POSTGRES_USER || 'postgres', + password: process.env.POSTGRES_PASSWORD || '', + }, +}); +``` + +### Comandos de Migração +```bash +# Gerar migração +npx drizzle-kit generate + +# Aplicar migração +npx drizzle-kit migrate + +# Visualizar schema +npx drizzle-kit studio +``` + +## Backup e Manutenção + +### Backup Automático +```bash +# Backup diário +pg_dump -h localhost -U postgres -d dre_gerencial > backup_$(date +%Y%m%d).sql + +# Restore +psql -h localhost -U postgres -d dre_gerencial < backup_20240101.sql +``` + +### Manutenção da View +```sql +-- Refresh da view materializada (se aplicável) +REFRESH MATERIALIZED VIEW view_dre_gerencial; + +-- Análise de performance +ANALYZE fato_financeiro_analitico; +ANALYZE view_dre_gerencial; +``` + +## Monitoramento + +### Queries de Monitoramento +```sql +-- Tamanho das tabelas +SELECT + schemaname, + tablename, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size +FROM pg_tables +WHERE schemaname = 'public' +ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; + +-- Queries lentas +SELECT query, mean_time, calls +FROM pg_stat_statements +ORDER BY mean_time DESC +LIMIT 10; +``` + +## Troubleshooting + +### Problemas Comuns + +1. **Conexão Recusada** + - Verificar se PostgreSQL está rodando + - Validar credenciais de conexão + - Verificar firewall/portas + +2. **Performance Lenta** + - Verificar índices existentes + - Analisar query execution plan + - Considerar particionamento por data + +3. **Dados Inconsistentes** + - Verificar refresh da view + - Validar integridade referencial + - Executar VACUUM ANALYZE + +## Próximos Passos + +1. **Implementar Cache Redis** para queries frequentes +2. **Adicionar Particionamento** por data para tabelas grandes +3. **Implementar Replicação** para alta disponibilidade +4. **Adicionar Monitoramento** de performance em tempo real +5. **Implementar Backup Automático** com retenção configurável diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..67c8900 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,580 @@ +# Deploy e Configuração - DRE Gerencial + +## Visão Geral + +Este guia cobre o processo completo de deploy e configuração do sistema DRE Gerencial em diferentes ambientes. + +## Pré-requisitos + +### 1. **Ambiente de Desenvolvimento** +- Node.js 18+ +- PostgreSQL 13+ +- npm ou yarn +- Git + +### 2. **Ambiente de Produção** +- Servidor Linux (Ubuntu 20.04+ recomendado) +- PostgreSQL 13+ +- Nginx (opcional, para proxy reverso) +- PM2 ou similar (para gerenciamento de processos) + +## Configuração do Ambiente + +### 1. **Variáveis de Ambiente** + +#### Desenvolvimento (`.env.local`) +```env +# Database +POSTGRES_DB=dre_gerencial +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=dev_password + +# Next.js +NEXT_PUBLIC_APP_URL=http://localhost:3000 +NODE_ENV=development +``` + +#### Produção (`.env.production`) +```env +# Database +POSTGRES_DB=dre_gerencial_prod +POSTGRES_HOST=prod-db-host +POSTGRES_PORT=5432 +POSTGRES_USER=prod_user +POSTGRES_PASSWORD=secure_prod_password + +# Next.js +NEXT_PUBLIC_APP_URL=https://dre-gerencial.com +NODE_ENV=production + +# Security +NEXTAUTH_SECRET=your-secret-key +NEXTAUTH_URL=https://dre-gerencial.com +``` + +### 2. **Configuração do Banco de Dados** + +#### Desenvolvimento +```bash +# Criar banco de desenvolvimento +createdb dre_gerencial + +# Conectar e verificar +psql -h localhost -U postgres -d dre_gerencial +``` + +#### Produção +```bash +# Criar usuário e banco de produção +sudo -u postgres psql +CREATE USER prod_user WITH PASSWORD 'secure_prod_password'; +CREATE DATABASE dre_gerencial_prod OWNER prod_user; +GRANT ALL PRIVILEGES ON DATABASE dre_gerencial_prod TO prod_user; +\q +``` + +## Deploy Local + +### 1. **Desenvolvimento** +```bash +# Instalar dependências +npm install + +# Executar migrações +npx drizzle-kit migrate + +# Iniciar servidor de desenvolvimento +npm run dev +``` + +### 2. **Build de Produção Local** +```bash +# Build otimizado +npm run build + +# Iniciar servidor de produção +npm run start +``` + +## Deploy em Servidor + +### 1. **Preparação do Servidor** + +#### Instalar Node.js +```bash +# Ubuntu/Debian +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Verificar instalação +node --version +npm --version +``` + +#### Instalar PostgreSQL +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install postgresql postgresql-contrib + +# Iniciar serviço +sudo systemctl start postgresql +sudo systemctl enable postgresql +``` + +#### Instalar PM2 (Gerenciador de Processos) +```bash +# Instalar PM2 globalmente +sudo npm install -g pm2 + +# Configurar PM2 para iniciar com o sistema +pm2 startup +pm2 save +``` + +### 2. **Deploy da Aplicação** + +#### Clonar Repositório +```bash +# Criar diretório da aplicação +sudo mkdir -p /var/www/dre-gerencial +sudo chown $USER:$USER /var/www/dre-gerencial + +# Clonar repositório +cd /var/www/dre-gerencial +git clone . + +# Instalar dependências +npm install --production +``` + +#### Configurar Variáveis de Ambiente +```bash +# Criar arquivo de ambiente de produção +cp .env.example .env.production + +# Editar variáveis +nano .env.production +``` + +#### Build da Aplicação +```bash +# Build para produção +npm run build + +# Verificar se build foi bem-sucedido +ls -la .next/ +``` + +#### Configurar PM2 +```bash +# Criar arquivo de configuração PM2 +cat > ecosystem.config.js << EOF +module.exports = { + apps: [{ + name: 'dre-gerencial', + script: 'npm', + args: 'start', + cwd: '/var/www/dre-gerencial', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'production', + PORT: 3000 + } + }] +}; +EOF + +# Iniciar aplicação +pm2 start ecosystem.config.js + +# Verificar status +pm2 status +pm2 logs dre-gerencial +``` + +### 3. **Configuração do Nginx (Opcional)** + +#### Instalar Nginx +```bash +sudo apt install nginx +sudo systemctl start nginx +sudo systemctl enable nginx +``` + +#### Configurar Proxy Reverso +```bash +# Criar configuração do site +sudo nano /etc/nginx/sites-available/dre-gerencial + +# Conteúdo do arquivo +server { + listen 80; + server_name dre-gerencial.com www.dre-gerencial.com; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } +} + +# Habilitar site +sudo ln -s /etc/nginx/sites-available/dre-gerencial /etc/nginx/sites-enabled/ + +# Testar configuração +sudo nginx -t + +# Recarregar Nginx +sudo systemctl reload nginx +``` + +#### Configurar SSL com Let's Encrypt +```bash +# Instalar Certbot +sudo apt install certbot python3-certbot-nginx + +# Obter certificado SSL +sudo certbot --nginx -d dre-gerencial.com -d www.dre-gerencial.com + +# Verificar renovação automática +sudo certbot renew --dry-run +``` + +## Deploy com Docker + +### 1. **Dockerfile** +```dockerfile +# Dockerfile +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN npm ci --only=production + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the application +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] +``` + +### 2. **Docker Compose** +```yaml +# docker-compose.yml +version: '3.8' + +services: + app: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - POSTGRES_DB=dre_gerencial + - POSTGRES_HOST=db + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + depends_on: + - db + restart: unless-stopped + + db: + image: postgres:13 + environment: + - POSTGRES_DB=dre_gerencial + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: unless-stopped + +volumes: + postgres_data: +``` + +### 3. **Deploy com Docker** +```bash +# Build e executar +docker-compose up -d + +# Verificar logs +docker-compose logs -f app + +# Parar serviços +docker-compose down +``` + +## Deploy Automatizado + +### 1. **GitHub Actions** + +#### Workflow de Deploy +```yaml +# .github/workflows/deploy.yml +name: Deploy to Production + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build application + run: npm run build + + - name: Deploy to server + uses: appleboy/ssh-action@v0.1.5 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSH_KEY }} + script: | + cd /var/www/dre-gerencial + git pull origin main + npm ci --production + npm run build + pm2 restart dre-gerencial +``` + +### 2. **Configuração de Secrets** +No GitHub, adicionar os seguintes secrets: +- `HOST`: IP do servidor +- `USERNAME`: usuário do servidor +- `SSH_KEY`: chave SSH privada + +## Monitoramento + +### 1. **PM2 Monitoring** +```bash +# Monitorar aplicação +pm2 monit + +# Ver logs em tempo real +pm2 logs dre-gerencial --lines 100 + +# Reiniciar aplicação +pm2 restart dre-gerencial + +# Parar aplicação +pm2 stop dre-gerencial +``` + +### 2. **Logs do Sistema** +```bash +# Logs do Nginx +sudo tail -f /var/log/nginx/access.log +sudo tail -f /var/log/nginx/error.log + +# Logs do PostgreSQL +sudo tail -f /var/log/postgresql/postgresql-13-main.log +``` + +### 3. **Monitoramento de Recursos** +```bash +# Uso de CPU e memória +htop + +# Espaço em disco +df -h + +# Status dos serviços +systemctl status nginx +systemctl status postgresql +``` + +## Backup e Restore + +### 1. **Backup do Banco de Dados** +```bash +# Backup completo +pg_dump -h localhost -U postgres -d dre_gerencial_prod > backup_$(date +%Y%m%d).sql + +# Backup apenas dados +pg_dump -h localhost -U postgres -d dre_gerencial_prod --data-only > data_backup_$(date +%Y%m%d).sql + +# Backup apenas schema +pg_dump -h localhost -U postgres -d dre_gerencial_prod --schema-only > schema_backup_$(date +%Y%m%d).sql +``` + +### 2. **Restore do Banco de Dados** +```bash +# Restore completo +psql -h localhost -U postgres -d dre_gerencial_prod < backup_20240101.sql + +# Restore apenas dados +psql -h localhost -U postgres -d dre_gerencial_prod < data_backup_20240101.sql +``` + +### 3. **Backup Automático** +```bash +# Criar script de backup +cat > /home/user/backup_dre.sh << EOF +#!/bin/bash +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/home/user/backups" +DB_NAME="dre_gerencial_prod" + +mkdir -p $BACKUP_DIR + +# Backup do banco +pg_dump -h localhost -U postgres -d $DB_NAME > $BACKUP_DIR/dre_backup_$DATE.sql + +# Manter apenas os últimos 7 backups +cd $BACKUP_DIR +ls -t dre_backup_*.sql | tail -n +8 | xargs -r rm + +echo "Backup completed: dre_backup_$DATE.sql" +EOF + +# Tornar executável +chmod +x /home/user/backup_dre.sh + +# Agendar backup diário +crontab -e +# Adicionar linha: +0 2 * * * /home/user/backup_dre.sh +``` + +## Troubleshooting + +### 1. **Problemas Comuns** + +#### Aplicação não inicia +```bash +# Verificar logs +pm2 logs dre-gerencial + +# Verificar se porta está em uso +netstat -tlnp | grep :3000 + +# Verificar variáveis de ambiente +pm2 env 0 +``` + +#### Erro de conexão com banco +```bash +# Verificar se PostgreSQL está rodando +sudo systemctl status postgresql + +# Testar conexão +psql -h localhost -U postgres -d dre_gerencial_prod + +# Verificar logs do PostgreSQL +sudo tail -f /var/log/postgresql/postgresql-13-main.log +``` + +#### Erro de permissões +```bash +# Verificar permissões do diretório +ls -la /var/www/dre-gerencial + +# Corrigir permissões +sudo chown -R $USER:$USER /var/www/dre-gerencial +chmod -R 755 /var/www/dre-gerencial +``` + +### 2. **Comandos Úteis** + +#### Reiniciar serviços +```bash +# Reiniciar aplicação +pm2 restart dre-gerencial + +# Reiniciar Nginx +sudo systemctl restart nginx + +# Reiniciar PostgreSQL +sudo systemctl restart postgresql +``` + +#### Verificar status +```bash +# Status da aplicação +pm2 status + +# Status dos serviços do sistema +sudo systemctl status nginx postgresql + +# Uso de recursos +free -h +df -h +``` + +## Próximos Passos + +1. **Implementar CI/CD** completo +2. **Adicionar monitoramento** com Prometheus/Grafana +3. **Implementar backup** automatizado +4. **Adicionar alertas** por email/Slack +5. **Implementar load balancing** para alta disponibilidade +6. **Adicionar CDN** para assets estáticos +7. **Implementar cache** com Redis +8. **Adicionar health checks** automatizados diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..f0f3375 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,529 @@ +# Guia de Desenvolvimento - DRE Gerencial + +## Configuração do Ambiente + +### 1. **Pré-requisitos** +- Node.js 18+ +- PostgreSQL 13+ +- npm ou yarn +- Git + +### 2. **Instalação** +```bash +# Clone o repositório +git clone +cd dre-modelo + +# Instale as dependências +npm install + +# Configure as variáveis de ambiente +cp .env.example .env.local +``` + +### 3. **Variáveis de Ambiente** +```env +# Database +POSTGRES_DB=dre_gerencial +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=sua_senha + +# Next.js +NEXT_PUBLIC_APP_URL=http://localhost:3000 +``` + +### 4. **Configuração do Banco** +```bash +# Criar banco de dados +createdb dre_gerencial + +# Executar migrações (se houver) +npx drizzle-kit migrate +``` + +## Scripts Disponíveis + +### 1. **Desenvolvimento** +```bash +# Iniciar servidor de desenvolvimento +npm run dev + +# Build para produção +npm run build + +# Iniciar servidor de produção +npm run start + +# Linting +npm run lint +``` + +### 2. **Banco de Dados** +```bash +# Gerar migração +npx drizzle-kit generate + +# Aplicar migração +npx drizzle-kit migrate + +# Visualizar schema +npx drizzle-kit studio +``` + +## Estrutura do Projeto + +``` +src/ +├── app/ # Next.js App Router +│ ├── api/ # API Routes +│ │ ├── analitico/ # API analítica +│ │ └── dre/ # API DRE +│ ├── DRE/ # Páginas DRE +│ │ ├── analitico.tsx # Componente analítico +│ │ ├── page.tsx # Página principal +│ │ └── teste.tsx # Componente principal +│ ├── globals.css # Estilos globais +│ └── layout.tsx # Layout raiz +├── components/ # Componentes reutilizáveis +│ └── ui/ # Componentes UI base +├── db/ # Configuração do banco +│ ├── index.ts # Conexão Drizzle +│ └── schema.ts # Schema do banco +└── lib/ # Utilitários + └── utils.ts # Funções utilitárias +``` + +## Padrões de Código + +### 1. **TypeScript** +- Sempre usar tipos explícitos +- Interfaces para props de componentes +- Tipos específicos para dados de API + +```typescript +// ✅ Bom +interface AnaliticoItem { + id: number; + valor: number; + data_competencia: string; +} + +// ❌ Evitar +const data: any = await response.json(); +``` + +### 2. **React Hooks** +- Usar `useCallback` para funções passadas como props +- Usar `useMemo` para cálculos pesados +- Evitar dependências desnecessárias em `useEffect` + +```typescript +// ✅ Bom +const fetchData = useCallback(async () => { + // lógica de fetch +}, [filtros]); + +// ❌ Evitar +const fetchData = async () => { + // lógica de fetch +}; +``` + +### 3. **Styling** +- Usar Tailwind CSS para styling +- Classes utilitárias para responsividade +- Variantes com class-variance-authority + +```typescript +// ✅ Bom +const buttonVariants = cva( + 'inline-flex items-center justify-center', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground', + outline: 'border border-input bg-background', + }, + }, + } +); + +// ❌ Evitar +const styles = { + button: 'bg-blue-500 text-white px-4 py-2', +}; +``` + +## Desenvolvimento de Novas Funcionalidades + +### 1. **Adicionando Nova API** + +#### Criar arquivo de rota +```typescript +// src/app/api/nova-funcionalidade/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import db from '@/db'; + +export async function GET(request: NextRequest) { + try { + // Lógica da API + const data = await db.execute(sql`SELECT * FROM tabela`); + + return NextResponse.json(data.rows); + } catch (error) { + console.error('Erro:', error); + return NextResponse.json( + { error: 'Erro interno' }, + { status: 500 } + ); + } +} +``` + +#### Adicionar tipos +```typescript +// src/types/nova-funcionalidade.ts +export interface NovaFuncionalidadeItem { + id: number; + nome: string; + valor: number; +} +``` + +### 2. **Adicionando Novo Componente** + +#### Estrutura do componente +```typescript +// src/components/NovaFuncionalidade.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; + +interface NovaFuncionalidadeProps { + filtros: { + dataInicio: string; + dataFim: string; + }; +} + +export default function NovaFuncionalidade({ filtros }: NovaFuncionalidadeProps) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchData = async () => { + setLoading(true); + try { + const response = await fetch(`/api/nova-funcionalidade?${new URLSearchParams(filtros)}`); + const result = await response.json(); + setData(result); + } catch (error) { + console.error('Erro:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [filtros]); + + return ( +
+

Nova Funcionalidade

+ + {loading ? ( +
Carregando...
+ ) : ( +
+ {/* Renderizar dados */} +
+ )} +
+ ); +} +``` + +### 3. **Adicionando Nova Página** + +#### Criar página +```typescript +// src/app/nova-pagina/page.tsx +import NovaFuncionalidade from '@/components/NovaFuncionalidade'; + +export default function NovaPagina() { + return ( +
+ +
+ ); +} +``` + +## Debugging + +### 1. **Logs de Desenvolvimento** +```typescript +// Usar console.log para debugging +console.log('Dados recebidos:', data); + +// Logs estruturados +console.log({ + timestamp: new Date().toISOString(), + component: 'AnaliticoComponent', + action: 'fetchData', + data: data.length +}); +``` + +### 2. **React Developer Tools** +- Instalar extensão do Chrome/Firefox +- Inspecionar estado dos componentes +- Profiler para performance + +### 3. **Network Tab** +- Verificar requisições de API +- Analisar tempo de resposta +- Verificar payloads + +## Testes + +### 1. **Configuração de Testes** +```bash +# Instalar dependências de teste +npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom + +# Configurar Jest +# jest.config.js +const nextJest = require('next/jest'); + +const createJestConfig = nextJest({ + dir: './', +}); + +const customJestConfig = { + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapping: { + '^@/(.*)$': '/src/$1', + }, + testEnvironment: 'jest-environment-jsdom', +}; + +module.exports = createJestConfig(customJestConfig); +``` + +### 2. **Testes Unitários** +```typescript +// __tests__/components/Analitico.test.tsx +import { render, screen } from '@testing-library/react'; +import AnaliticoComponent from '@/app/DRE/analitico'; + +describe('AnaliticoComponent', () => { + it('renders without crashing', () => { + const filtros = { + dataInicio: '2024-01', + dataFim: '2024-12', + }; + + render(); + expect(screen.getByText('Análise Analítica')).toBeInTheDocument(); + }); +}); +``` + +### 3. **Testes de API** +```typescript +// __tests__/api/analitico.test.ts +import { GET } from '@/app/api/analitico/route'; + +describe('/api/analitico', () => { + it('returns data for valid parameters', async () => { + const request = new Request('http://localhost:3000/api/analitico?dataInicio=2024-01&dataFim=2024-12'); + const response = await GET(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(Array.isArray(data)).toBe(true); + }); +}); +``` + +## Performance + +### 1. **Otimizações de Bundle** +```typescript +// Lazy loading de componentes +const AnaliticoComponent = lazy(() => import('./analitico')); + +// Dynamic imports +const HeavyComponent = dynamic(() => import('./HeavyComponent'), { + loading: () =>
Carregando...
, +}); +``` + +### 2. **Otimizações de Renderização** +```typescript +// Memoização de componentes +const MemoizedComponent = memo(({ data }) => { + return
{data.map(item => )}
; +}); + +// Memoização de cálculos +const expensiveValue = useMemo(() => { + return data.reduce((sum, item) => sum + item.valor, 0); +}, [data]); +``` + +### 3. **Otimizações de API** +```typescript +// Cache de dados +const { data, isLoading } = useQuery({ + queryKey: ['analitico', filtros], + queryFn: () => fetchAnaliticoData(filtros), + staleTime: 5 * 60 * 1000, // 5 minutos +}); +``` + +## Deploy + +### 1. **Build de Produção** +```bash +# Build otimizado +npm run build + +# Verificar build +npm run start +``` + +### 2. **Variáveis de Ambiente de Produção** +```env +# Produção +POSTGRES_DB=dre_gerencial_prod +POSTGRES_HOST=prod-db-host +POSTGRES_PORT=5432 +POSTGRES_USER=prod_user +POSTGRES_PASSWORD=prod_password + +NEXT_PUBLIC_APP_URL=https://dre-gerencial.com +``` + +### 3. **Docker (Opcional)** +```dockerfile +# Dockerfile +FROM node:18-alpine + +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +RUN npm run build + +EXPOSE 3000 +CMD ["npm", "start"] +``` + +## Troubleshooting + +### 1. **Problemas Comuns** + +#### Erro de Conexão com Banco +```bash +# Verificar se PostgreSQL está rodando +pg_ctl status + +# Verificar conexão +psql -h localhost -U postgres -d dre_gerencial +``` + +#### Erro de Build +```bash +# Limpar cache +rm -rf .next node_modules +npm install +npm run build +``` + +#### Erro de TypeScript +```bash +# Verificar tipos +npx tsc --noEmit + +# Atualizar tipos +npm update @types/react @types/react-dom +``` + +### 2. **Logs de Erro** +```typescript +// Error boundary +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return
Algo deu errado.
; + } + + return this.props.children; + } +} +``` + +## Contribuição + +### 1. **Fluxo de Trabalho** +```bash +# Criar branch +git checkout -b feature/nova-funcionalidade + +# Fazer commits +git add . +git commit -m "feat: adiciona nova funcionalidade" + +# Push +git push origin feature/nova-funcionalidade + +# Criar Pull Request +``` + +### 2. **Padrões de Commit** +``` +feat: nova funcionalidade +fix: correção de bug +docs: atualização de documentação +style: formatação de código +refactor: refatoração de código +test: adição de testes +chore: tarefas de manutenção +``` + +### 3. **Code Review** +- Verificar tipos TypeScript +- Testar funcionalidades +- Validar performance +- Verificar acessibilidade +- Revisar documentação + +## Próximos Passos + +1. **Implementar CI/CD** com GitHub Actions +2. **Adicionar testes E2E** com Playwright +3. **Implementar monitoramento** com Sentry +4. **Adicionar Storybook** para componentes +5. **Implementar PWA** para mobile +6. **Adicionar internacionalização** (i18n) +7. **Implementar cache** com Redis +8. **Adicionar métricas** com Analytics diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..a3b3946 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,603 @@ +# Troubleshooting - DRE Gerencial + +## Visão Geral + +Este guia contém soluções para problemas comuns encontrados durante o desenvolvimento, deploy e manutenção do sistema DRE Gerencial. + +## Problemas de Desenvolvimento + +### 1. **Erros de TypeScript** + +#### Erro: "Cannot find module '@/components/ui/button'" +```bash +# Verificar se o arquivo existe +ls -la src/components/ui/button.tsx + +# Verificar tsconfig.json +cat tsconfig.json | grep paths + +# Solução: Verificar se o path alias está correto +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +#### Erro: "Property 'valor' does not exist on type 'DREItem'" +```typescript +// Verificar interface +interface DREItem { + valor: string; // ou number + // outros campos... +} + +// Solução: Verificar se o tipo está correto na API +const valor = typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor; +``` + +### 2. **Erros de Build** + +#### Erro: "Module not found: Can't resolve 'xlsx'" +```bash +# Instalar dependência +npm install xlsx +npm install --save-dev @types/xlsx + +# Verificar se está no package.json +cat package.json | grep xlsx +``` + +#### Erro: "Failed to compile" +```bash +# Limpar cache +rm -rf .next node_modules +npm install +npm run build + +# Verificar se há erros de TypeScript +npx tsc --noEmit +``` + +### 3. **Problemas de Performance** + +#### Componente renderizando muito +```typescript +// Solução: Usar useMemo para cálculos pesados +const expensiveValue = useMemo(() => { + return data.reduce((sum, item) => sum + item.valor, 0); +}, [data]); + +// Usar useCallback para funções +const handleClick = useCallback((id: string) => { + // lógica +}, [dependencies]); +``` + +#### Lista muito grande causando lag +```typescript +// Solução: Implementar virtualização +import { FixedSizeList as List } from 'react-window'; + +const VirtualizedList = ({ items }) => ( + + {({ index, style, data }) => ( +
+ {data[index].name} +
+ )} +
+); +``` + +## Problemas de Banco de Dados + +### 1. **Erros de Conexão** + +#### Erro: "Connection refused" +```bash +# Verificar se PostgreSQL está rodando +sudo systemctl status postgresql + +# Iniciar PostgreSQL +sudo systemctl start postgresql + +# Verificar porta +netstat -tlnp | grep 5432 +``` + +#### Erro: "Authentication failed" +```bash +# Verificar usuário e senha +psql -h localhost -U postgres -d dre_gerencial + +# Verificar pg_hba.conf +sudo nano /etc/postgresql/13/main/pg_hba.conf + +# Configuração recomendada +local all postgres peer +host all all 127.0.0.1/32 md5 +``` + +### 2. **Erros de Query** + +#### Erro: "relation 'view_dre_gerencial' does not exist" +```sql +-- Verificar se a view existe +\dv view_dre_gerencial + +-- Criar view se não existir +CREATE VIEW view_dre_gerencial AS +SELECT + codfilial, + data_competencia, + data_caixa, + grupo, + subgrupo, + centro_custo, + codigo_conta, + conta, + valor +FROM fato_financeiro_analitico; +``` + +#### Erro: "column 'valor' does not exist" +```sql +-- Verificar estrutura da tabela +\d fato_financeiro_analitico + +-- Verificar se a coluna existe +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_name = 'fato_financeiro_analitico'; +``` + +### 3. **Problemas de Performance** + +#### Query muito lenta +```sql +-- Verificar plano de execução +EXPLAIN ANALYZE SELECT * FROM fato_financeiro_analitico +WHERE data_competencia BETWEEN '2024-01-01' AND '2024-12-31'; + +-- Criar índices necessários +CREATE INDEX idx_fato_financeiro_data_competencia +ON fato_financeiro_analitico (data_competencia); + +CREATE INDEX idx_fato_financeiro_centro_custo +ON fato_financeiro_analitico (codigo_centrocusto); +``` + +#### Timeout de conexão +```typescript +// Aumentar timeout no pool de conexões +const pool = new Pool({ + database: process.env.POSTGRES_DB, + host: process.env.POSTGRES_HOST, + port: Number(process.env.POSTGRES_PORT), + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + connectionTimeoutMillis: 10000, + idleTimeoutMillis: 30000, + max: 20, +}); +``` + +## Problemas de API + +### 1. **Erros 404** + +#### Rota não encontrada +```typescript +// Verificar se o arquivo de rota existe +ls -la src/app/api/analitico/route.ts + +// Verificar se a função está exportada +export async function GET(request: NextRequest) { + // implementação +} +``` + +#### Parâmetros obrigatórios não fornecidos +```typescript +// Verificar validação de parâmetros +if (!dataInicio || !dataFim) { + return NextResponse.json( + { message: 'Parâmetros obrigatórios: dataInicio, dataFim' }, + { status: 400 } + ); +} +``` + +### 2. **Erros 500** + +#### Erro interno do servidor +```typescript +// Adicionar logs detalhados +try { + const data = await db.execute(query); + return NextResponse.json(data.rows); +} catch (error) { + console.error('Erro detalhado:', { + message: error.message, + stack: error.stack, + query: query.toString(), + }); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); +} +``` + +#### Erro de memória +```bash +# Verificar uso de memória +free -h + +# Aumentar limite de memória do Node.js +export NODE_OPTIONS="--max-old-space-size=4096" +npm run dev +``` + +### 3. **Problemas de CORS** + +#### Erro: "Access to fetch at '...' from origin '...' has been blocked by CORS policy" +```typescript +// Adicionar headers CORS +export async function GET(request: NextRequest) { + const response = NextResponse.json(data); + + response.headers.set('Access-Control-Allow-Origin', '*'); + response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); + response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); + + return response; +} +``` + +## Problemas de Deploy + +### 1. **Erros de Build em Produção** + +#### Erro: "Module not found" +```bash +# Verificar se todas as dependências estão instaladas +npm ci --production + +# Verificar se não há dependências de desenvolvimento em produção +npm list --production +``` + +#### Erro: "Out of memory" +```bash +# Aumentar memória para build +export NODE_OPTIONS="--max-old-space-size=4096" +npm run build +``` + +### 2. **Problemas de PM2** + +#### Aplicação não inicia +```bash +# Verificar logs +pm2 logs dre-gerencial + +# Verificar configuração +pm2 show dre-gerencial + +# Reiniciar aplicação +pm2 restart dre-gerencial +``` + +#### Aplicação reinicia constantemente +```bash +# Verificar uso de memória +pm2 monit + +# Ajustar limite de memória +pm2 start ecosystem.config.js --max-memory-restart 1G +``` + +### 3. **Problemas de Nginx** + +#### Erro: "502 Bad Gateway" +```bash +# Verificar se a aplicação está rodando +pm2 status + +# Verificar logs do Nginx +sudo tail -f /var/log/nginx/error.log + +# Testar configuração +sudo nginx -t +``` + +#### Erro: "Connection refused" +```bash +# Verificar se a aplicação está escutando na porta correta +netstat -tlnp | grep 3000 + +# Verificar configuração do proxy +sudo nano /etc/nginx/sites-available/dre-gerencial +``` + +## Problemas de Dados + +### 1. **Dados não aparecem** + +#### Verificar se há dados no banco +```sql +-- Verificar se há dados na tabela +SELECT COUNT(*) FROM fato_financeiro_analitico; + +-- Verificar se há dados na view +SELECT COUNT(*) FROM view_dre_gerencial; + +-- Verificar dados por período +SELECT COUNT(*) FROM fato_financeiro_analitico +WHERE data_competencia BETWEEN '2024-01-01' AND '2024-12-31'; +``` + +#### Verificar filtros aplicados +```typescript +// Adicionar logs para debug +console.log('Filtros aplicados:', filtros); +console.log('Query executada:', query.toString()); +console.log('Resultado:', data.length); +``` + +### 2. **Dados incorretos** + +#### Verificar cálculos +```typescript +// Verificar se os cálculos estão corretos +const totalValor = data.reduce((sum, item) => { + const valor = typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor; + return sum + (isNaN(valor) ? 0 : valor); +}, 0); + +console.log('Total calculado:', totalValor); +``` + +#### Verificar formatação de datas +```typescript +// Verificar se as datas estão sendo formatadas corretamente +const formatDate = (dateString: string) => { + try { + return new Date(dateString).toLocaleDateString('pt-BR'); + } catch (error) { + console.error('Erro ao formatar data:', dateString, error); + return 'Data inválida'; + } +}; +``` + +### 3. **Performance de Dados** + +#### Query muito lenta +```sql +-- Verificar plano de execução +EXPLAIN ANALYZE SELECT * FROM fato_financeiro_analitico +WHERE data_competencia BETWEEN '2024-01-01' AND '2024-12-31' +AND codigo_centrocusto = '001'; + +-- Criar índices compostos +CREATE INDEX idx_fato_financeiro_composto +ON fato_financeiro_analitico (data_competencia, codigo_centrocusto); +``` + +#### Muitos dados carregando +```typescript +// Implementar paginação +const ITEMS_PER_PAGE = 100; +const [currentPage, setCurrentPage] = useState(1); + +const paginatedData = data.slice( + (currentPage - 1) * ITEMS_PER_PAGE, + currentPage * ITEMS_PER_PAGE +); +``` + +## Problemas de Interface + +### 1. **Componentes não renderizam** + +#### Verificar se o componente está sendo importado corretamente +```typescript +// Verificar import +import AnaliticoComponent from './analitico'; + +// Verificar se está sendo usado +{!loading && data.length > 0 && ( + +)} +``` + +#### Verificar se há erros no console +```typescript +// Adicionar error boundary +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.error('Erro no componente:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return
Erro ao carregar componente
; + } + + return this.props.children; + } +} +``` + +### 2. **Problemas de Styling** + +#### Classes Tailwind não aplicadas +```bash +# Verificar se Tailwind está configurado +cat tailwind.config.js + +# Verificar se as classes estão sendo incluídas +npm run build +``` + +#### Componentes não responsivos +```typescript +// Verificar classes responsivas +
+ {/* conteúdo */} +
+``` + +### 3. **Problemas de Interação** + +#### Cliques não funcionam +```typescript +// Verificar se o evento está sendo capturado +const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + console.log('Clique capturado'); + // lógica +}; + +// Verificar se o elemento está clicável + +``` + +#### Estados não atualizam +```typescript +// Verificar se o estado está sendo atualizado corretamente +const [data, setData] = useState([]); + +const updateData = (newData) => { + console.log('Atualizando dados:', newData); + setData(newData); +}; + +// Verificar se o useEffect está sendo executado +useEffect(() => { + console.log('useEffect executado'); + fetchData(); +}, [dependencies]); +``` + +## Comandos Úteis para Debug + +### 1. **Verificar Status do Sistema** +```bash +# Status dos serviços +sudo systemctl status nginx postgresql + +# Uso de recursos +htop +free -h +df -h + +# Logs do sistema +sudo journalctl -f +``` + +### 2. **Verificar Aplicação** +```bash +# Status da aplicação +pm2 status +pm2 logs dre-gerencial + +# Verificar processos +ps aux | grep node + +# Verificar portas +netstat -tlnp | grep :3000 +``` + +### 3. **Verificar Banco de Dados** +```bash +# Conectar ao banco +psql -h localhost -U postgres -d dre_gerencial + +# Verificar conexões ativas +SELECT * FROM pg_stat_activity; + +# Verificar tamanho do banco +SELECT pg_size_pretty(pg_database_size('dre_gerencial')); +``` + +## Logs e Monitoramento + +### 1. **Configurar Logs Detalhados** +```typescript +// Adicionar logs estruturados +const logger = { + info: (message: string, data?: any) => { + console.log({ + timestamp: new Date().toISOString(), + level: 'INFO', + message, + data, + }); + }, + error: (message: string, error?: any) => { + console.error({ + timestamp: new Date().toISOString(), + level: 'ERROR', + message, + error: error?.message || error, + stack: error?.stack, + }); + }, +}; +``` + +### 2. **Monitoramento de Performance** +```typescript +// Adicionar métricas de performance +const performanceMonitor = { + start: (label: string) => { + performance.mark(`${label}-start`); + }, + end: (label: string) => { + performance.mark(`${label}-end`); + performance.measure(label, `${label}-start`, `${label}-end`); + + const measure = performance.getEntriesByName(label)[0]; + console.log(`${label}: ${measure.duration}ms`); + }, +}; +``` + +## Próximos Passos + +1. **Implementar sistema de logs** centralizado +2. **Adicionar monitoramento** em tempo real +3. **Implementar alertas** automáticos +4. **Criar dashboard** de saúde do sistema +5. **Implementar backup** automatizado +6. **Adicionar testes** de carga +7. **Implementar rollback** automático +8. **Criar documentação** de runbooks diff --git a/package-lock.json b/package-lock.json index 786348b..5d3a6f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "dre-gerencial", "version": "0.1.0", "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^17.2.3", @@ -1113,6 +1117,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1861,6 +1903,67 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1876,6 +1979,303 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1894,6 +2294,171 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2193,6 +2758,66 @@ "tailwindcss": "4.1.14" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2261,7 +2886,7 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2903,6 +3528,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -3513,6 +4150,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4627,6 +5270,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6446,6 +7098,75 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -7435,6 +8156,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index db51c58..aec9ed6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^17.2.3", diff --git a/src/app/DRE/analitico.tsx b/src/app/DRE/analitico.tsx index 2b0a841..157458c 100644 --- a/src/app/DRE/analitico.tsx +++ b/src/app/DRE/analitico.tsx @@ -1,8 +1,32 @@ 'use client'; -import { Button } from '@/components/ui/button'; -import { ArrowDown, ArrowUp, ArrowUpDown, Download } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; +import * as React from "react"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + flexRender, +} from "@tanstack/react-table"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@/components/ui/select"; +import { ChevronUp, ChevronDown, Download } from "lucide-react"; import * as XLSX from 'xlsx'; interface AnaliticoItem { @@ -27,26 +51,6 @@ interface AnaliticoItem { updated_at: string; } -type SortField = - | 'data_competencia' - | 'data_vencimento' - | 'data_caixa' - | 'codigo_fornecedor' - | 'nome_fornecedor' - | 'codigo_centrocusto' - | 'codigo_conta' - | 'conta' - | 'valor' - | 'historico' - | 'historico2' - | 'recnum'; -type SortDirection = 'asc' | 'desc'; - -interface SortConfig { - field: SortField; - direction: SortDirection; -} - interface AnaliticoProps { filtros: { dataInicio: string; @@ -59,14 +63,15 @@ interface AnaliticoProps { } export default function AnaliticoComponent({ filtros }: AnaliticoProps) { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(false); - const [sortConfig, setSortConfig] = useState({ - field: 'data_competencia', - direction: 'desc', - }); + const [data, setData] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [globalFilter, setGlobalFilter] = React.useState(""); + const [columnFilters, setColumnFilters] = React.useState([]); + const [open, setOpen] = React.useState(false); + const [conditions, setConditions] = React.useState([{ column: "", operator: "contains", value: "" }]); + const [isScrolled, setIsScrolled] = React.useState(false); - const fetchData = useCallback(async () => { + const fetchData = React.useCallback(async () => { // Só faz a requisição se tiver dataInicio e dataFim if (!filtros.dataInicio || !filtros.dataFim) { setData([]); @@ -100,61 +105,139 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) { } }, [filtros]); - useEffect(() => { + React.useEffect(() => { fetchData(); }, [fetchData]); - const handleSort = (field: SortField) => { - setSortConfig((prev) => ({ - field, - direction: - prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc', - })); - }; + const columns = React.useMemo( + () => [ + { + accessorKey: "data_competencia", + header: "Data Comp.", + cell: ({ getValue }: any) => { + const value = getValue(); + return new Date(value).toLocaleDateString('pt-BR'); + } + }, + { + accessorKey: "data_vencimento", + header: "Data Venc.", + cell: ({ getValue }: any) => { + const value = getValue(); + return new Date(value).toLocaleDateString('pt-BR'); + } + }, + { + accessorKey: "data_caixa", + header: "Data Caixa", + cell: ({ getValue }: any) => { + const value = getValue(); + return new Date(value).toLocaleDateString('pt-BR'); + } + }, + { accessorKey: "codigo_fornecedor", header: "Cód. Fornec." }, + { accessorKey: "nome_fornecedor", header: "Fornecedor" }, + { accessorKey: "codigo_centrocusto", header: "Cód. Centro" }, + { accessorKey: "codigo_conta", header: "Cód. Conta" }, + { accessorKey: "conta", header: "Conta" }, + { + accessorKey: "valor", + header: "Valor", + cell: ({ getValue }: any) => { + const value = getValue(); + const formatted = new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(value); + const isNegative = value < 0; + return ( + + {formatted} + + ); + } + }, + { accessorKey: "historico", header: "Histórico" }, + { accessorKey: "historico2", header: "Histórico 2" }, + { accessorKey: "recnum", header: "Recnum" }, + ], + [] + ); - const getSortIcon = (field: SortField) => { - if (sortConfig.field !== field) { - return ; - } - return sortConfig.direction === 'asc' ? ( - - ) : ( - - ); - }; + const filterFns = React.useMemo( + () => ({ + advancedText: (row: any, columnId: string, filter: any) => { + if (!filter) return true; + const raw = row.getValue(columnId); + const v = raw == null ? "" : String(raw); + const op = filter.operator; + const q = (filter.value ?? "").toString(); + const a = v.toLowerCase(); + const b = q.toLowerCase(); + switch (op) { + case "contains": + return a.includes(b); + case "equals": + return a === b; + case "startsWith": + return a.startsWith(b); + case "endsWith": + return a.endsWith(b); + case "empty": + return a.length === 0; + case "notEmpty": + return a.length > 0; + default: + return true; + } + }, + }), + [] + ); - const sortedData = [...data].sort((a, b) => { - const aValue = a[sortConfig.field]; - const bValue = b[sortConfig.field]; - - if (typeof aValue === 'string' && typeof bValue === 'string') { - return sortConfig.direction === 'asc' - ? aValue.localeCompare(bValue) - : bValue.localeCompare(aValue); - } - - if (typeof aValue === 'number' && typeof bValue === 'number') { - return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue; - } - - return 0; + const table = useReactTable({ + data, + columns, + state: { globalFilter, columnFilters }, + onGlobalFilterChange: setGlobalFilter, + onColumnFiltersChange: setColumnFilters, + filterFns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), }); - const formatCurrency = (value: number) => { - return new Intl.NumberFormat('pt-BR', { - style: 'currency', - currency: 'BRL', - }).format(value); + const parentRef = React.useRef(null); + const rowVirtualizer = useVirtualizer({ + count: table.getRowModel().rows.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 36, + overscan: 20, + }); + + const virtualRows = rowVirtualizer.getVirtualItems(); + + React.useEffect(() => { + const handleScroll = () => { + if (!parentRef.current) return; + setIsScrolled(parentRef.current.scrollTop > 0); + }; + const el = parentRef.current; + el?.addEventListener("scroll", handleScroll); + return () => el?.removeEventListener("scroll", handleScroll); + }, []); + + const applyFilters = () => { + const filters = conditions + .filter((c) => c.column && (c.operator === "empty" || c.operator === "notEmpty" || (c.value ?? "") !== "")) + .map((c) => ({ id: c.column, value: { operator: c.operator, value: c.value } })); + setColumnFilters(filters); + setOpen(false); }; - 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'); + const clearFilters = () => { + setConditions([{ column: "", operator: "contains", value: "" }]); + setColumnFilters([]); }; const totalValor = data.reduce((sum, item) => { @@ -213,255 +296,319 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) { }; return ( -
-
-

Análise Analítica

- {data.length > 0 && ( - - )} -
- - {/* Filtros aplicados */} - {/*
-
- Filtros aplicados: -
- {filtros.centroCusto && ( - - Centro: {filtros.centroCusto} - - )} - {filtros.codigoGrupo && ( - - Grupo: {filtros.codigoGrupo} - - )} - {filtros.codigoSubgrupo && ( - - Subgrupo: {filtros.codigoSubgrupo} - - )} - {filtros.codigoConta && ( - - Conta: {filtros.codigoConta} - + + +
+
+
+ + + +
+
+

Análise Analítica

+

Relatório detalhado de transações

+
+
+
+ ) => setGlobalFilter(e.target.value)} + className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500" + /> + + {data.length > 0 && ( + )}
-
*/} - {/* Resumo */} - - {/* Tabela */} -
- {/* Header fixo */} -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
Histórico
-
Histórico 2
-
Recnum
-
-
- -
- {loading ? ( -
- Carregando dados analíticos... -
- ) : sortedData.length === 0 ? ( -
- Nenhum dado analítico encontrado para os filtros aplicados. -
- ) : ( - sortedData.map((row, index) => ( -
-
- {formatDate(row.data_competencia)} -
-
- {formatDate(row.data_vencimento)} -
-
- {formatDate(row.data_caixa)} -
-
- {row.codigo_fornecedor || '-'} -
-
- {row.nome_fornecedor || '-'} -
-
- {row.codigo_centrocusto || '-'} -
-
- {row.codigo_conta || '-'} -
-
- {row.conta || '-'} -
-
- {(() => { - const valor = - typeof row.valor === 'string' - ? parseFloat(row.valor) - : row.valor; - const { formatted, isNegative } = - formatCurrencyWithColor(valor); +
+ + + {table.getHeaderGroups().map((hg: any) => ( + + {hg.headers.map((header: any) => { + const sorted = header.column.getIsSorted(); return ( - - {formatted} - +
+ + {flexRender(header.column.columnDef.header, header.getContext())} + +
+ {sorted === "asc" ? ( + + ) : sorted === "desc" ? ( + + ) : ( +
+ + +
+ )} +
+
+ ); - })()} + })} + + ))} + +
+ +
+ + + {loading ? ( + + + + ) : virtualRows.length === 0 ? ( + + + + ) : ( + virtualRows.map((virtualRow: any) => { + const row = table.getRowModel().rows[virtualRow.index]; + return ( + + {row.getVisibleCells().map((cell: any, cellIndex: number) => ( + + ))} + + ); + }) + )} + +
+
+
+ Carregando dados analíticos... +
+
+
+
+ + + +
+ Nenhum dado analítico encontrado para os filtros aplicados. +
+
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+
+ + {data.length > 0 && ( +
+
+
+
+ + +
-
- {row.historico || '-'} -
-
- {row.historico2 || '-'} -
-
- {row.recnum || '-'} +
+

+ Total de Registros: {table.getRowModel().rows.length} +

+

Transações encontradas

- )) - )} -
-
- {data.length > 0 && ( -
-
-
-

- Total de Registros: {data.length} -

-
-
-

- {(() => { - const { formatted, isNegative } = - formatCurrencyWithColor(totalValor); - return ( - - Valor Total: {formatted} - - ); - })()} -

+
+

+ + Valor Total: {new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(totalValor)} + +

+

Soma de todos os valores

+
-
- )} -
+ )} + + + + + Filtros Avançados + + +
+ {conditions.map((cond, idx) => ( +
+
+ + +
+ +
+ + +
+ + {!(cond.operator === "empty" || cond.operator === "notEmpty") && ( +
+ + ) => { + const next = [...conditions]; + next[idx].value = e.target.value; + setConditions(next); + }} + placeholder="Digite o valor" + className="w-full bg-white border-gray-300" + /> +
+ )} + + {conditions.length > 1 && ( +
+ +
+ )} +
+ ))} + +
+ +
+
+ + + + + +
+
+ + ); -} +} \ No newline at end of file diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..794c886 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +export { Card, CardContent } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..230cd30 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,119 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..d63bae1 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..2e2b024 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,157 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}