fix: adicionados filtros e estilização na tabela de analítico

This commit is contained in:
Alessandro Gonçaalves 2025-10-20 12:15:53 -03:00
parent d7ab072622
commit 7758ad5abe
16 changed files with 4736 additions and 316 deletions

6
.eslintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}

95
docs/README.md Normal file
View File

@ -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)*

377
docs/api.md Normal file
View File

@ -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

185
docs/architecture.md Normal file
View File

@ -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

539
docs/components.md Normal file
View File

@ -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

264
docs/database.md Normal file
View File

@ -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

580
docs/deployment.md Normal file
View File

@ -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

529
docs/development.md Normal file
View File

@ -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

603
docs/troubleshooting.md Normal file
View File

@ -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

766
package-lock.json generated
View File

@ -8,7 +8,11 @@
"name": "dre-gerencial",
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
@ -1113,6 +1117,44 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1861,6 +1903,67 @@
"node": ">=12.4.0"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -1876,6 +1979,303 @@
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@ -1894,6 +2294,171 @@
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -2193,6 +2758,66 @@
"tailwindcss": "4.1.14"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.3"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@ -2261,7 +2886,7 @@
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@ -2903,6 +3528,18 @@
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@ -3513,6 +4150,12 @@
"node": ">=8"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@ -4627,6 +5270,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@ -6446,6 +7098,75 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -7435,6 +8156,49 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -9,7 +9,11 @@
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",

View File

@ -1,8 +1,32 @@
'use client';
import { Button } from '@/components/ui/button';
import { ArrowDown, ArrowUp, ArrowUpDown, Download } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import * as React from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
flexRender,
} from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { ChevronUp, ChevronDown, Download } from "lucide-react";
import * as XLSX from 'xlsx';
interface AnaliticoItem {
@ -27,26 +51,6 @@ interface AnaliticoItem {
updated_at: string;
}
type SortField =
| 'data_competencia'
| 'data_vencimento'
| 'data_caixa'
| 'codigo_fornecedor'
| 'nome_fornecedor'
| 'codigo_centrocusto'
| 'codigo_conta'
| 'conta'
| 'valor'
| 'historico'
| 'historico2'
| 'recnum';
type SortDirection = 'asc' | 'desc';
interface SortConfig {
field: SortField;
direction: SortDirection;
}
interface AnaliticoProps {
filtros: {
dataInicio: string;
@ -59,14 +63,15 @@ interface AnaliticoProps {
}
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [data, setData] = useState<AnaliticoItem[]>([]);
const [loading, setLoading] = useState(false);
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: 'data_competencia',
direction: 'desc',
});
const [data, setData] = React.useState<AnaliticoItem[]>([]);
const [loading, setLoading] = React.useState(false);
const [globalFilter, setGlobalFilter] = React.useState("");
const [columnFilters, setColumnFilters] = React.useState<any[]>([]);
const [open, setOpen] = React.useState(false);
const [conditions, setConditions] = React.useState([{ column: "", operator: "contains", value: "" }]);
const [isScrolled, setIsScrolled] = React.useState(false);
const fetchData = useCallback(async () => {
const fetchData = React.useCallback(async () => {
// Só faz a requisição se tiver dataInicio e dataFim
if (!filtros.dataInicio || !filtros.dataFim) {
setData([]);
@ -100,61 +105,139 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
}
}, [filtros]);
useEffect(() => {
React.useEffect(() => {
fetchData();
}, [fetchData]);
const handleSort = (field: SortField) => {
setSortConfig((prev) => ({
field,
direction:
prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc',
}));
};
const columns = React.useMemo(
() => [
{
accessorKey: "data_competencia",
header: "Data Comp.",
cell: ({ getValue }: any) => {
const value = getValue();
return new Date(value).toLocaleDateString('pt-BR');
}
},
{
accessorKey: "data_vencimento",
header: "Data Venc.",
cell: ({ getValue }: any) => {
const value = getValue();
return new Date(value).toLocaleDateString('pt-BR');
}
},
{
accessorKey: "data_caixa",
header: "Data Caixa",
cell: ({ getValue }: any) => {
const value = getValue();
return new Date(value).toLocaleDateString('pt-BR');
}
},
{ accessorKey: "codigo_fornecedor", header: "Cód. Fornec." },
{ accessorKey: "nome_fornecedor", header: "Fornecedor" },
{ accessorKey: "codigo_centrocusto", header: "Cód. Centro" },
{ accessorKey: "codigo_conta", header: "Cód. Conta" },
{ accessorKey: "conta", header: "Conta" },
{
accessorKey: "valor",
header: "Valor",
cell: ({ getValue }: any) => {
const value = getValue();
const formatted = new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value);
const isNegative = value < 0;
return (
<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) => {
if (sortConfig.field !== field) {
return <ArrowUpDown className="ml-1 h-3 w-3" />;
}
return sortConfig.direction === 'asc' ? (
<ArrowUp className="ml-1 h-3 w-3" />
) : (
<ArrowDown className="ml-1 h-3 w-3" />
);
};
const filterFns = React.useMemo(
() => ({
advancedText: (row: any, columnId: string, filter: any) => {
if (!filter) return true;
const raw = row.getValue(columnId);
const v = raw == null ? "" : String(raw);
const op = filter.operator;
const q = (filter.value ?? "").toString();
const a = v.toLowerCase();
const b = q.toLowerCase();
switch (op) {
case "contains":
return a.includes(b);
case "equals":
return a === b;
case "startsWith":
return a.startsWith(b);
case "endsWith":
return a.endsWith(b);
case "empty":
return a.length === 0;
case "notEmpty":
return a.length > 0;
default:
return true;
}
},
}),
[]
);
const sortedData = [...data].sort((a, b) => {
const aValue = a[sortConfig.field];
const bValue = b[sortConfig.field];
if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortConfig.direction === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue;
}
return 0;
const table = useReactTable({
data,
columns,
state: { globalFilter, columnFilters },
onGlobalFilterChange: setGlobalFilter,
onColumnFiltersChange: setColumnFilters,
filterFns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value);
const parentRef = React.useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: table.getRowModel().rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 36,
overscan: 20,
});
const virtualRows = rowVirtualizer.getVirtualItems();
React.useEffect(() => {
const handleScroll = () => {
if (!parentRef.current) return;
setIsScrolled(parentRef.current.scrollTop > 0);
};
const el = parentRef.current;
el?.addEventListener("scroll", handleScroll);
return () => el?.removeEventListener("scroll", handleScroll);
}, []);
const applyFilters = () => {
const filters = conditions
.filter((c) => c.column && (c.operator === "empty" || c.operator === "notEmpty" || (c.value ?? "") !== ""))
.map((c) => ({ id: c.column, value: { operator: c.operator, value: c.value } }));
setColumnFilters(filters);
setOpen(false);
};
const formatCurrencyWithColor = (value: number) => {
const formatted = formatCurrency(value);
const isNegative = value < 0;
return { formatted, isNegative };
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('pt-BR');
const clearFilters = () => {
setConditions([{ column: "", operator: "contains", value: "" }]);
setColumnFilters([]);
};
const totalValor = data.reduce((sum, item) => {
@ -213,255 +296,319 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
};
return (
<div className="w-full mt-2 border-t pt-1">
<div className="w-[95%] mx-auto flex justify-between items-center mb-1">
<h2 className="text-lg font-bold">Análise Analítica</h2>
{data.length > 0 && (
<Button
onClick={exportToExcel}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
)}
</div>
{/* Filtros aplicados */}
{/* <div className="mb-4 p-3 bg-gray-50 rounded-md">
<div className="text-sm">
<strong>Filtros aplicados:</strong>
<div className="flex flex-wrap gap-2 mt-1">
{filtros.centroCusto && (
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
Centro: {filtros.centroCusto}
</span>
)}
{filtros.codigoGrupo && (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">
Grupo: {filtros.codigoGrupo}
</span>
)}
{filtros.codigoSubgrupo && (
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
Subgrupo: {filtros.codigoSubgrupo}
</span>
)}
{filtros.codigoConta && (
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded text-xs">
Conta: {filtros.codigoConta}
</span>
<Card className="w-full h-[85vh] shadow-xl rounded-2xl border-0 bg-gradient-to-br from-white to-gray-50/30">
<CardContent className="p-6 h-full flex flex-col">
<div className="flex justify-between mb-6 flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 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 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" />
</svg>
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">Análise Analítica</h2>
<p className="text-sm text-gray-500">Relatório detalhado de transações</p>
</div>
</div>
<div className="flex gap-3">
<Input
placeholder="Filtrar tudo..."
value={globalFilter ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setGlobalFilter(e.target.value)}
className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
<Button
variant="outline"
onClick={() => setOpen(true)}
className="bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
>
Filtros Avançados
</Button>
{data.length > 0 && (
<Button
onClick={exportToExcel}
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700"
>
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
)}
</div>
</div>
</div> */}
{/* Resumo */}
{/* Tabela */}
<div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative mx-auto">
{/* Header fixo */}
<div
className="sticky top-0 z-30 border-b shadow-sm"
style={{ backgroundColor: 'white', opacity: 1 }}
>
<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);
<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">
<thead
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"}`}
>
{table.getHeaderGroups().map((hg: any) => (
<tr key={hg.id}>
{hg.headers.map((header: any) => {
const sorted = header.column.getIsSorted();
return (
<span
className={
isNegative ? 'text-red-600' : 'text-gray-900'
}
<th
key={header.id}
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}
</span>
<div className="flex items-center justify-between gap-2">
<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 className="flex-2 min-w-[120px] p-1 text-xs">
{row.historico || '-'}
</div>
<div className="flex-2 min-w-[120px] p-1 text-xs">
{row.historico2 || '-'}
</div>
<div className="flex-1 min-w-[80px] p-1 text-xs">
{row.recnum || '-'}
<div>
<h3 className="text-lg font-bold text-gray-900">
Total de Registros: <span className="text-blue-600">{table.getRowModel().rows.length}</span>
</h3>
<p className="text-sm text-gray-600">Transações encontradas</p>
</div>
</div>
))
)}
</div>
</div>
{data.length > 0 && (
<div className="w-[95%] mb-4 p-4 bg-blue-50 border rounded-md mx-auto">
<div className="flex justify-between items-center">
<div>
<h3 className="text-sm font-semibold">
Total de Registros: {data.length}
</h3>
</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 className="text-right">
<h3 className="text-lg font-bold">
<span className={totalValor < 0 ? 'text-red-600' : 'text-green-600'}>
Valor Total: {new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(totalValor)}
</span>
</h3>
<p className="text-sm text-gray-600">Soma de todos os valores</p>
</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>
);
}

View File

@ -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 }

View File

@ -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,
}

View File

@ -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 }

View File

@ -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,
}