fix: adicionados filtros e estilização na tabela de analítico
This commit is contained in:
parent
d7ab072622
commit
7758ad5abe
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)*
|
||||||
|
|
@ -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<T> {
|
||||||
|
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
|
||||||
|
|
@ -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<Set<string>>(new Set());
|
||||||
|
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Estados de ordenação
|
||||||
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
|
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<string, number>;
|
||||||
|
percentuaisPorMes?: Record<string, number>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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<DREItem[]>([]);
|
||||||
|
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
|
||||||
|
|
@ -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<DREItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(new Set());
|
||||||
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
|
field: 'descricao',
|
||||||
|
direction: 'asc',
|
||||||
|
});
|
||||||
|
const [analiticoFiltros, setAnaliticoFiltros] = useState({
|
||||||
|
dataInicio: '',
|
||||||
|
dataFim: '',
|
||||||
|
centroCusto: '',
|
||||||
|
codigoGrupo: '',
|
||||||
|
codigoSubgrupo: '',
|
||||||
|
codigoConta: '',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Funções Principais
|
||||||
|
|
||||||
|
##### `fetchData()`
|
||||||
|
```typescript
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await fetch('/api/dre');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erro ao carregar dados: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
setData(result);
|
||||||
|
|
||||||
|
// Extrair meses únicos dos dados
|
||||||
|
const meses = [...new Set(
|
||||||
|
result.map((item: DREItem) => {
|
||||||
|
const dataCompetencia = new Date(item.data_competencia);
|
||||||
|
return `${dataCompetencia.getFullYear()}-${String(
|
||||||
|
dataCompetencia.getMonth() + 1
|
||||||
|
).padStart(2, '0')}`;
|
||||||
|
})
|
||||||
|
)].sort() as string[];
|
||||||
|
|
||||||
|
setMesesDisponiveis(meses);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erro desconhecido');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `buildHierarchicalData()`
|
||||||
|
```typescript
|
||||||
|
const buildHierarchicalData = (): HierarchicalRow[] => {
|
||||||
|
const rows: HierarchicalRow[] = [];
|
||||||
|
|
||||||
|
// Agrupar por grupo, tratando grupo 05 como subgrupo do grupo 04
|
||||||
|
const grupos = data.reduce((acc, item) => {
|
||||||
|
if (item.grupo.includes('05')) {
|
||||||
|
// Lógica especial para grupo 05
|
||||||
|
const grupo04Key = Object.keys(acc).find((key) => key.includes('04'));
|
||||||
|
if (grupo04Key) {
|
||||||
|
acc[grupo04Key].push(item);
|
||||||
|
} else {
|
||||||
|
const grupo04Nome = '04 - GRUPO 04';
|
||||||
|
if (!acc[grupo04Nome]) {
|
||||||
|
acc[grupo04Nome] = [];
|
||||||
|
}
|
||||||
|
acc[grupo04Nome].push(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!acc[item.grupo]) {
|
||||||
|
acc[item.grupo] = [];
|
||||||
|
}
|
||||||
|
acc[item.grupo].push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
|
// Construir hierarquia completa
|
||||||
|
// ... lógica de construção hierárquica
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `handleRowClick()`
|
||||||
|
```typescript
|
||||||
|
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
|
||||||
|
if (!data.length) return;
|
||||||
|
|
||||||
|
// Calcular período baseado nos dados
|
||||||
|
const datas = data.map((item) => item.data_competencia);
|
||||||
|
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
|
||||||
|
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
|
||||||
|
|
||||||
|
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7);
|
||||||
|
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7);
|
||||||
|
|
||||||
|
const { codigoGrupo, codigoSubgrupo } = extractCodes(
|
||||||
|
row.grupo || '',
|
||||||
|
row.subgrupo
|
||||||
|
);
|
||||||
|
|
||||||
|
// Criar identificador único para a linha
|
||||||
|
const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
|
||||||
|
row.centro_custo || ''
|
||||||
|
}-${row.codigo_conta || ''}`;
|
||||||
|
setLinhaSelecionada(linhaId);
|
||||||
|
|
||||||
|
// Configurar filtros para análise analítica
|
||||||
|
const dataInicioFiltro = mesSelecionado || dataInicioStr;
|
||||||
|
const dataFimFiltro = mesSelecionado || dataFimStr;
|
||||||
|
|
||||||
|
setAnaliticoFiltros({
|
||||||
|
dataInicio: dataInicioFiltro,
|
||||||
|
dataFim: dataFimFiltro,
|
||||||
|
centroCusto: row.centro_custo || '',
|
||||||
|
codigoGrupo,
|
||||||
|
codigoSubgrupo,
|
||||||
|
codigoConta: row.codigo_conta?.toString() || '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Renderização
|
||||||
|
```typescript
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col items-center gap-2">
|
||||||
|
<div className="mb-1">
|
||||||
|
<h1 className="text-lg font-bold">DRE Gerencial</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabela hierárquica */}
|
||||||
|
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative">
|
||||||
|
{/* Header fixo */}
|
||||||
|
<div className="sticky top-0 z-30 border-b shadow-sm">
|
||||||
|
{/* ... header content */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dados hierárquicos */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{hierarchicalData.map((row, index) => (
|
||||||
|
<div key={index} className={`flex ${getRowStyle(row)}`}>
|
||||||
|
{/* ... row content */}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Componente Analítico */}
|
||||||
|
{!loading && data.length > 0 && (
|
||||||
|
<AnaliticoComponent filtros={analiticoFiltros} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Componente Analítico** (`src/app/DRE/analitico.tsx`)
|
||||||
|
|
||||||
|
#### Responsabilidades
|
||||||
|
- Visualização detalhada de transações
|
||||||
|
- Ordenação de dados analíticos
|
||||||
|
- Exportação para Excel
|
||||||
|
- Aplicação de filtros dinâmicos
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
```typescript
|
||||||
|
interface AnaliticoProps {
|
||||||
|
filtros: {
|
||||||
|
dataInicio: string;
|
||||||
|
dataFim: string;
|
||||||
|
centroCusto?: string;
|
||||||
|
codigoGrupo?: string;
|
||||||
|
codigoSubgrupo?: string;
|
||||||
|
codigoConta?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Estados
|
||||||
|
```typescript
|
||||||
|
const [data, setData] = useState<AnaliticoItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
|
field: 'data_competencia',
|
||||||
|
direction: 'desc',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Funções Principais
|
||||||
|
|
||||||
|
##### `fetchData()`
|
||||||
|
```typescript
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if (!filtros.dataInicio || !filtros.dataFim) {
|
||||||
|
setData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
dataInicio: filtros.dataInicio,
|
||||||
|
dataFim: filtros.dataFim,
|
||||||
|
...(filtros.centroCusto && { centroCusto: filtros.centroCusto }),
|
||||||
|
...(filtros.codigoGrupo && { codigoGrupo: filtros.codigoGrupo }),
|
||||||
|
...(filtros.codigoSubgrupo && { codigoSubgrupo: filtros.codigoSubgrupo }),
|
||||||
|
...(filtros.codigoConta && { codigoConta: filtros.codigoConta }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`/api/analitico?${params}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
setData(result as AnaliticoItem[]);
|
||||||
|
} else {
|
||||||
|
console.error('Erro ao buscar dados:', await response.text());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar dados:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filtros]);
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `exportToExcel()`
|
||||||
|
```typescript
|
||||||
|
const exportToExcel = () => {
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
// Preparar dados para exportação
|
||||||
|
const exportData = data.map((item) => ({
|
||||||
|
'Data Competência': new Date(item.data_competencia).toLocaleDateString('pt-BR'),
|
||||||
|
'Data Vencimento': new Date(item.data_vencimento).toLocaleDateString('pt-BR'),
|
||||||
|
'Data Caixa': new Date(item.data_caixa).toLocaleDateString('pt-BR'),
|
||||||
|
'Código Fornecedor': item.codigo_fornecedor,
|
||||||
|
Fornecedor: item.nome_fornecedor,
|
||||||
|
'Código Centro Custo': item.codigo_centrocusto,
|
||||||
|
'Centro Custo': item.codigo_centrocusto,
|
||||||
|
'Código Conta': item.codigo_conta,
|
||||||
|
Conta: item.conta,
|
||||||
|
Valor: typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor,
|
||||||
|
Histórico: item.historico,
|
||||||
|
'Histórico 2': item.historico2,
|
||||||
|
Recnum: item.recnum,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Criar workbook
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||||
|
|
||||||
|
// Adicionar resumo na segunda aba
|
||||||
|
const resumoData = [
|
||||||
|
{ Métrica: 'Total de Registros', Valor: data.length },
|
||||||
|
{ Métrica: 'Valor Total', Valor: totalValor },
|
||||||
|
];
|
||||||
|
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
|
||||||
|
|
||||||
|
// Adicionar abas ao workbook
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, 'Dados Analíticos');
|
||||||
|
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
|
||||||
|
|
||||||
|
// Gerar nome do arquivo com timestamp
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
|
||||||
|
const fileName = `analitico_${timestamp}.xlsx`;
|
||||||
|
|
||||||
|
// Fazer download
|
||||||
|
XLSX.writeFile(wb, fileName);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Componentes UI** (`src/components/ui/`)
|
||||||
|
|
||||||
|
#### Button Component
|
||||||
|
```typescript
|
||||||
|
// src/components/ui/button.tsx
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
|
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-10 px-4 py-2',
|
||||||
|
sm: 'h-9 rounded-md px-3',
|
||||||
|
lg: 'h-11 rounded-md px-8',
|
||||||
|
icon: 'h-10 w-10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
```
|
||||||
|
|
||||||
|
## Padrões de Design
|
||||||
|
|
||||||
|
### 1. **Composition Pattern**
|
||||||
|
- Componentes pequenos e focados
|
||||||
|
- Props tipadas com TypeScript
|
||||||
|
- Reutilização através de composition
|
||||||
|
|
||||||
|
### 2. **State Management**
|
||||||
|
- Estados locais com `useState`
|
||||||
|
- Callbacks com `useCallback` para performance
|
||||||
|
- Effects com `useEffect` para side effects
|
||||||
|
|
||||||
|
### 3. **Styling**
|
||||||
|
- Tailwind CSS para styling
|
||||||
|
- Class variance authority para variantes
|
||||||
|
- Responsive design mobile-first
|
||||||
|
|
||||||
|
### 4. **Type Safety**
|
||||||
|
- Interfaces TypeScript para props
|
||||||
|
- Tipos específicos para dados
|
||||||
|
- Validação de tipos em runtime
|
||||||
|
|
||||||
|
## Utilitários
|
||||||
|
|
||||||
|
### 1. **Formatação**
|
||||||
|
```typescript
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrencyWithColor = (value: number) => {
|
||||||
|
const formatted = formatCurrency(value);
|
||||||
|
const isNegative = value < 0;
|
||||||
|
return { formatted, isNegative };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Extração de Códigos**
|
||||||
|
```typescript
|
||||||
|
const extractCodes = (grupo: string, subgrupo?: string) => {
|
||||||
|
const grupoMatch = grupo.match(/^(\d+)/);
|
||||||
|
const codigoGrupo = grupoMatch ? grupoMatch[1] : '';
|
||||||
|
|
||||||
|
let codigoSubgrupo = '';
|
||||||
|
if (subgrupo) {
|
||||||
|
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
|
||||||
|
if (subgrupoMatch) {
|
||||||
|
codigoSubgrupo = subgrupoMatch[1];
|
||||||
|
} else {
|
||||||
|
codigoSubgrupo = subgrupo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { codigoGrupo, codigoSubgrupo };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Cálculos**
|
||||||
|
```typescript
|
||||||
|
const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
|
||||||
|
const valoresPorMes: Record<string, number> = {};
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const dataCompetencia = new Date(item.data_competencia);
|
||||||
|
const anoMes = `${dataCompetencia.getFullYear()}-${String(
|
||||||
|
dataCompetencia.getMonth() + 1
|
||||||
|
).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
if (!valoresPorMes[anoMes]) {
|
||||||
|
valoresPorMes[anoMes] = 0;
|
||||||
|
}
|
||||||
|
valoresPorMes[anoMes] += parseFloat(item.valor);
|
||||||
|
});
|
||||||
|
|
||||||
|
return valoresPorMes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcularPercentuaisPorMes = (
|
||||||
|
valoresPorMes: Record<string, number>,
|
||||||
|
grupo: string
|
||||||
|
): Record<string, number> => {
|
||||||
|
const percentuais: Record<string, number> = {};
|
||||||
|
|
||||||
|
// Se for o grupo 03, retorna 100% para todos os meses
|
||||||
|
if (grupo.includes('03')) {
|
||||||
|
Object.keys(valoresPorMes).forEach((mes) => {
|
||||||
|
percentuais[mes] = 100;
|
||||||
|
});
|
||||||
|
return percentuais;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para outros grupos, calcular percentual baseado no grupo 03
|
||||||
|
Object.keys(valoresPorMes).forEach((mes) => {
|
||||||
|
const valorAtual = valoresPorMes[mes];
|
||||||
|
|
||||||
|
// Encontrar o valor do grupo 03 para o mesmo mês
|
||||||
|
const grupo03Items = data.filter((item) => {
|
||||||
|
const dataCompetencia = new Date(item.data_competencia);
|
||||||
|
const anoMes = `${dataCompetencia.getFullYear()}-${String(
|
||||||
|
dataCompetencia.getMonth() + 1
|
||||||
|
).padStart(2, '0')}`;
|
||||||
|
return anoMes === mes && item.grupo.includes('03');
|
||||||
|
});
|
||||||
|
|
||||||
|
const valorGrupo03 = grupo03Items.reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (valorGrupo03 !== 0) {
|
||||||
|
percentuais[mes] = (valorAtual / valorGrupo03) * 100;
|
||||||
|
} else {
|
||||||
|
percentuais[mes] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return percentuais;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### 1. **Otimizações Implementadas**
|
||||||
|
- `useCallback` para funções de fetch
|
||||||
|
- `useMemo` para cálculos pesados (potencial)
|
||||||
|
- Renderização condicional
|
||||||
|
|
||||||
|
### 2. **Estratégias de Renderização**
|
||||||
|
- Lazy loading de componentes
|
||||||
|
- Virtualização para listas grandes (potencial)
|
||||||
|
- Debounce para filtros (potencial)
|
||||||
|
|
||||||
|
## Testes
|
||||||
|
|
||||||
|
### 1. **Testes Unitários**
|
||||||
|
```typescript
|
||||||
|
// Exemplo de teste para componente
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import Teste from './teste';
|
||||||
|
|
||||||
|
describe('Teste Component', () => {
|
||||||
|
it('renders DRE title', () => {
|
||||||
|
render(<Teste />);
|
||||||
|
expect(screen.getByText('DRE Gerencial')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Testes de Integração**
|
||||||
|
```typescript
|
||||||
|
// Teste de interação com API
|
||||||
|
describe('DRE Integration', () => {
|
||||||
|
it('loads data from API', async () => {
|
||||||
|
render(<Teste />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Carregando dados...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verificar se dados foram carregados
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Próximos Passos
|
||||||
|
|
||||||
|
1. **Implementar Context API** para estado global
|
||||||
|
2. **Adicionar React Query** para cache de dados
|
||||||
|
3. **Implementar Error Boundaries** robustos
|
||||||
|
4. **Adicionar testes unitários** e de integração
|
||||||
|
5. **Implementar lazy loading** de componentes
|
||||||
|
6. **Adicionar acessibilidade** (ARIA labels)
|
||||||
|
7. **Implementar temas** dark/light
|
||||||
|
8. **Adicionar animações** e transições
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 <repository-url> .
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
@ -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 <repository-url>
|
||||||
|
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 (
|
||||||
|
<div className="w-full">
|
||||||
|
<h2 className="text-lg font-bold mb-4">Nova Funcionalidade</h2>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div>Carregando...</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{/* Renderizar dados */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (
|
||||||
|
<div className="w-full min-h-screen p-4">
|
||||||
|
<NovaFuncionalidade filtros={{ dataInicio: '', dataFim: '' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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: ['<rootDir>/jest.setup.js'],
|
||||||
|
moduleNameMapping: {
|
||||||
|
'^@/(.*)$': '<rootDir>/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(<AnaliticoComponent filtros={filtros} />);
|
||||||
|
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: () => <div>Carregando...</div>,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Otimizações de Renderização**
|
||||||
|
```typescript
|
||||||
|
// Memoização de componentes
|
||||||
|
const MemoizedComponent = memo(({ data }) => {
|
||||||
|
return <div>{data.map(item => <Item key={item.id} data={item} />)}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 <div>Algo deu errado.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -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 }) => (
|
||||||
|
<List
|
||||||
|
height={400}
|
||||||
|
itemCount={items.length}
|
||||||
|
itemSize={50}
|
||||||
|
itemData={items}
|
||||||
|
>
|
||||||
|
{({ index, style, data }) => (
|
||||||
|
<div style={style}>
|
||||||
|
{data[index].name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 && (
|
||||||
|
<AnaliticoComponent filtros={analiticoFiltros} />
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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 <div>Erro ao carregar componente</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
<div className="w-full md:w-1/2 lg:w-1/3">
|
||||||
|
{/* conteúdo */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
className="cursor-pointer hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
Clique aqui
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
@ -8,7 +8,11 @@
|
||||||
"name": "dre-gerencial",
|
"name": "dre-gerencial",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|
@ -1113,6 +1117,44 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
@ -1861,6 +1903,67 @@
|
||||||
"node": ">=12.4.0"
|
"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": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
"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": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
|
|
@ -2193,6 +2758,66 @@
|
||||||
"tailwindcss": "4.1.14"
|
"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": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
|
|
@ -2261,7 +2886,7 @@
|
||||||
"version": "19.2.1",
|
"version": "19.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
|
||||||
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
|
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
|
|
@ -2903,6 +3528,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"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": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||||
|
|
@ -3513,6 +4150,12 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
|
|
@ -4627,6 +5270,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/get-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
|
@ -6446,6 +7098,75 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
|
|
@ -7435,6 +8156,49 @@
|
||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,32 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import * as React from "react";
|
||||||
import { ArrowDown, ArrowUp, ArrowUpDown, Download } from 'lucide-react';
|
import {
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
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';
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
interface AnaliticoItem {
|
interface AnaliticoItem {
|
||||||
|
|
@ -27,26 +51,6 @@ interface AnaliticoItem {
|
||||||
updated_at: string;
|
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 {
|
interface AnaliticoProps {
|
||||||
filtros: {
|
filtros: {
|
||||||
dataInicio: string;
|
dataInicio: string;
|
||||||
|
|
@ -59,14 +63,15 @@ interface AnaliticoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
||||||
const [data, setData] = useState<AnaliticoItem[]>([]);
|
const [data, setData] = React.useState<AnaliticoItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [globalFilter, setGlobalFilter] = React.useState("");
|
||||||
field: 'data_competencia',
|
const [columnFilters, setColumnFilters] = React.useState<any[]>([]);
|
||||||
direction: 'desc',
|
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
|
// Só faz a requisição se tiver dataInicio e dataFim
|
||||||
if (!filtros.dataInicio || !filtros.dataFim) {
|
if (!filtros.dataInicio || !filtros.dataFim) {
|
||||||
setData([]);
|
setData([]);
|
||||||
|
|
@ -100,61 +105,139 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
||||||
}
|
}
|
||||||
}, [filtros]);
|
}, [filtros]);
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const handleSort = (field: SortField) => {
|
const columns = React.useMemo(
|
||||||
setSortConfig((prev) => ({
|
() => [
|
||||||
field,
|
{
|
||||||
direction:
|
accessorKey: "data_competencia",
|
||||||
prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc',
|
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 (
|
||||||
|
<span className={isNegative ? 'text-red-600' : 'text-gray-900'}>
|
||||||
|
{formatted}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ accessorKey: "historico", header: "Histórico" },
|
||||||
|
{ accessorKey: "historico2", header: "Histórico 2" },
|
||||||
|
{ accessorKey: "recnum", header: "Recnum" },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const getSortIcon = (field: SortField) => {
|
const filterFns = React.useMemo(
|
||||||
if (sortConfig.field !== field) {
|
() => ({
|
||||||
return <ArrowUpDown className="ml-1 h-3 w-3" />;
|
advancedText: (row: any, columnId: string, filter: any) => {
|
||||||
}
|
if (!filter) return true;
|
||||||
return sortConfig.direction === 'asc' ? (
|
const raw = row.getValue(columnId);
|
||||||
<ArrowUp className="ml-1 h-3 w-3" />
|
const v = raw == null ? "" : String(raw);
|
||||||
) : (
|
const op = filter.operator;
|
||||||
<ArrowDown className="ml-1 h-3 w-3" />
|
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 table = useReactTable({
|
||||||
const aValue = a[sortConfig.field];
|
data,
|
||||||
const bValue = b[sortConfig.field];
|
columns,
|
||||||
|
state: { globalFilter, columnFilters },
|
||||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
return sortConfig.direction === 'asc'
|
onColumnFiltersChange: setColumnFilters,
|
||||||
? aValue.localeCompare(bValue)
|
filterFns,
|
||||||
: bValue.localeCompare(aValue);
|
getCoreRowModel: getCoreRowModel(),
|
||||||
}
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
||||||
return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
const parentRef = React.useRef<HTMLDivElement>(null);
|
||||||
return new Intl.NumberFormat('pt-BR', {
|
const rowVirtualizer = useVirtualizer({
|
||||||
style: 'currency',
|
count: table.getRowModel().rows.length,
|
||||||
currency: 'BRL',
|
getScrollElement: () => parentRef.current,
|
||||||
}).format(value);
|
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 clearFilters = () => {
|
||||||
const formatted = formatCurrency(value);
|
setConditions([{ column: "", operator: "contains", value: "" }]);
|
||||||
const isNegative = value < 0;
|
setColumnFilters([]);
|
||||||
return { formatted, isNegative };
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('pt-BR');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalValor = data.reduce((sum, item) => {
|
const totalValor = data.reduce((sum, item) => {
|
||||||
|
|
@ -213,255 +296,319 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full mt-2 border-t pt-1">
|
<Card className="w-full h-[85vh] shadow-xl rounded-2xl border-0 bg-gradient-to-br from-white to-gray-50/30">
|
||||||
<div className="w-[95%] mx-auto flex justify-between items-center mb-1">
|
<CardContent className="p-6 h-full flex flex-col">
|
||||||
<h2 className="text-lg font-bold">Análise Analítica</h2>
|
<div className="flex justify-between mb-6 flex-wrap gap-4">
|
||||||
{data.length > 0 && (
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||||
onClick={exportToExcel}
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
variant="outline"
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||||
size="sm"
|
</svg>
|
||||||
className="flex items-center gap-2"
|
</div>
|
||||||
>
|
<div>
|
||||||
<Download className="h-4 w-4" />
|
<h2 className="text-xl font-bold text-gray-900">Análise Analítica</h2>
|
||||||
Exportar XLSX
|
<p className="text-sm text-gray-500">Relatório detalhado de transações</p>
|
||||||
</Button>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="flex gap-3">
|
||||||
|
<Input
|
||||||
{/* Filtros aplicados */}
|
placeholder="Filtrar tudo..."
|
||||||
{/* <div className="mb-4 p-3 bg-gray-50 rounded-md">
|
value={globalFilter ?? ""}
|
||||||
<div className="text-sm">
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setGlobalFilter(e.target.value)}
|
||||||
<strong>Filtros aplicados:</strong>
|
className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||||
<div className="flex flex-wrap gap-2 mt-1">
|
/>
|
||||||
{filtros.centroCusto && (
|
<Button
|
||||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
|
variant="outline"
|
||||||
Centro: {filtros.centroCusto}
|
onClick={() => setOpen(true)}
|
||||||
</span>
|
className="bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
|
||||||
)}
|
>
|
||||||
{filtros.codigoGrupo && (
|
Filtros Avançados
|
||||||
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">
|
</Button>
|
||||||
Grupo: {filtros.codigoGrupo}
|
{data.length > 0 && (
|
||||||
</span>
|
<Button
|
||||||
)}
|
onClick={exportToExcel}
|
||||||
{filtros.codigoSubgrupo && (
|
variant="outline"
|
||||||
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
|
size="sm"
|
||||||
Subgrupo: {filtros.codigoSubgrupo}
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700"
|
||||||
</span>
|
>
|
||||||
)}
|
<Download className="h-4 w-4" />
|
||||||
{filtros.codigoConta && (
|
Exportar XLSX
|
||||||
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded text-xs">
|
</Button>
|
||||||
Conta: {filtros.codigoConta}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Resumo */}
|
<div className="relative flex-1 bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||||
|
<table className="min-w-full border-collapse table-fixed">
|
||||||
{/* Tabela */}
|
<thead
|
||||||
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative mx-auto">
|
className={`bg-gradient-to-r from-blue-50 to-indigo-50 sticky top-0 z-20 transition-all duration-200 ${isScrolled ? "shadow-lg" : "shadow-sm"}`}
|
||||||
{/* Header fixo */}
|
>
|
||||||
<div
|
{table.getHeaderGroups().map((hg: any) => (
|
||||||
className="sticky top-0 z-30 border-b shadow-sm"
|
<tr key={hg.id}>
|
||||||
style={{ backgroundColor: 'white', opacity: 1 }}
|
{hg.headers.map((header: any) => {
|
||||||
>
|
const sorted = header.column.getIsSorted();
|
||||||
<div
|
|
||||||
className="flex p-2 font-semibold text-xs"
|
|
||||||
style={{ backgroundColor: 'white', opacity: 1 }}
|
|
||||||
>
|
|
||||||
<div className="flex-1 min-w-[80px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('data_competencia')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Data Comp.
|
|
||||||
{getSortIcon('data_competencia')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('data_vencimento')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Data Venc.
|
|
||||||
{getSortIcon('data_vencimento')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('data_caixa')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Data Caixa
|
|
||||||
{getSortIcon('data_caixa')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('codigo_fornecedor')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Cód. Fornec.
|
|
||||||
{getSortIcon('codigo_fornecedor')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-2 min-w-[120px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('nome_fornecedor')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Fornecedor
|
|
||||||
{getSortIcon('nome_fornecedor')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('codigo_centrocusto')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Cód. Centro
|
|
||||||
{getSortIcon('codigo_centrocusto')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('codigo_conta')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Cód. Conta
|
|
||||||
{getSortIcon('codigo_conta')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-2 min-w-[120px]">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('conta')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Conta
|
|
||||||
{getSortIcon('conta')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] text-right">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleSort('valor')}
|
|
||||||
className="h-auto p-0 font-semibold"
|
|
||||||
>
|
|
||||||
Valor
|
|
||||||
{getSortIcon('valor')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-2 min-w-[120px]">Histórico</div>
|
|
||||||
<div className="flex-2 min-w-[120px]">Histórico 2</div>
|
|
||||||
<div className="flex-1 min-w-[80px]">Recnum</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-8 text-center text-sm text-gray-500">
|
|
||||||
Carregando dados analíticos...
|
|
||||||
</div>
|
|
||||||
) : sortedData.length === 0 ? (
|
|
||||||
<div className="p-8 text-center text-sm text-gray-500">
|
|
||||||
Nenhum dado analítico encontrado para os filtros aplicados.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
sortedData.map((row, index) => (
|
|
||||||
<div key={index} className="flex border-b hover:bg-gray-50">
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{formatDate(row.data_competencia)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{formatDate(row.data_vencimento)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{formatDate(row.data_caixa)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{row.codigo_fornecedor || '-'}
|
|
||||||
</div>
|
|
||||||
<div className="flex-2 min-w-[120px] p-1 text-xs">
|
|
||||||
{row.nome_fornecedor || '-'}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{row.codigo_centrocusto || '-'}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{row.codigo_conta || '-'}
|
|
||||||
</div>
|
|
||||||
<div className="flex-2 min-w-[120px] p-1 text-xs">
|
|
||||||
{row.conta || '-'}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] text-right p-1 text-xs font-medium">
|
|
||||||
{(() => {
|
|
||||||
const valor =
|
|
||||||
typeof row.valor === 'string'
|
|
||||||
? parseFloat(row.valor)
|
|
||||||
: row.valor;
|
|
||||||
const { formatted, isNegative } =
|
|
||||||
formatCurrencyWithColor(valor);
|
|
||||||
return (
|
return (
|
||||||
<span
|
<th
|
||||||
className={
|
key={header.id}
|
||||||
isNegative ? 'text-red-600' : 'text-gray-900'
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
}
|
className="text-left px-4 py-4 border-b border-gray-200 cursor-pointer select-none group hover:bg-blue-100/50 transition-colors duration-150 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{formatted}
|
<div className="flex items-center justify-between gap-2">
|
||||||
</span>
|
<span className="font-semibold text-gray-800 text-sm uppercase tracking-wide truncate">
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col flex-shrink-0">
|
||||||
|
{sorted === "asc" ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-blue-600" />
|
||||||
|
) : sorted === "desc" ? (
|
||||||
|
<ChevronDown className="w-4 h-4 text-blue-600" />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col opacity-30 group-hover:opacity-60 transition-opacity">
|
||||||
|
<ChevronUp className="w-3 h-3 -mb-1" />
|
||||||
|
<ChevronDown className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
);
|
);
|
||||||
})()}
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div ref={parentRef} className="overflow-auto h-full bg-white">
|
||||||
|
<table className="min-w-full border-collapse table-fixed">
|
||||||
|
<tbody
|
||||||
|
style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: "relative" }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="p-12 text-center">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
<span className="text-gray-500 font-medium">Carregando dados analíticos...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : virtualRows.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="p-12 text-center">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-500 font-medium">Nenhum dado analítico encontrado para os filtros aplicados.</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
virtualRows.map((virtualRow: any) => {
|
||||||
|
const row = table.getRowModel().rows[virtualRow.index];
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className="hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30 transition-all duration-150 border-b border-gray-100"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
display: "table",
|
||||||
|
width: "100%",
|
||||||
|
tableLayout: "fixed",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell: any, cellIndex: number) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={`px-4 py-3 text-sm whitespace-nowrap overflow-hidden ${
|
||||||
|
cellIndex === 0 ? 'font-medium text-gray-900' : 'text-gray-700'
|
||||||
|
} ${
|
||||||
|
cell.column.id === 'valor' ? 'text-right font-semibold' : ''
|
||||||
|
}`}
|
||||||
|
title={cell.getValue()}
|
||||||
|
>
|
||||||
|
<div className="truncate">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.length > 0 && (
|
||||||
|
<div className="mt-6 p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl shadow-sm">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-2 min-w-[120px] p-1 text-xs">
|
<div>
|
||||||
{row.historico || '-'}
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
</div>
|
Total de Registros: <span className="text-blue-600">{table.getRowModel().rows.length}</span>
|
||||||
<div className="flex-2 min-w-[120px] p-1 text-xs">
|
</h3>
|
||||||
{row.historico2 || '-'}
|
<p className="text-sm text-gray-600">Transações encontradas</p>
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-[80px] p-1 text-xs">
|
|
||||||
{row.recnum || '-'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="text-right">
|
||||||
)}
|
<h3 className="text-lg font-bold">
|
||||||
</div>
|
<span className={totalValor < 0 ? 'text-red-600' : 'text-green-600'}>
|
||||||
</div>
|
Valor Total: {new Intl.NumberFormat('pt-BR', {
|
||||||
{data.length > 0 && (
|
style: 'currency',
|
||||||
<div className="w-[95%] mb-4 p-4 bg-blue-50 border rounded-md mx-auto">
|
currency: 'BRL',
|
||||||
<div className="flex justify-between items-center">
|
}).format(totalValor)}
|
||||||
<div>
|
</span>
|
||||||
<h3 className="text-sm font-semibold">
|
</h3>
|
||||||
Total de Registros: {data.length}
|
<p className="text-sm text-gray-600">Soma de todos os valores</p>
|
||||||
</h3>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold">
|
|
||||||
{(() => {
|
|
||||||
const { formatted, isNegative } =
|
|
||||||
formatCurrencyWithColor(totalValor);
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={isNegative ? 'text-red-600' : 'text-blue-600'}
|
|
||||||
>
|
|
||||||
Valor Total: {formatted}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
|
||||||
|
<DialogHeader className="pb-4">
|
||||||
|
<DialogTitle className="text-xl font-semibold text-gray-900">Filtros Avançados</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
|
||||||
|
{conditions.map((cond, idx) => (
|
||||||
|
<div key={idx} className="flex gap-3 items-start p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Coluna
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={cond.column}
|
||||||
|
onValueChange={(v: string) => {
|
||||||
|
const next = [...conditions];
|
||||||
|
next[idx].column = v;
|
||||||
|
setConditions(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full bg-white border-gray-300">
|
||||||
|
<SelectValue placeholder="Selecione a coluna" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{columns.map((col: any) => (
|
||||||
|
<SelectItem key={col.accessorKey} value={col.accessorKey}>
|
||||||
|
{col.header}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Operador
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={cond.operator}
|
||||||
|
onValueChange={(v: string) => {
|
||||||
|
const next = [...conditions];
|
||||||
|
next[idx].operator = v;
|
||||||
|
if (v === "empty" || v === "notEmpty") next[idx].value = "";
|
||||||
|
setConditions(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full bg-white border-gray-300">
|
||||||
|
<SelectValue placeholder="Selecione o operador" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="contains">contém</SelectItem>
|
||||||
|
<SelectItem value="equals">igual a</SelectItem>
|
||||||
|
<SelectItem value="startsWith">começa com</SelectItem>
|
||||||
|
<SelectItem value="endsWith">termina com</SelectItem>
|
||||||
|
<SelectItem value="empty">está vazio</SelectItem>
|
||||||
|
<SelectItem value="notEmpty">não está vazio</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!(cond.operator === "empty" || cond.operator === "notEmpty") && (
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Valor
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={cond.value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const next = [...conditions];
|
||||||
|
next[idx].value = e.target.value;
|
||||||
|
setConditions(next);
|
||||||
|
}}
|
||||||
|
placeholder="Digite o valor"
|
||||||
|
className="w-full bg-white border-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{conditions.length > 1 && (
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const next = conditions.filter((_, i) => i !== idx);
|
||||||
|
setConditions(next);
|
||||||
|
}}
|
||||||
|
className="mt-6 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex justify-center pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
setConditions((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ column: "", operator: "contains", value: "" },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200"
|
||||||
|
>
|
||||||
|
<span className="text-lg">+</span>
|
||||||
|
Adicionar condição
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex gap-3 pt-6 border-t border-gray-200">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Limpar todos
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={applyFilters}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
Aplicar filtros
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
export { Card, CardContent }
|
||||||
|
|
@ -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<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-white px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
|
|
@ -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<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-white px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-white text-gray-900 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-100 focus:text-gray-900 hover:bg-gray-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue