feat: Arcádia Suite v2.0 - Sistema completo com 66 páginas, 6 motores, módulo Retail
This commit is contained in:
parent
065ab19a17
commit
ec173da844
|
|
@ -1,8 +1,65 @@
|
||||||
node_modules
|
node_modules/
|
||||||
dist
|
dist/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
server/public
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Replit specific
|
||||||
|
.cache/
|
||||||
|
.upm/
|
||||||
|
.config/
|
||||||
|
.local/
|
||||||
|
.pythonlibs/
|
||||||
|
.replit
|
||||||
|
replit.nix
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
server/public/
|
||||||
vite.config.ts.*
|
vite.config.ts.*
|
||||||
|
|
||||||
|
# Backups and archives
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
*.zip
|
||||||
|
plus_backup_*/
|
||||||
|
|
||||||
|
# Metabase
|
||||||
|
metabase/metabase.jar
|
||||||
metabase/metabase-data.*
|
metabase/metabase-data.*
|
||||||
metabase/plugins/
|
metabase/plugins/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.venv/
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# WhatsApp sessions
|
||||||
|
whatsapp-sessions/
|
||||||
|
|
||||||
|
# Uploads (user-generated content)
|
||||||
|
uploads/
|
||||||
|
plus/public/uploads/
|
||||||
|
plus/storage/logs/
|
||||||
|
|
||||||
|
# PHP dependencies (install via composer)
|
||||||
|
plus/vendor/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Attached assets
|
||||||
|
attached_assets/
|
||||||
|
|
||||||
|
# Package lock (regenerated on install)
|
||||||
|
package-lock.json
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,705 @@
|
||||||
|
# MAPA GERAL DO SISTEMA RETAIL - Arcádia Suite
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. VISÃO MACRO DO MÓDULO
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ ARCÁDIA RETAIL - MAPA GERAL ║
|
||||||
|
║ Loja de Celulares e Assistência Técnica ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ║
|
||||||
|
║ │DASHBOARD│ │ PDV │ │ PESSOAS │ │ ESTOQUE │ │SERVIÇOS │ ║
|
||||||
|
║ │ (KPIs) │ │ (Caixa) │ │(Cadastro│ │(Depósito│ │ (O.S.) │ ║
|
||||||
|
║ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ║
|
||||||
|
║ │ │ │ │ │ ║
|
||||||
|
║ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ║
|
||||||
|
║ │TRADE-IN │ │ COMPRAS │ │CADASTROS│ │RELATÓRIO│ │COMISSÕES│ ║
|
||||||
|
║ │(Aval.) │ │(Aquis.) │ │ (Config)│ │(Reports)│ │ (Metas) │ ║
|
||||||
|
║ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ║
|
||||||
|
║ │ │ │ │ │ ║
|
||||||
|
║ └────────────┴────────────┴─────┬──────┴────────────┘ ║
|
||||||
|
║ │ ║
|
||||||
|
║ ┌─────────┴─────────┐ ║
|
||||||
|
║ │ CONFIGURAÇÃO │ ║
|
||||||
|
║ │ Plus / Empresas │ ║
|
||||||
|
║ └───────────────────┘ ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. FLUXO PRINCIPAL DE OPERAÇÕES
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ ENTRADA DE MERCADORIA │
|
||||||
|
└──────────────────────┬───────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────────┬┴──────────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ COMPRA │ │ TRADE-IN │ │ CONSIGNAÇÃO │
|
||||||
|
│ Fornecedor │ │ Cliente │ │ Parceiro │
|
||||||
|
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────┴───────┐ │
|
||||||
|
│ │ Avaliação │ │
|
||||||
|
│ │ 19 itens │ │
|
||||||
|
│ │ checklist │ │
|
||||||
|
│ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────┴───────┐ │
|
||||||
|
│ │ Aprovação │ │
|
||||||
|
│ │ Gera Crédito│ │
|
||||||
|
│ │ Cria O.S. │ │
|
||||||
|
│ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────┴───────┐ │
|
||||||
|
│ │ Revisão │ │
|
||||||
|
│ │ O.S. Interna│ │
|
||||||
|
│ │ Manutenção │ │
|
||||||
|
│ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ ESTOQUE │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │Depósito │ │Depósito │ │Depósito │ │
|
||||||
|
│ │ Loja │ │ Central │ │Trânsito │ │
|
||||||
|
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||||
|
│ └────────────┼────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────┬┴──────────────────┐ │
|
||||||
|
│ │ Dispositivos │ Produtos │ │
|
||||||
|
│ │ (por IMEI) │ (por qtd/série) │ │
|
||||||
|
│ └────────────────┴───────────────────┘ │
|
||||||
|
└────────────────────┬────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ PDV │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||||
|
│ │Dispositivos│ │ Produtos │ │ Faturar O.S. │ │
|
||||||
|
│ │ (IMEI) │ │(Acessórios)│ │ (Serviços) │ │
|
||||||
|
│ └─────┬──────┘ └─────┬──────┘ └──────┬───────┘ │
|
||||||
|
│ └───────────────┼────────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────┐ │
|
||||||
|
│ │ CARRINHO │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Subtotal │ │
|
||||||
|
│ │ - Desconto │ │
|
||||||
|
│ │ - Trade-In │ │
|
||||||
|
│ │ - Crédito │ │
|
||||||
|
│ │ = TOTAL │ │
|
||||||
|
│ └────────┬─────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────┐ │
|
||||||
|
│ │ PAGAMENTO │ │
|
||||||
|
│ │ 💵 Dinheiro │ │
|
||||||
|
│ │ 💳 Débito │ │
|
||||||
|
│ │ 💳 Crédito (Nx) │ │
|
||||||
|
│ │ 📱 PIX │ │
|
||||||
|
│ │ 🔀 Combinado │ │
|
||||||
|
│ └────────┬─────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────┐ │
|
||||||
|
│ │ VENDA │ │
|
||||||
|
│ │ Impressão A4 │ │
|
||||||
|
│ │ Sync Plus │ │
|
||||||
|
│ │ NF-e/NFC-e │ │
|
||||||
|
│ └──────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────┼────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────────┐ ┌─────────┐ ┌────────────┐
|
||||||
|
│ COMISSÃO │ │RELATÓRIO│ │ DEVOLUÇÃO │
|
||||||
|
│ Vendedor │ │Caixa │ │ Troca │
|
||||||
|
│ Metas │ │Diário │ │ Crédito │
|
||||||
|
└────────────┘ └─────────┘ └────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. CICLO DE VIDA DO DISPOSITIVO (IMEI)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ JORNADA DO DISPOSITIVO POR IMEI │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ENTRADA │
|
||||||
|
│ ═══════ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ COMPRA │ │ TRADE-IN │ │ CONSIGNAÇÃO │ │
|
||||||
|
│ │ Fornecedor │ │ Cliente │ │ Parceiro │ │
|
||||||
|
│ │ condition: │ │ condition: │ │ condition: │ │
|
||||||
|
│ │ new │ │ used/refurb │ │ varies │ │
|
||||||
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌──────┴───────┐ │ │
|
||||||
|
│ │ │ AVALIAÇÃO │ │ │
|
||||||
|
│ │ │ Checklist │ │ │
|
||||||
|
│ │ │ 19 itens │ │ │
|
||||||
|
│ │ └──────┬───────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌──────┴───────┐ │ │
|
||||||
|
│ │ │ O.S. INT. │ │ │
|
||||||
|
│ │ │ Revisão │ │ │
|
||||||
|
│ │ │ Manutenção │ │ │
|
||||||
|
│ │ └──────┬───────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ESTOQUE (status: in_stock) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ IMEI: 35XXXXXXXXXXXXX │ │
|
||||||
|
│ │ Marca: Samsung | Modelo: S24 Ultra │ │
|
||||||
|
│ │ Cor: Preto | Storage: 256GB | RAM: 12GB │ │
|
||||||
|
│ │ Condição: refurbished │ │
|
||||||
|
│ │ Preço Compra: R$ 2.500 | Preço Venda: R$ 3.500 │ │
|
||||||
|
│ │ Depósito: Loja Centro │ │
|
||||||
|
│ └──────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ MOVIMENTAÇÕES │ │
|
||||||
|
│ ═══════════════ │ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────┐ ┌──────┴──────┐ ┌──────────────┐ │
|
||||||
|
│ │TRANSFER. │◄───│ DISPONÍVEL │───►│ ASSISTÊNCIA │ │
|
||||||
|
│ │Entre lojas│ │ para venda │ │ O.S. cliente│ │
|
||||||
|
│ │in_transit │ │ in_stock │ │ in_service │ │
|
||||||
|
│ └──────────┘ └──────┬──────┘ └──────┬───────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ (retorna após reparo) │
|
||||||
|
│ │ └──────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ SAÍDA ▼ ▼ │
|
||||||
|
│ ═════ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ VENDA │ │ DE VOLTA AO │ │
|
||||||
|
│ │ PDV + NF-e │ │ ESTOQUE │ │
|
||||||
|
│ │ status:sold │ │ in_stock │ │
|
||||||
|
│ └──────┬───────┘ └──────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────┴───────┐ │
|
||||||
|
│ │ DEVOLUÇÃO? │ │
|
||||||
|
│ │ returned │──────────► Volta ao estoque │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ RASTREAMENTO: device_history + imei_history (kardex completo) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. FLUXO DO TRADE-IN (4 ETAPAS DETALHADAS)
|
||||||
|
|
||||||
|
```
|
||||||
|
╔═══════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ TRADE-IN - FLUXO COMPLETO ║
|
||||||
|
╠═══════════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ ETAPA 1: AVALIAÇÃO ║
|
||||||
|
║ ══════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ Cliente chega com dispositivo usado │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ 1. Identificar cliente (busca por nome/CPF) │ ║
|
||||||
|
║ │ 2. Registrar IMEI, Marca, Modelo │ ║
|
||||||
|
║ │ 3. Preencher Checklist (19 itens): │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ ┌──────────────────────────────────────────────────────┐ │ ║
|
||||||
|
║ │ │ ☐ Liga corretamente ☐ Sensores/NFC │ │ ║
|
||||||
|
║ │ │ ☐ Sem avarias/fantasma ☐ Face ID/Touch ID │ │ ║
|
||||||
|
║ │ │ ☐ Sem manchas na tela ☐ Microfones │ │ ║
|
||||||
|
║ │ │ ☐ Botões funcionando ☐ Áudio auricular │ │ ║
|
||||||
|
║ │ │ ☐ Marcas de uso ☐ Alto-falante │ │ ║
|
||||||
|
║ │ │ ☐ Wi-Fi ☐ Carregamento │ │ ║
|
||||||
|
║ │ │ ☐ Chip ☐ Câmeras │ │ ║
|
||||||
|
║ │ │ ☐ 4G/5G ☐ Flash │ │ ║
|
||||||
|
║ │ │ ☐ Possui carregador ☐ 3uTools OK │ │ ║
|
||||||
|
║ │ │ 🔋 Bateria: ____% │ │ ║
|
||||||
|
║ │ └──────────────────────────────────────────────────────┘ │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ 4. Listar peças necessárias (se houver) │ ║
|
||||||
|
║ │ 5. Definir valor estimado │ ║
|
||||||
|
║ │ 6. Assinatura digital do cliente │ ║
|
||||||
|
║ │ 7. Imprimir comprovante │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Status: PENDING │ ║
|
||||||
|
║ └────────────────────────────────────────────┬───────────────┘ ║
|
||||||
|
║ │ ║
|
||||||
|
║ ▼ ║
|
||||||
|
║ ETAPA 2: APROVAÇÃO ║
|
||||||
|
║ ══════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ Gerente revisa avaliação e decide: │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ [APROVAR] [REJEITAR] │ ║
|
||||||
|
║ │ │ │ │ ║
|
||||||
|
║ │ ▼ ▼ │ ║
|
||||||
|
║ │ Automaticamente: Avaliação │ ║
|
||||||
|
║ │ ✅ Gera CRÉDITO para cliente rejeitada. │ ║
|
||||||
|
║ │ (R$ do valor estimado) Sem ações. │ ║
|
||||||
|
║ │ ✅ Cria O.S. INTERNA │ ║
|
||||||
|
║ │ (INT2602XXXXXX) │ ║
|
||||||
|
║ │ ✅ Registra no IMEI History │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Status: APPROVED │ ║
|
||||||
|
║ └────────────────────────────────────────────┬───────────────┘ ║
|
||||||
|
║ │ ║
|
||||||
|
║ ▼ ║
|
||||||
|
║ ETAPA 3: REVISÃO / MANUTENÇÃO ║
|
||||||
|
║ ═════════════════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ Técnico trabalha na O.S. Interna: │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ 1. Diagnóstico técnico │ ║
|
||||||
|
║ │ 2. Troca de peças (registra peças + custos) │ ║
|
||||||
|
║ │ 3. Limpeza e preparação │ ║
|
||||||
|
║ │ 4. Teste de qualidade │ ║
|
||||||
|
║ │ 5. Preenche checklist de conclusão │ ║
|
||||||
|
║ │ 6. Finaliza O.S. Interna │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Custos: │ ║
|
||||||
|
║ │ ├── Peças utilizadas: R$ XXX │ ║
|
||||||
|
║ │ ├── Mão de obra: R$ XXX │ ║
|
||||||
|
║ │ └── Total reparo: R$ XXX │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Status O.S.: COMPLETED │ ║
|
||||||
|
║ └────────────────────────────────────────────┬───────────────┘ ║
|
||||||
|
║ │ ║
|
||||||
|
║ ▼ ║
|
||||||
|
║ ETAPA 4: ENTRADA NO ESTOQUE ║
|
||||||
|
║ ═══════════════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ Ao finalizar O.S. Interna: │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ ✅ Cria dispositivo no estoque (mobile_devices) │ ║
|
||||||
|
║ │ condition: "refurbished" │ ║
|
||||||
|
║ │ acquisitionType: "trade_in" │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ 💰 Cálculo do preço sugerido: │ ║
|
||||||
|
║ │ Custo aquisição = Valor Trade-In + Custo Reparo │ ║
|
||||||
|
║ │ Preço sugerido = Custo × (1 + Margem%) │ ║
|
||||||
|
║ │ Ex: R$1500 + R$300 = R$1800 × 1.30 = R$ 2.340 │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ ✅ Registra no IMEI History │ ║
|
||||||
|
║ │ ✅ Registra no Activity Feed │ ║
|
||||||
|
║ │ ✅ Disponível no PDV para venda │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Status: IN_STOCK (pronto para venda) │ ║
|
||||||
|
║ └────────────────────────────────────────────────────────────┘ ║
|
||||||
|
╚═══════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. MAPA DO PDV (PONTO DE VENDA)
|
||||||
|
|
||||||
|
```
|
||||||
|
╔═══════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ PDV - PONTO DE VENDA ║
|
||||||
|
╠═══════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ BARRA SUPERIOR ║
|
||||||
|
║ ┌──────────────────────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ 🏪 Empresa: Loja Centro 👤 Vendedor: João Silva │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ [Sangria] [Reforço] [Devolução] [Selecionar Cliente] [Limpar] │ ║
|
||||||
|
║ └──────────────────────────────────────────────────────────────────┘ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌───────────────────────────────────┬──────────────────────────────┐ ║
|
||||||
|
║ │ CATÁLOGO (3 colunas) │ CARRINHO (2 colunas) │ ║
|
||||||
|
║ │ │ │ ║
|
||||||
|
║ │ ┌──────────┬──────────┬────────┐ │ ┌──────────────────────────┐│ ║
|
||||||
|
║ │ │📱Disposit│📦Produtos│🔧Fat.OS│ │ │ Samsung S24 Ultra ││ ║
|
||||||
|
║ │ └──────────┴──────────┴────────┘ │ │ IMEI: 35XXX 📱Celular ││ ║
|
||||||
|
║ │ │ │ R$ 3.500,00 ││ ║
|
||||||
|
║ │ [🔍 Buscar... ] │ ├──────────────────────────┤│ ║
|
||||||
|
║ │ │ │ Capinha Silicone ││ ║
|
||||||
|
║ │ ┌──────────────────────────────┐ │ │ 2x R$ 45,00 ││ ║
|
||||||
|
║ │ │ Samsung S24 Ultra │ │ │ R$ 90,00 ││ ║
|
||||||
|
║ │ │ 256GB | Preto | IMEI: 35XX │ │ ├──────────────────────────┤│ ║
|
||||||
|
║ │ │ 🟢 Novo 📦 Loja Centro │ │ │ O.S. #OS2602ABC ││ ║
|
||||||
|
║ │ │ R$ 3.500,00 │ │ │ Troca de tela 🔧Serviço ││ ║
|
||||||
|
║ │ │ [+ Adicionar] │ │ │ R$ 450,00 ││ ║
|
||||||
|
║ │ ├──────────────────────────────┤ │ ╞══════════════════════════╡│ ║
|
||||||
|
║ │ │ iPhone 15 Pro │ │ │ Subtotal R$ 4.040,00 ││ ║
|
||||||
|
║ │ │ 128GB | Branco | IMEI: 86XX │ │ │ Desconto -R$ 40,00 ││ ║
|
||||||
|
║ │ │ 🔵 Recond 📦 Loja Centro │ │ │ Trade-In -R$ 1.500,00 ││ ║
|
||||||
|
║ │ │ R$ 4.200,00 │ │ │ Crédito -R$ 200,00 ││ ║
|
||||||
|
║ │ │ [+ Adicionar] │ │ │─────────────────────────││ ║
|
||||||
|
║ │ ├──────────────────────────────┤ │ │ TOTAL R$ 2.300,00 ││ ║
|
||||||
|
║ │ │ Xiaomi 14 │ │ │ ││ ║
|
||||||
|
║ │ │ 512GB | Azul | IMEI: 86XX │ │ │ 💙 Crédito: R$ 200,00 ││ ║
|
||||||
|
║ │ │ 🟡 Usado 📦 Loja Centro │ │ │ ││ ║
|
||||||
|
║ │ │ R$ 2.800,00 │ │ │ [↕️ Trade-In] ││ ║
|
||||||
|
║ │ │ [+ Adicionar] │ │ │ [💰 FINALIZAR VENDA] ││ ║
|
||||||
|
║ │ └──────────────────────────────┘ │ └──────────────────────────┘│ ║
|
||||||
|
║ └───────────────────────────────────┴──────────────────────────────┘ ║
|
||||||
|
║ ║
|
||||||
|
║ MODAL DE PAGAMENTO ║
|
||||||
|
║ ┌──────────────────────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ Total: R$ 2.300,00 │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ [Dinheiro ✓] [Débito] [Crédito] [PIX] [Combinado] │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Valor: [R$ 2.300,00] Parcelas: [1x] │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ Desconto: [___]% ou [R$ ___] │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ │ [CONFIRMAR PAGAMENTO] │ ║
|
||||||
|
║ └──────────────────────────────────────────────────────────────────┘ ║
|
||||||
|
╚═══════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. FLUXO DA ORDEM DE SERVIÇO (12 ESTADOS)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ORDEM DE SERVIÇO - FLUXO DE STATUS │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ │
|
||||||
|
│ │ ABERTA │ ◄── Cliente entrega dispositivo │
|
||||||
|
│ │ open │ │
|
||||||
|
│ └────┬─────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────┐ │
|
||||||
|
│ │ DIAGNÓSTICO │ ◄── Técnico analisa problema │
|
||||||
|
│ │ diagnosis │ │
|
||||||
|
│ └──────┬──────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────┐ │
|
||||||
|
│ │ ORÇAMENTO │ ◄── Técnico elabora orçamento │
|
||||||
|
│ │ quote │ │
|
||||||
|
│ └──────┬──────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ AGUARD. APROVAÇÃO │ ◄── Enviado para cliente │
|
||||||
|
│ │ pending_approval │ │
|
||||||
|
│ └─────┬────────┬─────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ APROVOU │ │ REJEITOU │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ APROVADA │ │ REJEITADA│ ──► FIM │
|
||||||
|
│ │ approved │ │ rejected │ │
|
||||||
|
│ └────┬─────┘ └──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────┐ │
|
||||||
|
│ │ EM REPARO│ ◄── Técnico inicia trabalho │
|
||||||
|
│ │in_repair │ │
|
||||||
|
│ └────┬─────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────┼──────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ │ │
|
||||||
|
│ ┌──────────────┐ │ │
|
||||||
|
│ │ AGUARD. PEÇAS│ │ ◄── Se precisar de peça │
|
||||||
|
│ │waiting_parts │───────────────┘ │
|
||||||
|
│ └──────────────┘ (peça chegou, volta para reparo) │
|
||||||
|
│ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ QUALIDADE │ ◄── Verificação pós-reparo │
|
||||||
|
│ │quality_check │ │
|
||||||
|
│ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ PRONTO PARA │ ◄── Disponível para retirada │
|
||||||
|
│ │ RETIRADA │ (aparece no PDV p/ faturar) │
|
||||||
|
│ │ ready_pickup │ │
|
||||||
|
│ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ CONCLUÍDA │ ◄── Cliente retirou + pagou │
|
||||||
|
│ │ completed │ (faturada no PDV) │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Em qualquer momento: ──────────► ┌──────────────┐ │
|
||||||
|
│ │ CANCELADA │ │
|
||||||
|
│ │ cancelled │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. MAPA DE RELATÓRIOS
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ RELATÓRIOS DO RETAIL │
|
||||||
|
├──────────────┬──────────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ OS POR │ Status │ Qtd │ Valor Total │
|
||||||
|
│ STATUS │ open │ 12 │ R$ 5.400 │
|
||||||
|
│ │ in_repair │ 8 │ R$ 3.200 │
|
||||||
|
│ │ completed │ 45 │ R$ 22.500 │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┼──────────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ OS POR │ Técnico │ Total │ Concl. │ Andamento │ Receita │
|
||||||
|
│ TÉCNICO │ Carlos │ 15 │ 12 │ 3 │ R$8.500 │
|
||||||
|
│ │ Pedro │ 10 │ 8 │ 2 │ R$5.200 │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┼──────────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ VENDAS POR │ Vendedor │ Vendas │ Receita │ Ticket │ Dias │
|
||||||
|
│ VENDEDOR │ João │ 28 │ R$45.000 │ R$1607 │ 22 │
|
||||||
|
│ │ Maria │ 22 │ R$38.000 │ R$1727 │ 20 │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┼──────────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ MARGEM │ Dispositivo │ IMEI │ Custo │ Venda │ Margem │
|
||||||
|
│ POR IMEI │ S24 Ultra │ 35XXX │ R$2500 │ R$3500 │ 40.0% │
|
||||||
|
│ │ iPhone 15 │ 86XXX │ R$3800 │ R$4200 │ 10.5% │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┼──────────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ CAIXA │ ┌────────────────────────────────────────────┐ │
|
||||||
|
│ DIÁRIO │ │ Total Vendas: R$ 12.500 (8 vendas) │ │
|
||||||
|
│ │ │ Dinheiro: R$ 4.200 │ Cartão: R$ 5.800 │ │
|
||||||
|
│ │ │ PIX: R$ 2.500 │ Combinado: R$ 0 │ │
|
||||||
|
│ (4 queries) │ │ Sangrias: -R$ 500 │ Reforços: +R$ 200 │ │
|
||||||
|
│ │ │ Saldo Caixa: R$ 3.900 │ │
|
||||||
|
│ │ └────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ │ POR VENDEDOR: │
|
||||||
|
│ │ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
|
||||||
|
│ │ │Vendedor│Vendas│Dinh. │Déb. │Créd.│ PIX │TOTAL │ │
|
||||||
|
│ │ │João │ 5 │R$2100│R$1500│R$800│R$1200│R$5600│ │
|
||||||
|
│ │ │Maria │ 3 │R$2100│R$1300│R$600│R$1300│R$6900│ │
|
||||||
|
│ │ │TOTAL │ 8 │R$4200│R$2800│R$1400│R$2500│R$12.5│ │
|
||||||
|
│ │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┼──────────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ GIRO DE │ Produto │ Estoque │ Vendas 30d │ Turnover │
|
||||||
|
│ ESTOQUE │ Capinha │ 50 │ 35 │ 0.70 │
|
||||||
|
│ │ Película │ 80 │ 42 │ 0.53 │
|
||||||
|
│ │ │
|
||||||
|
└──────────────┴──────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. ARQUITETURA TÉCNICA
|
||||||
|
|
||||||
|
```
|
||||||
|
╔═══════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ ARQUITETURA DO MÓDULO RETAIL ║
|
||||||
|
╠═══════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ FRONTEND (React + TypeScript + Tailwind + shadcn/ui) ║
|
||||||
|
║ ═══════════════════════════════════════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ client/src/pages/ArcadiaRetail.tsx ──── 10.067 linhas ║
|
||||||
|
║ │ ║
|
||||||
|
║ ├── Dashboard (KPIs, Feed, Alertas) ║
|
||||||
|
║ ├── PDV (Carrinho, Catálogo, Pagamento) ║
|
||||||
|
║ ├── Pessoas (CRUD, Papéis, Histórico) ║
|
||||||
|
║ ├── Estoque (Depósitos, Séries, Inventário) ║
|
||||||
|
║ ├── Serviços (O.S. CRUD, Checklist) ║
|
||||||
|
║ ├── Trade-In (Avaliações, Fluxo 4 etapas) ║
|
||||||
|
║ ├── Compras (Pedidos, Recebimento) ║
|
||||||
|
║ ├── Cadastros (Pagamento, Vendedores, Promoções) ║
|
||||||
|
║ ├── Relatórios (6 sub-abas) ║
|
||||||
|
║ ├── Comissões (Dashboard, Metas, Fechamento) ║
|
||||||
|
║ └── Configuração (Plus, Empresas, Sync) ║
|
||||||
|
║ ║
|
||||||
|
║ client/src/components/TradeInForm.tsx ─── 988 linhas ║
|
||||||
|
║ └── Formulário completo c/ checklist + assinatura ║
|
||||||
|
║ ║
|
||||||
|
║ ───────────────────────────────────────────────────────────── ║
|
||||||
|
║ API (fetch → /api/retail/*) ║
|
||||||
|
║ ───────────────────────────────────────────────────────────── ║
|
||||||
|
║ ║
|
||||||
|
║ BACKEND (Express.js + Drizzle ORM) ║
|
||||||
|
║ ══════════════════════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ server/retail/routes.ts ──── 5.218 linhas (~130 endpoints) ║
|
||||||
|
║ │ ║
|
||||||
|
║ ├── CRUD: Devices, Evaluations, ServiceOrders, ║
|
||||||
|
║ │ Persons, Products, Warehouses, etc. ║
|
||||||
|
║ ├── PDV: Sessions, Sales, Payments, CashMovements ║
|
||||||
|
║ ├── Trade-In: Approve, Process, FullFlow, Stock ║
|
||||||
|
║ ├── Reports: OsStatus, OsTech, SalesSeller, ║
|
||||||
|
║ │ MarginIMEI, DailyCash, StockTurnover ║
|
||||||
|
║ ├── Returns: Search, Process, Credits ║
|
||||||
|
║ └── Commissions: Plans, Goals, Calculate, Close ║
|
||||||
|
║ ║
|
||||||
|
║ server/retail/plus-sync.ts ──── 542 linhas ║
|
||||||
|
║ └── Sync: Customers, Sales+Items, NF-e ║
|
||||||
|
║ ║
|
||||||
|
║ ───────────────────────────────────────────────────────────── ║
|
||||||
|
║ Drizzle ORM → PostgreSQL ║
|
||||||
|
║ ───────────────────────────────────────────────────────────── ║
|
||||||
|
║ ║
|
||||||
|
║ BANCO DE DADOS (PostgreSQL + 40 tabelas) ║
|
||||||
|
║ ════════════════════════════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ║
|
||||||
|
║ │ Dispositivos │ │ Vendas │ │ Estoque │ ║
|
||||||
|
║ │ mobile_devices│ │ pos_sales │ │ warehouses │ ║
|
||||||
|
║ │ device_history│ │pos_sale_items│ │warehouse_stk │ ║
|
||||||
|
║ │ imei_history │ │pos_sessions │ │stock_movem. │ ║
|
||||||
|
║ └──────────────┘ │cash_movements│ │transfers │ ║
|
||||||
|
║ └──────────────┘ │serials │ ║
|
||||||
|
║ ┌──────────────┐ │inventories │ ║
|
||||||
|
║ │ Trade-In │ ┌──────────────┐ └──────────────┘ ║
|
||||||
|
║ │ evaluations │ │ Serviços │ ║
|
||||||
|
║ │ checklist_* │ │service_orders│ ┌──────────────┐ ║
|
||||||
|
║ │ acquisitions │ │ so_items │ │ Cadastros │ ║
|
||||||
|
║ │ transfer_doc │ │ warranties │ │payment_meth. │ ║
|
||||||
|
║ └──────────────┘ └──────────────┘ │ sellers │ ║
|
||||||
|
║ │ comm_plans │ ║
|
||||||
|
║ ┌──────────────┐ ┌──────────────┐ │ promotions │ ║
|
||||||
|
║ │ Créditos │ │ Devoluções │ │ price_tables │ ║
|
||||||
|
║ │customer_cred.│ │return_exch. │ │product_types │ ║
|
||||||
|
║ └──────────────┘ │return_items │ └──────────────┘ ║
|
||||||
|
║ └──────────────┘ ║
|
||||||
|
║ ║
|
||||||
|
║ ───────────────────────────────────────────────────────────── ║
|
||||||
|
║ ║
|
||||||
|
║ INTEGRAÇÃO PLUS (Laravel ERP - Porta 8080) ║
|
||||||
|
║ ══════════════════════════════════════════ ║
|
||||||
|
║ ║
|
||||||
|
║ ┌───────────┐ ┌───────────┐ ┌───────────┐ ║
|
||||||
|
║ │Proxy Rev. │ │ SSO │ │Auto-Start │ ║
|
||||||
|
║ │/plus/* → │ │HMAC-SHA256│ │php artisan│ ║
|
||||||
|
║ │:8080 │ │Token │ │serve │ ║
|
||||||
|
║ └───────────┘ └───────────┘ └───────────┘ ║
|
||||||
|
║ │ ║
|
||||||
|
║ ▼ ║
|
||||||
|
║ ┌─────────────────────────────────────────────────┐ ║
|
||||||
|
║ │ Plus (Laravel) │ ║
|
||||||
|
║ │ ├── NF-e / NFC-e (SEFAZ) │ ║
|
||||||
|
║ │ ├── Clientes / Fornecedores │ ║
|
||||||
|
║ │ ├── Vendas / Faturamento │ ║
|
||||||
|
║ │ └── Fiscal completo │ ║
|
||||||
|
║ └─────────────────────────────────────────────────┘ ║
|
||||||
|
║ ║
|
||||||
|
╚═══════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. SEGURANÇA MULTI-TENANT
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CAMADAS DE SEGURANÇA │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ CAMADA 1: AUTENTICAÇÃO │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Passport.js Session → req.isAuthenticated() │ │
|
||||||
|
│ │ Todas as rotas /api/retail/* exigem login │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ CAMADA 2: TENANT SCOPING │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ req.user.tenantId → obrigatório │ │
|
||||||
|
│ │ Se ausente → 403 "Tenant not identified" │ │
|
||||||
|
│ │ WHERE tenant_id = $tenantId em TODAS as queries │ │
|
||||||
|
│ │ Dados de um tenant NUNCA vazam para outro │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ CAMADA 3: MODULE GATING │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ requireModule('retail') → verifica tenants.features.retail │ │
|
||||||
|
│ │ Se módulo inativo → 403 "Módulo não habilitado" │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ CAMADA 4: OPERAÇÕES SENSÍVEIS │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Devoluções → Exige senha do GERENTE │ │
|
||||||
|
│ │ Sangrias → Registra responsável + autorizador │ │
|
||||||
|
│ │ Exclusões → Soft delete quando possível │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ CAMADA 5: AUDITORIA │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ retail_activity_feed → Todas as operações relevantes │ │
|
||||||
|
│ │ device_history → Movimentação de IMEI │ │
|
||||||
|
│ │ imei_history → Kardex completo por IMEI │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. MAPA DE INTEGRAÇÕES
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ INTEGRAÇÕES DO RETAIL │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────┐ │
|
||||||
|
│ │ ARCÁDIA RETAIL │ │
|
||||||
|
│ │ (Express) │ │
|
||||||
|
│ └────────┬─────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────┼───────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ PLUS (PHP) │ │ ERPNEXT │ │ POSTGRESQL │ │
|
||||||
|
│ │ :8080 │ │ (API ext.) │ │ (Drizzle) │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ ▸ NF-e/NFC-e│ │ ▸ Clientes │ │ ▸ 40+ tabelas│ │
|
||||||
|
│ │ ▸ Clientes │ │ ▸ Fornecedor │ │ ▸ Retail │ │
|
||||||
|
│ │ ▸ Vendas │ │ ▸ Estoque │ │ ▸ Multi-ten. │ │
|
||||||
|
│ │ ▸ Fiscal │ │ ▸ Financeiro │ │ │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ SEFAZ │ │
|
||||||
|
│ │ NF-e / NFC-e (via Plus Laravel + nfelib) │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Métricas Finais:**
|
||||||
|
- **16.800+ linhas** de código
|
||||||
|
- **40+ tabelas** no banco
|
||||||
|
- **~130 endpoints** REST
|
||||||
|
- **11 abas** na interface
|
||||||
|
- **19 itens** no checklist de avaliação
|
||||||
|
- **12 estados** de O.S.
|
||||||
|
- **4 etapas** no fluxo de Trade-In
|
||||||
|
- **5 formas** de pagamento
|
||||||
|
- **4 queries** no caixa diário
|
||||||
|
|
@ -0,0 +1,469 @@
|
||||||
|
# Arcádia Suite - Mapa Geral do Sistema
|
||||||
|
|
||||||
|
## Visão Geral
|
||||||
|
|
||||||
|
**Arcádia Suite** é o Escritório Estratégico para a Empresa Moderna. Uma plataforma que centraliza produtividade, inteligência, tomada de decisão e governança, orquestrando ERPs, pessoas e dados.
|
||||||
|
|
||||||
|
**Princípio Central:** Separação absoluta entre decisão e execução.
|
||||||
|
- Arcádia **pensa, governa e orienta**
|
||||||
|
- ERPs **executam, registram e obedecem**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura de 4 Camadas
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CAMADA DE APRESENTAÇÃO │
|
||||||
|
│ React 18 + TypeScript + Tailwind CSS + shadcn/ui │
|
||||||
|
│ Interface tipo browser com abas + omnibox │
|
||||||
|
│ 66 páginas/módulos │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CAMADA DE ORQUESTRAÇÃO │
|
||||||
|
│ Express.js + Socket.IO + Manus Agent │
|
||||||
|
│ Porta 5000 (API + WebSocket) │
|
||||||
|
│ 38 arquivos de rotas / 23 ferramentas registradas │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CAMADA DE INTELIGÊNCIA │
|
||||||
|
│ FastAPI (Contábil 8003, BI 8004, Automação 8005) │
|
||||||
|
│ Communication Engine (Node 8006) │
|
||||||
|
│ OpenAI GPT-4o (Manus/Dev Center) + GPT-4o-mini (WhatsApp) │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CAMADA DE DADOS │
|
||||||
|
│ PostgreSQL + Drizzle ORM │
|
||||||
|
│ Knowledge Graph + ChromaDB (embeddings) │
|
||||||
|
│ Session Store + Multi-tenant │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mapa de Portas
|
||||||
|
|
||||||
|
| Porta | Serviço | Tecnologia |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| 5000 | API Principal + Frontend | Express.js + React |
|
||||||
|
| 8002 | Motor Fiscal (Fisco) | FastAPI (Python) |
|
||||||
|
| 8003 | Motor Contábil | FastAPI (Python) |
|
||||||
|
| 8004 | Motor BI (Insights) | FastAPI (Python) |
|
||||||
|
| 8005 | Motor Automação | FastAPI (Python) |
|
||||||
|
| 8006 | Motor Comunicação | Node.js/Express |
|
||||||
|
| 8080 | Arcádia Plus (Laravel) | PHP/Laravel |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Módulos do Frontend (66 páginas)
|
||||||
|
|
||||||
|
### Núcleo & Administração
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| Home | `/` | Dashboard principal |
|
||||||
|
| SOE | `/soe` | Sistema Operacional Empresarial |
|
||||||
|
| ERP | `/erp` | Módulo ERP legado |
|
||||||
|
| Admin | `/admin` | Administração do sistema |
|
||||||
|
| SuperAdmin | `/super-admin` | Gestão multi-tenant |
|
||||||
|
|
||||||
|
### Módulos de Negócio
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| Financeiro | `/financeiro` | Contas a pagar/receber, fluxo de caixa |
|
||||||
|
| Contábil | `/contabil` | Contabilidade, DRE, balancetes |
|
||||||
|
| Fiscal | `/fisco` | NF-e, NFC-e, CFOP, NCM, CEST |
|
||||||
|
| CRM | `/crm` | Gestão de relacionamento com cliente |
|
||||||
|
| People | `/people` | RH, colaboradores, folha |
|
||||||
|
| Production | `/production` | Ordens de produção |
|
||||||
|
| Quality | `/quality` | Controle de qualidade |
|
||||||
|
|
||||||
|
### Varejo & Comércio
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| Retail | `/retail` | Varejo (celulares, assistência técnica) |
|
||||||
|
| RetailReports | `/retail-reports` | Relatórios do varejo |
|
||||||
|
| Marketplace | `/marketplace` | Marketplace integrado |
|
||||||
|
| Valuation | `/valuation` | Avaliação de trade-in |
|
||||||
|
|
||||||
|
### Comunicação
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| WhatsApp | `/whatsapp` | Multi-sessão WhatsApp |
|
||||||
|
| Chat | `/chat` | Chat interno |
|
||||||
|
| XOS Inbox | `/xos/inbox` | Caixa de entrada unificada |
|
||||||
|
| XOS CRM | `/xos/crm` | CRM unificado |
|
||||||
|
| XOS Campaigns | `/xos/campaigns` | Campanhas de marketing |
|
||||||
|
| XOS Tickets | `/xos/tickets` | Sistema de tickets |
|
||||||
|
|
||||||
|
### Inteligência & IA
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| Scientist | `/scientist` | Auto-programação com IA |
|
||||||
|
| Knowledge | `/knowledge` | Base de conhecimento/grafo |
|
||||||
|
| BI Workspace | `/bi` | Business Intelligence |
|
||||||
|
| Manus | `/agent` | Agente autônomo central |
|
||||||
|
|
||||||
|
### Desenvolvimento & DevOps
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| IDE | `/ide` | Editor Monaco + Terminal |
|
||||||
|
| Dev Center | `/dev-center` | Centro de desenvolvimento XOS |
|
||||||
|
| XOS Pipeline | `/xos/pipeline` | Pipeline autônomo de código |
|
||||||
|
| XOS Governance | `/xos/governance` | Governança e políticas |
|
||||||
|
| API Hub | `/api-hub` | Central de APIs |
|
||||||
|
| API Tester | `/api-tester` | Testador de APIs |
|
||||||
|
| DocType Builder | `/doctype-builder` | Construtor de tipos |
|
||||||
|
| Page Builder | `/page-builder` | Construtor de páginas |
|
||||||
|
| Workflow Builder | `/workflow-builder` | Construtor de workflows |
|
||||||
|
|
||||||
|
### Operações & Engenharia
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| Engineering Hub | `/engineering` | Hub de engenharia |
|
||||||
|
| Field Operations | `/field-ops` | Operações de campo |
|
||||||
|
| Process Compass | `/compass` | Bússola de processos |
|
||||||
|
| Suppliers Portal | `/suppliers` | Portal de fornecedores |
|
||||||
|
|
||||||
|
### Plataforma
|
||||||
|
| Página | Rota | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| Engine Room | `/engine-room` | Casa de Máquinas (status dos motores) |
|
||||||
|
| Automations | `/automations` | Motor de automações |
|
||||||
|
| Plus | `/plus` | ERP Laravel (proxy) |
|
||||||
|
| LMS | `/lms` | Sistema de aprendizagem |
|
||||||
|
| Communities | `/communities` | Comunidades |
|
||||||
|
| Support | `/support` | Central de suporte |
|
||||||
|
| Migration | `/migration` | Migração de dados |
|
||||||
|
| Central APIs | `/central-apis` | APIs centrais |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## APIs do Backend (38 grupos de rotas)
|
||||||
|
|
||||||
|
### Core
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/login`, `/api/register` | `server/auth.ts` | Autenticação |
|
||||||
|
| `/api/admin/*` | `server/admin/routes.ts` | Administração |
|
||||||
|
| `/api/erp/*` | `server/erp/routes.ts` | ERP principal |
|
||||||
|
| `/api/soe/*` | `server/erp/routes.ts` | SOE (alias) |
|
||||||
|
| `/api/users/*` | `server/routes.ts` | Gestão de usuários |
|
||||||
|
|
||||||
|
### Negócio
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/financeiro/*` | `server/financeiro/routes.ts` | Financeiro |
|
||||||
|
| `/api/contabil/*` | `server/contabil/routes.ts` | Contabilidade |
|
||||||
|
| `/api/fisco/*` | `server/fisco/routes.ts` | Fiscal |
|
||||||
|
| `/api/crm/*` | `server/crm/routes.ts` | CRM |
|
||||||
|
| `/api/people/*` | `server/people/routes.ts` | RH/Pessoas |
|
||||||
|
| `/api/production/*` | `server/production/routes.ts` | Produção |
|
||||||
|
| `/api/quality/*` | `server/quality/routes.ts` | Qualidade |
|
||||||
|
| `/api/retail/*` | `server/retail/routes.ts` | Varejo |
|
||||||
|
| `/api/valuation/*` | `server/valuation/routes.ts` | Avaliação trade-in |
|
||||||
|
| `/api/marketplace/*` | `server/marketplace/routes.ts` | Marketplace |
|
||||||
|
|
||||||
|
### Comunicação
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/whatsapp/*` | `server/whatsapp/routes.ts` | WhatsApp multi-sessão |
|
||||||
|
| `/api/chat/*` | `server/chat/routes.ts` | Chat interno |
|
||||||
|
| `/api/email/*` | `server/email/routes.ts` | E-mail |
|
||||||
|
| `/api/comm/*` | proxy | Motor de Comunicação |
|
||||||
|
| `/api/xos/*` | `server/xos/routes.ts` | XOS CRM unificado |
|
||||||
|
|
||||||
|
### Inteligência
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/manus/*` | `server/manus/routes.ts` | Agente Manus IA |
|
||||||
|
| `/api/knowledge/*` | `server/learning/routes.ts` | Knowledge Graph |
|
||||||
|
| `/api/bi/*` | `server/bi/routes.ts` | Business Intelligence |
|
||||||
|
| `/api/bi/metaset/*` | `server/metaset/routes.ts` | Motor BI MetaSet |
|
||||||
|
| `/api/scientist/*` | `server/routes.ts` | Cientista de dados |
|
||||||
|
|
||||||
|
### Desenvolvimento
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/ide/*` | `server/ide/routes.ts` | IDE integrada |
|
||||||
|
| `/api/dev-center/*` | `server/blackboard/routes.ts` | Dev Center/Blackboard |
|
||||||
|
| `/api/xos/pipeline` | `server/blackboard/routes.ts` | Pipeline autônomo |
|
||||||
|
| `/api/governance/*` | `server/governance/routes.ts` | Governança |
|
||||||
|
| `/api/lowcode/*` | `server/lowcode/routes.ts` | Low-code engine |
|
||||||
|
|
||||||
|
### Protocolos de Interoperabilidade
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/mcp/v1/*` | `server/mcp/routes.ts` | Model Context Protocol |
|
||||||
|
| `/api/a2a/v1/*` | `server/routes.ts` | Agent to Agent Protocol |
|
||||||
|
| `/api/api-central/*` | `server/api-central/routes.ts` | Central de APIs |
|
||||||
|
|
||||||
|
### Infraestrutura
|
||||||
|
| Rota Base | Arquivo | Descrição |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `/api/engine-room/*` | `server/engine-room/routes.ts` | Casa de Máquinas |
|
||||||
|
| `/api/automations/*` | `server/automations/routes.ts` | Motor de Automação |
|
||||||
|
| `/api/modules/*` | `server/modules/loader.ts` | Módulos dinâmicos |
|
||||||
|
| `/api/login-bridge/*` | `server/login-bridge/routes.ts` | SSO Bridge |
|
||||||
|
| `/api/migration/*` | `server/migration/routes.ts` | Migração |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Motores (Engines)
|
||||||
|
|
||||||
|
### Motor IA - Manus (Node.js, porta 5000)
|
||||||
|
- **Modelo:** GPT-4o (Dev Center), GPT-4o-mini (WhatsApp)
|
||||||
|
- **Agentes:** 6 agentes autônomos (Architect, Generator, Validator, Executor, Researcher, Evolution)
|
||||||
|
- **Ferramentas:** 23 ferramentas registradas (GitHub, filesystem, BI, git)
|
||||||
|
- **Pipeline:** Design → Codegen → Validation → Staging → Evolution
|
||||||
|
|
||||||
|
### Motor Fiscal - Fisco (Python, porta 8002)
|
||||||
|
- NF-e / NFC-e via nfelib
|
||||||
|
- NCMs, CFOPs, CESTs, grupos tributários
|
||||||
|
- Certificados digitais
|
||||||
|
- Comunicação com SEFAZ
|
||||||
|
|
||||||
|
### Motor Contábil (Python, porta 8003)
|
||||||
|
- Plano de contas
|
||||||
|
- Lançamentos contábeis
|
||||||
|
- DRE, Balanço Patrimonial
|
||||||
|
|
||||||
|
### Motor BI - Insights (Python, porta 8004)
|
||||||
|
- Execução SQL
|
||||||
|
- Geração de gráficos
|
||||||
|
- Análise com Pandas
|
||||||
|
- Cache inteligente
|
||||||
|
|
||||||
|
### Motor Automação (Python, porta 8005)
|
||||||
|
- Cron scheduler
|
||||||
|
- Event bus
|
||||||
|
- Executor de workflows
|
||||||
|
|
||||||
|
### Motor Comunicação (Node.js, porta 8006)
|
||||||
|
- Unifica XOS CRM + WhatsApp + Email
|
||||||
|
- Contatos, threads, mensagens unificados
|
||||||
|
- Filas de atendimento
|
||||||
|
- Eventos para Knowledge Graph
|
||||||
|
|
||||||
|
### Arcádia Plus - ERP Laravel (PHP, porta 8080)
|
||||||
|
- NF-e/NFC-e/CT-e/MDF-e
|
||||||
|
- PDV (ponto de venda)
|
||||||
|
- Cardápio digital
|
||||||
|
- Ordens de serviço
|
||||||
|
- Estoque com rastreabilidade
|
||||||
|
- Integrações e-commerce (WooCommerce, Mercado Livre, NuvemShop)
|
||||||
|
- Integrações delivery (iFood)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dev Center XOS - 6 Agentes Autônomos
|
||||||
|
|
||||||
|
```
|
||||||
|
Prompt em Português
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ Architect │ ──▶ │ Generator │ ──▶ │ Validator │
|
||||||
|
│ (Design) │ │ (Codegen) │ │ (Typecheck) │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ Evolution │ ◀── │ Researcher │ ◀── │ Executor │
|
||||||
|
│ (Aprende) │ │ (Pesquisa) │ │ (Staging) │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Módulo Retail (Varejo de Celulares)
|
||||||
|
|
||||||
|
### Funcionalidades Core
|
||||||
|
- **Vendas com IMEI:** Rastreamento individual de aparelhos
|
||||||
|
- **Trade-in:** Avaliação com checklist de 19 itens
|
||||||
|
- **Ordens de Serviço:** Gestão completa de assistência técnica
|
||||||
|
- **Garantia:** Controle de garantias por IMEI
|
||||||
|
- **Caixa Diário:** Reconciliação de caixa com fechamento
|
||||||
|
- **Comissões:** Cálculo automático por vendedor
|
||||||
|
|
||||||
|
### Checklist Trade-in (19 itens)
|
||||||
|
1. Liga normalmente
|
||||||
|
2. Problemas na tela
|
||||||
|
3. WiFi funcionando
|
||||||
|
4. Bluetooth funcionando
|
||||||
|
5. Câmera frontal
|
||||||
|
6. Câmera traseira
|
||||||
|
7. Microfone
|
||||||
|
8. Alto-falante
|
||||||
|
9. Botões físicos
|
||||||
|
10. Sensor biométrico
|
||||||
|
11. Carregamento
|
||||||
|
12. Bateria saudável
|
||||||
|
13. GPS funcionando
|
||||||
|
14. Giroscópio
|
||||||
|
15. Acelerômetro
|
||||||
|
16. NFC
|
||||||
|
17. Resistência à água
|
||||||
|
18. Face ID / reconhecimento facial
|
||||||
|
19. Vibração
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Banco de Dados (PostgreSQL + Drizzle ORM)
|
||||||
|
|
||||||
|
### Tabelas Principais
|
||||||
|
| Grupo | Tabelas |
|
||||||
|
|-------|---------|
|
||||||
|
| **Identidade** | `users`, `profiles`, `tenants` |
|
||||||
|
| **Produtividade** | `workspace_pages`, `page_blocks`, `dashboard_widgets`, `quick_notes` |
|
||||||
|
| **Comunicação** | `conversations`, `messages`, `chat_threads`, `chat_messages` |
|
||||||
|
| **WhatsApp** | `whatsapp_sessions`, `whatsapp_contacts`, `whatsapp_messages`, `whatsapp_tickets` |
|
||||||
|
| **ERP Core** | `applications`, `erp_connections`, `agent_tasks`, `task_executions` |
|
||||||
|
| **Conhecimento** | `knowledge_base`, `knowledge_graph_nodes`, `knowledge_graph_edges` |
|
||||||
|
| **Governança** | `xos_governance_*`, `xos_job_queue`, `xos_agent_metrics` |
|
||||||
|
| **Pipeline** | `xos_staging_changes`, `xos_dev_pipelines` |
|
||||||
|
| **Comunicação Unificada** | `comm_contacts`, `comm_threads`, `comm_messages`, `comm_channels` |
|
||||||
|
| **Varejo** | Via módulos dinâmicos (`/api/modules/retail-reports`) |
|
||||||
|
| **Financeiro** | Contas, lançamentos, conciliação |
|
||||||
|
| **Fiscal** | NCMs, CFOPs, notas fiscais |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integrações Externas
|
||||||
|
|
||||||
|
| Serviço | Uso |
|
||||||
|
|---------|-----|
|
||||||
|
| **OpenAI** | GPT-4o (Manus, Dev Center), GPT-4o-mini (WhatsApp) |
|
||||||
|
| **GitHub** | Commits automáticos, análise de repositórios |
|
||||||
|
| **ERPNext** | Integração com ERP externo (clientes, produtos, vendas) |
|
||||||
|
| **WhatsApp/Baileys** | Multi-sessão de atendimento |
|
||||||
|
| **SEFAZ** | NF-e/NFC-e via nfelib |
|
||||||
|
| **Cloud-DFE** | SDK fiscal (NF-e, NFC-e, CT-e, MDF-e) |
|
||||||
|
| **WooCommerce** | E-commerce integration |
|
||||||
|
| **Mercado Livre** | Marketplace |
|
||||||
|
| **NuvemShop** | E-commerce |
|
||||||
|
| **iFood** | Delivery (pedidos, cardápio) |
|
||||||
|
| **Asaas** | Pagamentos, boletos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocolos de Interoperabilidade
|
||||||
|
|
||||||
|
| Protocolo | Rota | Descrição |
|
||||||
|
|-----------|------|-----------|
|
||||||
|
| **MCP** | `/api/mcp/v1/` | Model Context Protocol - exposição de ferramentas |
|
||||||
|
| **A2A** | `/api/a2a/v1/` | Agent to Agent - comunicação bidirecional |
|
||||||
|
| **AP2** | Planejado | Agent Payment Protocol |
|
||||||
|
| **UCP** | Planejado | Unified Commerce Protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como Rodar Localmente
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
- Node.js 20+
|
||||||
|
- Python 3.11+
|
||||||
|
- PostgreSQL 16+
|
||||||
|
- PHP 8.2+ (opcional, para Arcádia Plus)
|
||||||
|
|
||||||
|
### Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Extrair o backup
|
||||||
|
tar xzf arcadia-suite-backup.tar.gz
|
||||||
|
|
||||||
|
# 2. Instalar dependências Node
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 3. Instalar dependências Python
|
||||||
|
pip install fastapi uvicorn pandas numpy psycopg2-binary nfelib lxml cryptography
|
||||||
|
|
||||||
|
# 4. Configurar variáveis de ambiente
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar .env com suas credenciais:
|
||||||
|
# DATABASE_URL=postgresql://user:pass@localhost:5432/arcadia
|
||||||
|
# OPENAI_API_KEY=sk-...
|
||||||
|
# GITHUB_TOKEN=ghp_...
|
||||||
|
|
||||||
|
# 5. Criar banco de dados
|
||||||
|
createdb arcadia
|
||||||
|
|
||||||
|
# 6. Executar migrations
|
||||||
|
npx drizzle-kit push
|
||||||
|
|
||||||
|
# 7. Iniciar em desenvolvimento
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variáveis de Ambiente Necessárias
|
||||||
|
| Variável | Descrição |
|
||||||
|
|----------|-----------|
|
||||||
|
| `DATABASE_URL` | URL de conexão PostgreSQL |
|
||||||
|
| `OPENAI_API_KEY` | Chave da API OpenAI |
|
||||||
|
| `GITHUB_TOKEN` | Token GitHub para integrações |
|
||||||
|
| `ERPNEXT_URL` | URL do ERPNext (opcional) |
|
||||||
|
| `ERPNEXT_API_KEY` | Chave API ERPNext (opcional) |
|
||||||
|
| `ERPNEXT_API_SECRET` | Segredo API ERPNext (opcional) |
|
||||||
|
| `SESSION_SECRET` | Segredo para sessões Express |
|
||||||
|
|
||||||
|
### Credenciais Padrão
|
||||||
|
- **Usuário:** admin
|
||||||
|
- **Senha:** admin
|
||||||
|
- **Role:** master
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura de Diretórios
|
||||||
|
|
||||||
|
```
|
||||||
|
arcadia-suite/
|
||||||
|
├── client/ # Frontend React
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── pages/ # 66 páginas
|
||||||
|
│ │ ├── components/ # Componentes reutilizáveis
|
||||||
|
│ │ ├── hooks/ # Custom hooks
|
||||||
|
│ │ └── lib/ # Utilitários
|
||||||
|
│ └── public/ # Assets estáticos
|
||||||
|
├── server/ # Backend Express
|
||||||
|
│ ├── admin/ # Administração
|
||||||
|
│ ├── autonomous/ # Ferramentas autônomas
|
||||||
|
│ ├── bi/ # Business Intelligence
|
||||||
|
│ ├── blackboard/ # Dev Center (6 agentes)
|
||||||
|
│ ├── chat/ # Chat interno
|
||||||
|
│ ├── communication/ # Motor de comunicação
|
||||||
|
│ ├── contabil/ # Motor contábil
|
||||||
|
│ ├── crm/ # CRM
|
||||||
|
│ ├── engine-room/ # Casa de Máquinas
|
||||||
|
│ ├── erp/ # ERP/SOE
|
||||||
|
│ ├── financeiro/ # Financeiro
|
||||||
|
│ ├── fisco/ # Fiscal
|
||||||
|
│ ├── governance/ # Governança XOS
|
||||||
|
│ ├── ide/ # IDE integrada
|
||||||
|
│ ├── integrations/ # Integrações externas
|
||||||
|
│ ├── learning/ # Knowledge Graph
|
||||||
|
│ ├── manus/ # Agente Manus
|
||||||
|
│ ├── mcp/ # Model Context Protocol
|
||||||
|
│ ├── modules/ # Módulos dinâmicos
|
||||||
|
│ ├── people/ # RH
|
||||||
|
│ ├── plus/ # Proxy Laravel
|
||||||
|
│ ├── production/ # Produção
|
||||||
|
│ ├── python/ # Scripts Python
|
||||||
|
│ ├── quality/ # Qualidade
|
||||||
|
│ ├── retail/ # Varejo
|
||||||
|
│ ├── whatsapp/ # WhatsApp
|
||||||
|
│ └── xos/ # XOS unificado
|
||||||
|
├── shared/ # Código compartilhado
|
||||||
|
│ ├── schema.ts # Schema principal (Drizzle)
|
||||||
|
│ └── schemas/ # Schemas modulares
|
||||||
|
├── plus/ # ERP Laravel (PHP)
|
||||||
|
├── python-service/ # Serviço Python
|
||||||
|
├── db/ # Configuração do banco
|
||||||
|
├── migrations/ # Migrations Drizzle
|
||||||
|
└── docs/ # Documentação
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Arcádia Suite v2.0 - O Escritório Estratégico para a Empresa Moderna*
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Arcádia Suite
|
||||||
|
|
||||||
|
O Escritório Estratégico para a Empresa Moderna.
|
||||||
|
|
||||||
|
Plataforma que centraliza produtividade, inteligência, tomada de decisão e governança, orquestrando ERPs, pessoas e dados.
|
||||||
|
|
||||||
|
## Arquitetura
|
||||||
|
|
||||||
|
- **Frontend:** React 18 + TypeScript + Tailwind CSS + shadcn/ui (66 páginas)
|
||||||
|
- **Backend:** Express.js + Socket.IO (38 grupos de rotas API)
|
||||||
|
- **Inteligência:** OpenAI GPT-4o + 6 Agentes Autônomos (Dev Center XOS)
|
||||||
|
- **Motores:** Fiscal (8002), Contábil (8003), BI (8004), Automação (8005), Comunicação (8006)
|
||||||
|
- **ERP Plus:** Laravel/PHP (8080) com NF-e/NFC-e/CT-e/MDF-e
|
||||||
|
- **Banco:** PostgreSQL 16 + Drizzle ORM + ChromaDB
|
||||||
|
|
||||||
|
## Módulos Principais
|
||||||
|
|
||||||
|
- SOE (Sistema Operacional Empresarial)
|
||||||
|
- Financeiro, Contábil, Fiscal
|
||||||
|
- CRM + WhatsApp Multi-sessão
|
||||||
|
- Varejo (Celulares + Assistência Técnica)
|
||||||
|
- Business Intelligence
|
||||||
|
- Dev Center com Pipeline Autônomo
|
||||||
|
- Casa de Máquinas (Engine Room)
|
||||||
|
- Governança XOS
|
||||||
|
- Knowledge Graph + IA
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
cp .env.example .env # Configure DATABASE_URL, OPENAI_API_KEY
|
||||||
|
npx drizzle-kit push
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Login padrão:** admin / admin
|
||||||
|
|
||||||
|
## Documentação
|
||||||
|
|
||||||
|
- [Mapa Geral do Sistema](MAPA_SISTEMA_ARCADIA.md)
|
||||||
|
- [Relatório Técnico Retail](RELATORIO_TECNICO_RETAIL.md)
|
||||||
|
- [Mapa Retail](MAPA_GERAL_RETAIL.md)
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
Proprietário - Arcádia Suite
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,74 +1,89 @@
|
||||||
import { Switch, Route } from "wouter";
|
import { Switch, Route } from "wouter";
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
import { queryClient } from "./lib/queryClient";
|
import { queryClient } from "./lib/queryClient";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { AuthProvider } from "@/hooks/use-auth";
|
import { AuthProvider } from "@/hooks/use-auth";
|
||||||
import { ErpProfileProvider } from "@/contexts/ErpProfileContext";
|
import { SoeMotorProvider } from "@/contexts/SoeMotorContext";
|
||||||
import { ProtectedRoute } from "@/lib/protected-route";
|
import { ProtectedRoute } from "@/lib/protected-route";
|
||||||
import { CommandPalette } from "@/components/CommandPalette";
|
import { CommandPalette } from "@/components/CommandPalette";
|
||||||
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
|
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
|
||||||
import NotFound from "@/pages/not-found";
|
import NotFound from "@/pages/not-found";
|
||||||
import AuthPage from "@/pages/auth-page";
|
import AuthPage from "@/pages/auth-page";
|
||||||
import Agent from "@/pages/Agent";
|
|
||||||
import Admin from "@/pages/Admin";
|
|
||||||
import Chat from "@/pages/Chat";
|
|
||||||
import WhatsApp from "@/pages/WhatsApp";
|
|
||||||
import Automations from "@/pages/Automations";
|
|
||||||
import BiWorkspace from "@/pages/BiWorkspace";
|
|
||||||
import ProcessCompass from "@/pages/ProcessCompass";
|
|
||||||
import WorkspacePage from "@/pages/WorkspacePage";
|
|
||||||
import AppViewer from "@/pages/AppViewer";
|
|
||||||
import Crm from "@/pages/Crm";
|
|
||||||
import Production from "@/pages/Production";
|
|
||||||
import Support from "@/pages/Support";
|
|
||||||
import Valuation from "@/pages/Valuation";
|
|
||||||
import Canvas from "@/pages/Canvas";
|
|
||||||
import IDE from "@/pages/IDE";
|
|
||||||
import Scientist from "@/pages/Scientist";
|
|
||||||
import Knowledge from "@/pages/Knowledge";
|
|
||||||
import CentralApis from "@/pages/CentralApis";
|
|
||||||
import ApiTesterPage from "@/pages/ApiTesterPage";
|
|
||||||
import ApiHub from "@/pages/ApiHub";
|
|
||||||
import Cockpit from "@/pages/Cockpit";
|
|
||||||
import Fisco from "@/pages/Fisco";
|
|
||||||
import People from "@/pages/People";
|
|
||||||
import Contabil from "@/pages/Contabil";
|
|
||||||
import ERP from "@/pages/ERP";
|
|
||||||
import Financeiro from "@/pages/Financeiro";
|
|
||||||
import Communities from "@/pages/Communities";
|
|
||||||
import ArcadiaNext from "@/pages/ArcadiaNext";
|
|
||||||
import QualityModule from "@/pages/QualityModule";
|
|
||||||
import CommercialEnv from "@/pages/CommercialEnv";
|
|
||||||
import FieldOperations from "@/pages/FieldOperations";
|
|
||||||
import TechnicalModule from "@/pages/TechnicalModule";
|
|
||||||
import SuppliersPortal from "@/pages/SuppliersPortal";
|
|
||||||
import NPSSurvey from "@/pages/NPSSurvey";
|
|
||||||
import EngineeringHub from "@/pages/EngineeringHub";
|
|
||||||
import DocTypeBuilder from "@/pages/DocTypeBuilder";
|
|
||||||
import PageBuilder from "@/pages/PageBuilder";
|
|
||||||
import DevelopmentModule from "@/pages/DevelopmentModule";
|
|
||||||
import ArcadiaRetail from "@/pages/ArcadiaRetail";
|
|
||||||
import Plus from "@/pages/Plus";
|
|
||||||
import SuperAdmin from "@/pages/SuperAdmin";
|
|
||||||
import Marketplace from "@/pages/Marketplace";
|
|
||||||
import LMS from "@/pages/LMS";
|
|
||||||
import AppCenter from "@/pages/AppCenter";
|
|
||||||
import XosCentral from "@/pages/XosCentral";
|
|
||||||
import XosCrm from "@/pages/XosCrm";
|
|
||||||
import XosInbox from "@/pages/XosInbox";
|
|
||||||
import XosTickets from "@/pages/XosTickets";
|
|
||||||
import Migration from "@/pages/Migration";
|
|
||||||
import DevCenter from "@/pages/DevCenter";
|
|
||||||
import XosCampaigns from "@/pages/XosCampaigns";
|
|
||||||
import XosAutomations from "@/pages/XosAutomations";
|
|
||||||
import XosSites from "@/pages/XosSites";
|
|
||||||
import XosGovernance from "@/pages/XosGovernance";
|
|
||||||
import XosPipeline from "@/pages/XosPipeline";
|
|
||||||
|
|
||||||
|
const Agent = lazy(() => import("@/pages/Agent"));
|
||||||
|
const Admin = lazy(() => import("@/pages/Admin"));
|
||||||
|
const Chat = lazy(() => import("@/pages/Chat"));
|
||||||
|
const WhatsApp = lazy(() => import("@/pages/WhatsApp"));
|
||||||
|
const Automations = lazy(() => import("@/pages/Automations"));
|
||||||
|
const BiWorkspace = lazy(() => import("@/pages/BiWorkspace"));
|
||||||
|
const ProcessCompass = lazy(() => import("@/pages/ProcessCompass"));
|
||||||
|
const WorkspacePage = lazy(() => import("@/pages/WorkspacePage"));
|
||||||
|
const AppViewer = lazy(() => import("@/pages/AppViewer"));
|
||||||
|
const Crm = lazy(() => import("@/pages/Crm"));
|
||||||
|
const Production = lazy(() => import("@/pages/Production"));
|
||||||
|
const Support = lazy(() => import("@/pages/Support"));
|
||||||
|
const Valuation = lazy(() => import("@/pages/Valuation"));
|
||||||
|
const Canvas = lazy(() => import("@/pages/Canvas"));
|
||||||
|
const IDE = lazy(() => import("@/pages/IDE"));
|
||||||
|
const Scientist = lazy(() => import("@/pages/Scientist"));
|
||||||
|
const Knowledge = lazy(() => import("@/pages/Knowledge"));
|
||||||
|
const CentralApis = lazy(() => import("@/pages/CentralApis"));
|
||||||
|
const ApiTesterPage = lazy(() => import("@/pages/ApiTesterPage"));
|
||||||
|
const ApiHub = lazy(() => import("@/pages/ApiHub"));
|
||||||
|
const Cockpit = lazy(() => import("@/pages/Cockpit"));
|
||||||
|
const Fisco = lazy(() => import("@/pages/Fisco"));
|
||||||
|
const People = lazy(() => import("@/pages/People"));
|
||||||
|
const Contabil = lazy(() => import("@/pages/Contabil"));
|
||||||
|
const SOE = lazy(() => import("@/pages/SOE"));
|
||||||
|
const Financeiro = lazy(() => import("@/pages/Financeiro"));
|
||||||
|
const Communities = lazy(() => import("@/pages/Communities"));
|
||||||
|
const ArcadiaNext = lazy(() => import("@/pages/ArcadiaNext"));
|
||||||
|
const QualityModule = lazy(() => import("@/pages/QualityModule"));
|
||||||
|
const CommercialEnv = lazy(() => import("@/pages/CommercialEnv"));
|
||||||
|
const FieldOperations = lazy(() => import("@/pages/FieldOperations"));
|
||||||
|
const TechnicalModule = lazy(() => import("@/pages/TechnicalModule"));
|
||||||
|
const SuppliersPortal = lazy(() => import("@/pages/SuppliersPortal"));
|
||||||
|
const NPSSurvey = lazy(() => import("@/pages/NPSSurvey"));
|
||||||
|
const EngineeringHub = lazy(() => import("@/pages/EngineeringHub"));
|
||||||
|
const DocTypeBuilder = lazy(() => import("@/pages/DocTypeBuilder"));
|
||||||
|
const PageBuilder = lazy(() => import("@/pages/PageBuilder"));
|
||||||
|
const DevelopmentModule = lazy(() => import("@/pages/DevelopmentModule"));
|
||||||
|
const ArcadiaRetail = lazy(() => import("@/pages/ArcadiaRetail"));
|
||||||
|
const Plus = lazy(() => import("@/pages/Plus"));
|
||||||
|
const SuperAdmin = lazy(() => import("@/pages/SuperAdmin"));
|
||||||
|
const Marketplace = lazy(() => import("@/pages/Marketplace"));
|
||||||
|
const LMS = lazy(() => import("@/pages/LMS"));
|
||||||
|
const AppCenter = lazy(() => import("@/pages/AppCenter"));
|
||||||
|
const XosCentral = lazy(() => import("@/pages/XosCentral"));
|
||||||
|
const XosCrm = lazy(() => import("@/pages/XosCrm"));
|
||||||
|
const XosInbox = lazy(() => import("@/pages/XosInbox"));
|
||||||
|
const XosTickets = lazy(() => import("@/pages/XosTickets"));
|
||||||
|
const Migration = lazy(() => import("@/pages/Migration"));
|
||||||
|
const DevCenter = lazy(() => import("@/pages/DevCenter"));
|
||||||
|
const XosCampaigns = lazy(() => import("@/pages/XosCampaigns"));
|
||||||
|
const XosAutomations = lazy(() => import("@/pages/XosAutomations"));
|
||||||
|
const XosSites = lazy(() => import("@/pages/XosSites"));
|
||||||
|
const XosGovernance = lazy(() => import("@/pages/XosGovernance"));
|
||||||
|
const XosPipeline = lazy(() => import("@/pages/XosPipeline"));
|
||||||
|
|
||||||
|
|
||||||
|
function LoadingFallback() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh" }}>
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<div style={{ width: 40, height: 40, border: "3px solid #e5e7eb", borderTopColor: "#3b82f6", borderRadius: "50%", animation: "spin 1s linear infinite", margin: "0 auto" }} />
|
||||||
|
<p style={{ marginTop: 16, color: "#6b7280" }}>Carregando...</p>
|
||||||
|
</div>
|
||||||
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<ProtectedRoute path="/" component={Cockpit} />
|
<ProtectedRoute path="/" component={Cockpit} />
|
||||||
<ProtectedRoute path="/agent" component={Agent} />
|
<ProtectedRoute path="/agent" component={Agent} />
|
||||||
|
|
@ -93,7 +108,8 @@ function Router() {
|
||||||
<ProtectedRoute path="/fisco" component={Fisco} />
|
<ProtectedRoute path="/fisco" component={Fisco} />
|
||||||
<ProtectedRoute path="/people" component={People} />
|
<ProtectedRoute path="/people" component={People} />
|
||||||
<ProtectedRoute path="/contabil" component={Contabil} />
|
<ProtectedRoute path="/contabil" component={Contabil} />
|
||||||
<ProtectedRoute path="/erp" component={ERP} />
|
<ProtectedRoute path="/soe" component={SOE} />
|
||||||
|
<ProtectedRoute path="/erp" component={SOE} />
|
||||||
<ProtectedRoute path="/financeiro" component={Financeiro} />
|
<ProtectedRoute path="/financeiro" component={Financeiro} />
|
||||||
<ProtectedRoute path="/communities" component={Communities} />
|
<ProtectedRoute path="/communities" component={Communities} />
|
||||||
<ProtectedRoute path="/quality" component={QualityModule} />
|
<ProtectedRoute path="/quality" component={QualityModule} />
|
||||||
|
|
@ -129,6 +145,7 @@ function Router() {
|
||||||
<Route path="/auth" component={AuthPage} />
|
<Route path="/auth" component={AuthPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,14 +153,14 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ErpProfileProvider>
|
<SoeMotorProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<KnowledgeCollectorInit />
|
<KnowledgeCollectorInit />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<Router />
|
<Router />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ErpProfileProvider>
|
</SoeMotorProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,7 @@ import {
|
||||||
Calculator, Receipt, UserCog, Building2, Package, Wallet,
|
Calculator, Receipt, UserCog, Building2, Package, Wallet,
|
||||||
Hash, ChevronDown, Circle, MessageSquare
|
Hash, ChevronDown, Circle, MessageSquare
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png";
|
const browserIcon = "/arcadia_suite_icon.png";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
@ -777,16 +777,16 @@ export default function Home() {
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className="p-3 hover:shadow-md transition-all border-slate-200 hover:border-primary/30 bg-white group relative cursor-pointer"
|
className="p-3 hover:shadow-md transition-all border-slate-200 hover:border-primary/30 bg-white group relative cursor-pointer"
|
||||||
data-testid="card-module-erp"
|
data-testid="card-module-soe"
|
||||||
onClick={() => navigate("/erp")}
|
onClick={() => navigate("/soe")}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-lg bg-purple-500 flex items-center justify-center text-white shadow-sm">
|
<div className="w-10 h-10 rounded-lg bg-purple-500 flex items-center justify-center text-white shadow-sm">
|
||||||
<Package className="h-5 w-5" />
|
<Building2 className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium text-sm truncate">Arcádia ERP</h3>
|
<h3 className="font-medium text-sm truncate">Arcádia SOE</h3>
|
||||||
<p className="text-xs text-slate-400 truncate">Gestão Empresarial</p>
|
<p className="text-xs text-slate-400 truncate">Sistema Operacional</p>
|
||||||
</div>
|
</div>
|
||||||
<ExternalLink className="h-3.5 w-3.5 text-slate-300 group-hover:text-primary" />
|
<ExternalLink className="h-3.5 w-3.5 text-slate-300 group-hover:text-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import express, { type Request, Response, NextFunction } from "express";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
import { serveStatic } from "./static";
|
import { serveStatic } from "./static";
|
||||||
import { registerAllTools } from "./autonomous/tools";
|
import { registerAllTools } from "./autonomous/tools";
|
||||||
|
import { storage } from "./storage";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
@ -270,8 +271,6 @@ function startShellService(name: string, scriptPath: string, port: number) {
|
||||||
return shellProcess;
|
return shellProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
startShellService("metabase", path.join(process.cwd(), "metabase/start-metabase.sh"), 8088);
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
|
|
||||||
|
|
@ -333,6 +332,30 @@ app.use((req, res, next) => {
|
||||||
// It's registered after session middleware to enable SSO authentication
|
// It's registered after session middleware to enable SSO authentication
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// Seed master user if not exists
|
||||||
|
try {
|
||||||
|
const { scrypt, randomBytes } = await import("crypto");
|
||||||
|
const { promisify } = await import("util");
|
||||||
|
const scryptAsync = promisify(scrypt);
|
||||||
|
const existingAdmin = await storage.getUserByUsername("admin");
|
||||||
|
if (!existingAdmin) {
|
||||||
|
const salt = randomBytes(16).toString("hex");
|
||||||
|
const buf = (await scryptAsync("admin", salt, 64)) as Buffer;
|
||||||
|
const hashedPassword = `${buf.toString("hex")}.${salt}`;
|
||||||
|
await storage.createUser({
|
||||||
|
username: "admin",
|
||||||
|
password: hashedPassword,
|
||||||
|
name: "Administrador Master",
|
||||||
|
email: "admin@arcadia.suite",
|
||||||
|
role: "master",
|
||||||
|
status: "active",
|
||||||
|
});
|
||||||
|
console.log("[Seed] Usuário master 'admin' criado com sucesso");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("[Seed] Verificação de usuário master:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
await registerRoutes(httpServer, app);
|
await registerRoutes(httpServer, app);
|
||||||
|
|
||||||
await registerAllTools();
|
await registerAllTools();
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,41 @@ import {
|
||||||
retailSellerGoals, retailStoreGoals, retailCommissionClosures, retailCommissionClosureItems,
|
retailSellerGoals, retailStoreGoals, retailCommissionClosures, retailCommissionClosureItems,
|
||||||
retailWarehouseStock, retailStockMovements, retailProductSerials,
|
retailWarehouseStock, retailStockMovements, retailProductSerials,
|
||||||
retailStockTransfers, retailStockTransferItems, retailInventories, retailInventoryItems,
|
retailStockTransfers, retailStockTransferItems, retailInventories, retailInventoryItems,
|
||||||
products, purchaseOrders, purchaseOrderItems, suppliers
|
products, purchaseOrders, purchaseOrderItems, suppliers,
|
||||||
|
tenantEmpresas, tenants, type TenantFeatures,
|
||||||
|
posCashMovements, serviceWarranties
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { eq, desc, and, ilike, sql, or, asc, inArray, gte, lte } from "drizzle-orm";
|
import { eq, desc, and, ilike, sql, or, asc, inArray, gte, lte, isNull, between, count, sum } from "drizzle-orm";
|
||||||
|
import { retailPlusSyncService } from "./plus-sync";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
const requireModule = (moduleKey: keyof TenantFeatures) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
return res.status(401).json({ error: "Tenant not identified" });
|
||||||
|
}
|
||||||
|
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantId));
|
||||||
|
if (!tenant) {
|
||||||
|
return res.status(404).json({ error: "Tenant not found" });
|
||||||
|
}
|
||||||
|
const features = (tenant?.features as TenantFeatures) || {};
|
||||||
|
if (!features[moduleKey]) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: `Módulo "${moduleKey}" não está ativo para este tenant`,
|
||||||
|
moduleKey,
|
||||||
|
action: "Ative o módulo em Admin → Módulos"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const requireAuth = (req: Request, res: Response, next: NextFunction) => {
|
const requireAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (!req.isAuthenticated()) {
|
if (!req.isAuthenticated()) {
|
||||||
return res.status(401).json({ error: "Not authenticated" });
|
return res.status(401).json({ error: "Not authenticated" });
|
||||||
|
|
@ -1108,6 +1137,14 @@ router.get("/devices/imei/:imei", async (req: Request, res: Response) => {
|
||||||
|
|
||||||
router.post("/devices", async (req: Request, res: Response) => {
|
router.post("/devices", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
if (req.body.imei) {
|
||||||
|
const existing = await db.select({ id: mobileDevices.id }).from(mobileDevices)
|
||||||
|
.where(eq(mobileDevices.imei, req.body.imei))
|
||||||
|
.limit(1);
|
||||||
|
if (existing.length > 0) {
|
||||||
|
return res.status(400).json({ error: "IMEI já cadastrado no sistema. Cada aparelho deve ter um IMEI único." });
|
||||||
|
}
|
||||||
|
}
|
||||||
const [device] = await db.insert(mobileDevices).values(req.body).returning();
|
const [device] = await db.insert(mobileDevices).values(req.body).returning();
|
||||||
await db.insert(deviceHistory).values({
|
await db.insert(deviceHistory).values({
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
|
|
@ -1516,11 +1553,61 @@ router.put("/service-orders/:id", async (req: Request, res: Response) => {
|
||||||
const laborCost = String(parseFloat(req.body.laborCost || 0));
|
const laborCost = String(parseFloat(req.body.laborCost || 0));
|
||||||
const totalCost = String(parseFloat(partsCost) + parseFloat(laborCost));
|
const totalCost = String(parseFloat(partsCost) + parseFloat(laborCost));
|
||||||
|
|
||||||
|
const updatePayload: any = { ...req.body, partsCost, laborCost, totalCost, updatedAt: sql`CURRENT_TIMESTAMP` };
|
||||||
|
if (req.body.checklistCompletedBy) {
|
||||||
|
updatePayload.checklistCompletedAt = sql`CURRENT_TIMESTAMP`;
|
||||||
|
}
|
||||||
const [order] = await db.update(serviceOrders)
|
const [order] = await db.update(serviceOrders)
|
||||||
.set({ ...req.body, partsCost, laborCost, totalCost, updatedAt: sql`CURRENT_TIMESTAMP` })
|
.set(updatePayload)
|
||||||
.where(eq(serviceOrders.id, parseInt(req.params.id)))
|
.where(eq(serviceOrders.id, parseInt(req.params.id)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
// Auto-deduct parts from stock when OS is completed (RN-02)
|
||||||
|
if (req.body.status === "completed" || req.body.status === "ready_pickup") {
|
||||||
|
const items = await db.select().from(serviceOrderItems)
|
||||||
|
.where(and(
|
||||||
|
eq(serviceOrderItems.serviceOrderId, order.id),
|
||||||
|
eq(serviceOrderItems.itemType, "part"),
|
||||||
|
eq(serviceOrderItems.status, "pending")
|
||||||
|
));
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.itemCode) {
|
||||||
|
const [product] = await db.select().from(products)
|
||||||
|
.where(eq(products.code, item.itemCode));
|
||||||
|
if (product) {
|
||||||
|
const newQty = Math.max(0, parseFloat(product.stockQty as string || "0") - (item.quantity || 1));
|
||||||
|
await db.update(products)
|
||||||
|
.set({ stockQty: String(newQty) })
|
||||||
|
.where(eq(products.id, product.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.update(serviceOrderItems)
|
||||||
|
.set({ status: "applied" })
|
||||||
|
.where(eq(serviceOrderItems.id, item.id));
|
||||||
|
}
|
||||||
|
// Auto-create warranty when OS is completed
|
||||||
|
if (req.body.status === "completed" && order.imei) {
|
||||||
|
const warrantyDays = order.serviceType === "repair" ? 90 : order.serviceType === "maintenance" ? 30 : 60;
|
||||||
|
const startDate = new Date().toISOString().split('T')[0];
|
||||||
|
const endDateObj = new Date();
|
||||||
|
endDateObj.setDate(endDateObj.getDate() + warrantyDays);
|
||||||
|
await db.insert(serviceWarranties).values({
|
||||||
|
tenantId: order.tenantId,
|
||||||
|
storeId: order.storeId,
|
||||||
|
serviceOrderId: order.id,
|
||||||
|
deviceId: order.deviceId,
|
||||||
|
imei: order.imei,
|
||||||
|
serviceType: order.serviceType || "repair",
|
||||||
|
warrantyDays,
|
||||||
|
startDate,
|
||||||
|
endDate: endDateObj.toISOString().split('T')[0],
|
||||||
|
customerName: order.customerName,
|
||||||
|
customerPhone: order.customerPhone,
|
||||||
|
description: `Garantia automática - OS ${order.orderNumber}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Se a OS é de Trade-In e tem uma avaliação vinculada, atualizar o status da avaliação também
|
// Se a OS é de Trade-In e tem uma avaliação vinculada, atualizar o status da avaliação também
|
||||||
if (order.isInternal && order.sourceEvaluationId && req.body.evaluationStatus) {
|
if (order.isInternal && order.sourceEvaluationId && req.body.evaluationStatus) {
|
||||||
const evaluationStatusMap: Record<string, string> = {
|
const evaluationStatusMap: Record<string, string> = {
|
||||||
|
|
@ -1651,6 +1738,46 @@ router.post("/service-orders/:id/items", async (req: Request, res: Response) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/service-orders/:id/items", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const items = await db.select().from(serviceOrderItems)
|
||||||
|
.where(eq(serviceOrderItems.serviceOrderId, parseInt(req.params.id)))
|
||||||
|
.orderBy(desc(serviceOrderItems.createdAt));
|
||||||
|
res.json(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching service order items:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch items" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/service-orders/:id/items/:itemId", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const serviceOrderId = parseInt(req.params.id);
|
||||||
|
const itemId = parseInt(req.params.itemId);
|
||||||
|
|
||||||
|
await db.delete(serviceOrderItems).where(
|
||||||
|
and(
|
||||||
|
eq(serviceOrderItems.id, itemId),
|
||||||
|
eq(serviceOrderItems.serviceOrderId, serviceOrderId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = await db.select().from(serviceOrderItems)
|
||||||
|
.where(eq(serviceOrderItems.serviceOrderId, serviceOrderId));
|
||||||
|
const partsCost = items.filter(i => i.itemType === 'part').reduce((sum, i) => sum + parseFloat(i.totalPrice as any), 0);
|
||||||
|
const laborCost = items.filter(i => i.itemType === 'labor').reduce((sum, i) => sum + parseFloat(i.totalPrice as any), 0);
|
||||||
|
|
||||||
|
await db.update(serviceOrders)
|
||||||
|
.set({ partsCost: String(partsCost), laborCost: String(laborCost), totalCost: String(partsCost + laborCost), updatedAt: sql`CURRENT_TIMESTAMP` })
|
||||||
|
.where(eq(serviceOrders.id, serviceOrderId));
|
||||||
|
|
||||||
|
res.json({ success: true, items });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error removing service order item:", error);
|
||||||
|
res.status(500).json({ error: "Failed to remove item" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ========== POS SESSIONS ==========
|
// ========== POS SESSIONS ==========
|
||||||
router.get("/pos-sessions", async (req: Request, res: Response) => {
|
router.get("/pos-sessions", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1726,6 +1853,319 @@ router.put("/pos-sessions/:id/close", async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== CASH MOVEMENTS (Sangria/Reforço) ==========
|
||||||
|
router.get("/cash-movements", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { sessionId } = req.query;
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
const conditions = [];
|
||||||
|
if (sessionId) conditions.push(eq(posCashMovements.sessionId, parseInt(sessionId as string)));
|
||||||
|
if (tenantId) conditions.push(eq(posCashMovements.tenantId, tenantId));
|
||||||
|
const movements = conditions.length > 0
|
||||||
|
? await db.select().from(posCashMovements).where(and(...conditions)).orderBy(desc(posCashMovements.createdAt))
|
||||||
|
: await db.select().from(posCashMovements).orderBy(desc(posCashMovements.createdAt));
|
||||||
|
res.json(movements);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cash movements:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch cash movements" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/cash-movements", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const user = (req as any).user;
|
||||||
|
const [movement] = await db.insert(posCashMovements).values({
|
||||||
|
...req.body,
|
||||||
|
performedBy: user?.id,
|
||||||
|
performedByName: user?.name || user?.username,
|
||||||
|
tenantId: user?.tenantId
|
||||||
|
}).returning();
|
||||||
|
res.json(movement);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating cash movement:", error);
|
||||||
|
res.status(500).json({ error: "Failed to create cash movement" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== SERVICE WARRANTIES ==========
|
||||||
|
router.get("/warranties", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { imei, status, serviceOrderId } = req.query;
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
const conditions = [];
|
||||||
|
if (tenantId) conditions.push(eq(serviceWarranties.tenantId, tenantId));
|
||||||
|
if (imei) conditions.push(eq(serviceWarranties.imei, imei as string));
|
||||||
|
if (status) conditions.push(eq(serviceWarranties.status, status as string));
|
||||||
|
if (serviceOrderId) conditions.push(eq(serviceWarranties.serviceOrderId, parseInt(serviceOrderId as string)));
|
||||||
|
const warranties = conditions.length > 0
|
||||||
|
? await db.select().from(serviceWarranties).where(and(...conditions)).orderBy(desc(serviceWarranties.createdAt))
|
||||||
|
: await db.select().from(serviceWarranties).orderBy(desc(serviceWarranties.createdAt));
|
||||||
|
res.json(warranties);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching warranties:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch warranties" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/warranties", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const user = (req as any).user;
|
||||||
|
const startDate = req.body.startDate || new Date().toISOString().split('T')[0];
|
||||||
|
const warrantyDays = parseInt(req.body.warrantyDays || 90);
|
||||||
|
const endDateObj = new Date(startDate);
|
||||||
|
endDateObj.setDate(endDateObj.getDate() + warrantyDays);
|
||||||
|
const endDate = endDateObj.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const [warranty] = await db.insert(serviceWarranties).values({
|
||||||
|
...req.body,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
warrantyDays,
|
||||||
|
tenantId: user?.tenantId
|
||||||
|
}).returning();
|
||||||
|
res.json(warranty);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating warranty:", error);
|
||||||
|
res.status(500).json({ error: "Failed to create warranty" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/warranties/check/:imei", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const conditions = [
|
||||||
|
eq(serviceWarranties.imei, req.params.imei),
|
||||||
|
eq(serviceWarranties.status, "active"),
|
||||||
|
gte(serviceWarranties.endDate, today)
|
||||||
|
];
|
||||||
|
if (tenantId) conditions.push(eq(serviceWarranties.tenantId, tenantId));
|
||||||
|
const warranties = await db.select().from(serviceWarranties)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(serviceWarranties.endDate));
|
||||||
|
res.json({ hasActiveWarranty: warranties.length > 0, warranties });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking warranty:", error);
|
||||||
|
res.status(500).json({ error: "Failed to check warranty" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/warranties/:id/claim", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const [warranty] = await db.update(serviceWarranties)
|
||||||
|
.set({ status: "claimed", claimedAt: sql`CURRENT_TIMESTAMP`, claimNotes: req.body.notes })
|
||||||
|
.where(eq(serviceWarranties.id, parseInt(req.params.id)))
|
||||||
|
.returning();
|
||||||
|
res.json(warranty);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error claiming warranty:", error);
|
||||||
|
res.status(500).json({ error: "Failed to claim warranty" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== STOCK ALERTS ==========
|
||||||
|
router.get("/stock-alerts", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
const conditions = [
|
||||||
|
eq(products.status, "active"),
|
||||||
|
sql`CAST(${products.stockQty} AS NUMERIC) <= CAST(${products.minStock} AS NUMERIC)`,
|
||||||
|
sql`CAST(${products.minStock} AS NUMERIC) > 0`
|
||||||
|
];
|
||||||
|
if (tenantId) conditions.push(eq(products.tenantId, tenantId));
|
||||||
|
const lowStockProducts = await db.select().from(products)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(asc(products.name));
|
||||||
|
res.json(lowStockProducts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stock alerts:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch stock alerts" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== REPORTS ==========
|
||||||
|
router.get("/reports/os-by-status", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
|
||||||
|
const result = await db.execute(sql`
|
||||||
|
SELECT status, COUNT(*) as count,
|
||||||
|
COALESCE(SUM(CAST(total_cost AS NUMERIC)), 0) as total_value
|
||||||
|
FROM service_orders WHERE tenant_id = ${tenantId}
|
||||||
|
GROUP BY status ORDER BY count DESC
|
||||||
|
`);
|
||||||
|
res.json(result.rows || result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching OS report:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch report" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/reports/os-by-technician", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
|
||||||
|
const result = await db.execute(sql`
|
||||||
|
SELECT technician_name,
|
||||||
|
COUNT(*) as total_os,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'completed') as completed,
|
||||||
|
COUNT(*) FILTER (WHERE status IN ('open','diagnosis','in_repair')) as in_progress,
|
||||||
|
COALESCE(SUM(CAST(total_cost AS NUMERIC)) FILTER (WHERE status = 'completed'), 0) as total_revenue
|
||||||
|
FROM service_orders
|
||||||
|
WHERE technician_name IS NOT NULL AND tenant_id = ${tenantId}
|
||||||
|
GROUP BY technician_name ORDER BY total_os DESC
|
||||||
|
`);
|
||||||
|
res.json(result.rows || result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching technician report:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch report" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/reports/sales-by-seller", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { dateFrom, dateTo } = req.query;
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
|
||||||
|
let dateFilter = sql`TRUE`;
|
||||||
|
if (dateFrom && dateTo) {
|
||||||
|
dateFilter = sql`created_at >= ${dateFrom}::date AND created_at <= (${dateTo}::date + INTERVAL '1 day')`;
|
||||||
|
} else {
|
||||||
|
dateFilter = sql`DATE(created_at) >= CURRENT_DATE - INTERVAL '30 days'`;
|
||||||
|
}
|
||||||
|
const tenantFilter = sql`AND tenant_id = ${tenantId}`;
|
||||||
|
const result = await db.execute(sql`
|
||||||
|
SELECT sold_by,
|
||||||
|
COUNT(*) as total_sales,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)), 0) as total_revenue,
|
||||||
|
COALESCE(AVG(CAST(total_amount AS NUMERIC)), 0) as avg_ticket,
|
||||||
|
COUNT(DISTINCT DATE(created_at)) as active_days
|
||||||
|
FROM pos_sales
|
||||||
|
WHERE status = 'completed' AND ${dateFilter} ${tenantFilter}
|
||||||
|
GROUP BY sold_by ORDER BY total_revenue DESC
|
||||||
|
`);
|
||||||
|
res.json(result.rows || result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching sales report:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch report" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/reports/margin-by-imei", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const result = await db.execute(sql`
|
||||||
|
SELECT d.id, d.brand, d.model, d.imei, d.condition,
|
||||||
|
CAST(d.purchase_price AS NUMERIC) as cost,
|
||||||
|
CAST(d.selling_price AS NUMERIC) as sale_price,
|
||||||
|
CAST(d.selling_price AS NUMERIC) - CAST(d.purchase_price AS NUMERIC) as margin,
|
||||||
|
CASE WHEN CAST(d.purchase_price AS NUMERIC) > 0
|
||||||
|
THEN ROUND(((CAST(d.selling_price AS NUMERIC) - CAST(d.purchase_price AS NUMERIC)) / CAST(d.purchase_price AS NUMERIC)) * 100, 2)
|
||||||
|
ELSE 0 END as margin_percent,
|
||||||
|
d.status
|
||||||
|
FROM mobile_devices d
|
||||||
|
WHERE d.purchase_price IS NOT NULL AND CAST(d.purchase_price AS NUMERIC) > 0
|
||||||
|
ORDER BY margin DESC
|
||||||
|
`);
|
||||||
|
res.json(result.rows || result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching margin report:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch report" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/reports/daily-cash", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { date } = req.query;
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
|
||||||
|
const targetDate = date ? String(date) : new Date().toISOString().split('T')[0];
|
||||||
|
const tenantFilter = sql`AND tenant_id = ${tenantId}`;
|
||||||
|
|
||||||
|
const summaryResult = await db.execute(sql`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)), 0) as total_sales,
|
||||||
|
COUNT(*) as sale_count,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'cash'), 0) as cash_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method IN ('credit','debit')), 0) as card_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'pix'), 0) as pix_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'combined'), 0) as combined_total
|
||||||
|
FROM pos_sales
|
||||||
|
WHERE status = 'completed' AND DATE(created_at) = ${targetDate}::date ${tenantFilter}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const movementsResult = await db.execute(sql`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CAST(amount AS NUMERIC)) FILTER (WHERE type = 'withdrawal'), 0) as withdrawals,
|
||||||
|
COALESCE(SUM(CAST(amount AS NUMERIC)) FILTER (WHERE type = 'reinforcement'), 0) as reinforcements
|
||||||
|
FROM pos_cash_movements
|
||||||
|
WHERE DATE(created_at) = ${targetDate}::date ${tenantFilter}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const salesResult = await db.execute(sql`
|
||||||
|
SELECT id, sale_number, customer_name, total_amount, payment_method, sold_by,
|
||||||
|
discount_amount, subtotal, created_at, notes
|
||||||
|
FROM pos_sales
|
||||||
|
WHERE status = 'completed' AND DATE(created_at) = ${targetDate}::date ${tenantFilter}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const bySellerResult = await db.execute(sql`
|
||||||
|
SELECT
|
||||||
|
sold_by,
|
||||||
|
COUNT(*) as sale_count,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)), 0) as total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'cash'), 0) as cash_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method IN ('credit','debit')), 0) as card_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'credit'), 0) as credit_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'debit'), 0) as debit_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'pix'), 0) as pix_total,
|
||||||
|
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'combined'), 0) as combined_total,
|
||||||
|
COALESCE(SUM(CAST(discount_amount AS NUMERIC)), 0) as total_discount
|
||||||
|
FROM pos_sales
|
||||||
|
WHERE status = 'completed' AND DATE(created_at) = ${targetDate}::date ${tenantFilter}
|
||||||
|
GROUP BY sold_by
|
||||||
|
ORDER BY total DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const summary = (summaryResult.rows as any[])?.[0] || {};
|
||||||
|
const movements = (movementsResult.rows as any[])?.[0] || {};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...summary,
|
||||||
|
withdrawals: movements.withdrawals || 0,
|
||||||
|
reinforcements: movements.reinforcements || 0,
|
||||||
|
sales: salesResult.rows || [],
|
||||||
|
bySeller: bySellerResult.rows || [],
|
||||||
|
date: targetDate
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching daily cash report:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch report" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/reports/stock-turnover", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const result = await db.execute(sql`
|
||||||
|
SELECT p.id, p.code, p.name, p.category,
|
||||||
|
CAST(p.stock_qty AS NUMERIC) as current_stock,
|
||||||
|
CAST(p.min_stock AS NUMERIC) as min_stock,
|
||||||
|
COALESCE((SELECT COUNT(*) FROM pos_sale_items psi WHERE psi.item_code = p.code AND psi.created_at >= CURRENT_DATE - INTERVAL '30 days'), 0) as sales_30d,
|
||||||
|
CASE WHEN CAST(p.stock_qty AS NUMERIC) > 0
|
||||||
|
THEN ROUND(COALESCE((SELECT COUNT(*) FROM pos_sale_items psi WHERE psi.item_code = p.code AND psi.created_at >= CURRENT_DATE - INTERVAL '30 days'), 0)::numeric / CAST(p.stock_qty AS NUMERIC), 2)
|
||||||
|
ELSE 0 END as turnover_ratio
|
||||||
|
FROM products p
|
||||||
|
WHERE p.status = 'active'
|
||||||
|
ORDER BY turnover_ratio DESC
|
||||||
|
LIMIT 50
|
||||||
|
`);
|
||||||
|
res.json(result.rows || result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stock turnover:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch report" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ========== POS SALES ==========
|
// ========== POS SALES ==========
|
||||||
router.get("/sales", async (req: Request, res: Response) => {
|
router.get("/sales", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -2078,8 +2518,19 @@ router.put("/transfers/:id/receive", async (req: Request, res: Response) => {
|
||||||
// ========== PDV PRODUCTS ==========
|
// ========== PDV PRODUCTS ==========
|
||||||
router.get("/pdv-products", async (req: Request, res: Response) => {
|
router.get("/pdv-products", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
const tenantId = (req.user as any)?.tenantId;
|
||||||
|
const conditions = [
|
||||||
|
eq(products.status, "active"),
|
||||||
|
or(
|
||||||
|
eq(products.trackingType, "none"),
|
||||||
|
isNull(products.trackingType)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
if (tenantId) {
|
||||||
|
conditions.push(eq(products.tenantId, tenantId));
|
||||||
|
}
|
||||||
const allProducts = await db.select().from(products)
|
const allProducts = await db.select().from(products)
|
||||||
.where(eq(products.status, "active"))
|
.where(and(...conditions))
|
||||||
.orderBy(asc(products.name));
|
.orderBy(asc(products.name));
|
||||||
res.json(allProducts);
|
res.json(allProducts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2898,7 +3349,7 @@ router.post("/evaluations/:id/approve-and-process", async (req: Request, res: Re
|
||||||
// 3. Record in IMEI history
|
// 3. Record in IMEI history
|
||||||
await db.insert(imeiHistory).values({
|
await db.insert(imeiHistory).values({
|
||||||
tenantId: evaluation.tenantId,
|
tenantId: evaluation.tenantId,
|
||||||
deviceId: null as any, // Will be created when OS is finalized
|
deviceId: null as any,
|
||||||
imei: evaluation.imei,
|
imei: evaluation.imei,
|
||||||
action: "trade_in_approved",
|
action: "trade_in_approved",
|
||||||
newStatus: "in_revision",
|
newStatus: "in_revision",
|
||||||
|
|
@ -2910,11 +3361,46 @@ router.post("/evaluations/:id/approve-and-process", async (req: Request, res: Re
|
||||||
createdByName: (req.user as any)?.name || "Sistema",
|
createdByName: (req.user as any)?.name || "Sistema",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. Generate customer credit if person and value are valid
|
||||||
|
let credit = null;
|
||||||
|
const personId = evaluation.personId;
|
||||||
|
const estimatedValueNum = evaluation.estimatedValue ? parseFloat(evaluation.estimatedValue) : 0;
|
||||||
|
if (personId && estimatedValueNum > 0) {
|
||||||
|
let customerCpf = null;
|
||||||
|
let customerName = evaluation.customerName || "Cliente";
|
||||||
|
const [person] = await db.select().from(persons)
|
||||||
|
.where(eq(persons.id, personId))
|
||||||
|
.limit(1);
|
||||||
|
if (person) {
|
||||||
|
customerCpf = person.cpfCnpj;
|
||||||
|
customerName = person.fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
[credit] = await db.insert(customerCredits).values({
|
||||||
|
storeId: evaluation.storeId || undefined,
|
||||||
|
personId: personId,
|
||||||
|
customerName,
|
||||||
|
customerCpf: customerCpf || undefined,
|
||||||
|
amount: evaluation.estimatedValue!,
|
||||||
|
remainingAmount: evaluation.estimatedValue!,
|
||||||
|
origin: "trade_in",
|
||||||
|
originId: evaluation.id,
|
||||||
|
description: `Trade-In: ${evaluation.brand} ${evaluation.model} (IMEI: ${evaluation.imei})`,
|
||||||
|
status: "active",
|
||||||
|
createdBy: (req.user as any)?.id
|
||||||
|
}).returning();
|
||||||
|
|
||||||
|
await db.update(deviceEvaluations)
|
||||||
|
.set({ creditGenerated: true, creditId: credit.id })
|
||||||
|
.where(eq(deviceEvaluations.id, evaluationId));
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Trade-In aprovado e O.S. Interna criada com sucesso",
|
message: "Trade-In aprovado e O.S. Interna criada com sucesso",
|
||||||
evaluation: { ...evaluation, status: "approved" },
|
evaluation: { ...evaluation, status: "approved" },
|
||||||
serviceOrder,
|
serviceOrder,
|
||||||
|
credit,
|
||||||
nextStep: "Realize a revisão/manutenção do dispositivo e finalize a O.S. para entrada no estoque"
|
nextStep: "Realize a revisão/manutenção do dispositivo e finalize a O.S. para entrada no estoque"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -4604,5 +5090,129 @@ router.post("/customer-credits/:creditId/use", async (req: Request, res: Respons
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== PLUS ERP SYNC ROUTES ==========
|
||||||
|
|
||||||
|
const plusSync = retailPlusSyncService;
|
||||||
|
|
||||||
|
router.get("/plus/status", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const status = await plusSync.getPlusStatus();
|
||||||
|
res.json(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking Plus status:", error);
|
||||||
|
res.status(500).json({ error: "Failed to check Plus connection" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/sync/customers", requireModule("plus"), async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { tenantId, empresaId } = req.body;
|
||||||
|
if (!tenantId) return res.status(400).json({ error: "tenantId required" });
|
||||||
|
const result = await plusSync.syncAllPersonsToPlus(tenantId, empresaId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error syncing customers to Plus:", error);
|
||||||
|
res.status(500).json({ error: "Failed to sync customers" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/sync/sales", requireModule("plus"), async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { saleId } = req.body;
|
||||||
|
if (!saleId) return res.status(400).json({ error: "saleId required" });
|
||||||
|
const result = await plusSync.syncSaleToPlus(saleId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error syncing sale to Plus:", error);
|
||||||
|
res.status(500).json({ error: "Failed to sync sale" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/sync/nfe", requireModule("plus"), async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { saleId, tipo } = req.body;
|
||||||
|
if (!saleId) return res.status(400).json({ error: "saleId required" });
|
||||||
|
const result = await plusSync.emitirNFeSale(saleId, tipo || "nfce");
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error emitting NF-e:", error);
|
||||||
|
res.status(500).json({ error: "Failed to emit NF-e" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/import/customers", requireModule("plus"), async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { tenantId, empresaId } = req.body;
|
||||||
|
if (!tenantId) return res.status(400).json({ error: "tenantId required" });
|
||||||
|
const result = await plusSync.importClientesFromPlus(tenantId, empresaId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error importing customers from Plus:", error);
|
||||||
|
res.status(500).json({ error: "Failed to import customers" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/import/products", requireModule("plus"), async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { tenantId, empresaId } = req.body;
|
||||||
|
if (!tenantId) return res.status(400).json({ error: "tenantId required" });
|
||||||
|
const result = await plusSync.importProdutosFromPlus(tenantId, empresaId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error importing products from Plus:", error);
|
||||||
|
res.status(500).json({ error: "Failed to import products" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/plus/empresas", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tenantId = parseInt(req.query.tenantId as string) || 1;
|
||||||
|
const empresas = await db.select().from(tenantEmpresas)
|
||||||
|
.where(and(eq(tenantEmpresas.tenantId, tenantId), eq(tenantEmpresas.status, "active")));
|
||||||
|
res.json(empresas);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching empresas:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch empresas" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/empresas", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tenantId = parseInt(req.body.tenantId as string) || 1;
|
||||||
|
const { razaoSocial, nomeFantasia, cnpj, tipo } = req.body;
|
||||||
|
if (!razaoSocial || !cnpj) return res.status(400).json({ error: "razaoSocial and cnpj required" });
|
||||||
|
const [empresa] = await db.insert(tenantEmpresas).values({
|
||||||
|
tenantId,
|
||||||
|
razaoSocial,
|
||||||
|
nomeFantasia: nomeFantasia || razaoSocial,
|
||||||
|
cnpj,
|
||||||
|
tipo: tipo || "matriz",
|
||||||
|
status: "active",
|
||||||
|
}).returning();
|
||||||
|
res.json(empresa);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating empresa:", error);
|
||||||
|
res.status(500).json({ error: "Failed to create empresa" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/plus/empresas/:id/bind", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const empresaLocalId = parseInt(req.params.id);
|
||||||
|
const { plusEmpresaId } = req.body;
|
||||||
|
if (!plusEmpresaId) return res.status(400).json({ error: "plusEmpresaId required" });
|
||||||
|
|
||||||
|
const [updated] = await db.update(tenantEmpresas)
|
||||||
|
.set({ plusEmpresaId })
|
||||||
|
.where(eq(tenantEmpresas.id, empresaLocalId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
res.json(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error binding empresa to Plus:", error);
|
||||||
|
res.status(500).json({ error: "Failed to bind empresa" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { insertApplicationSchema } from "@shared/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { setupAuth } from "./auth";
|
import { setupAuth } from "./auth";
|
||||||
import { registerChatRoutes } from "./replit_integrations/chat";
|
import { registerChatRoutes } from "./replit_integrations/chat";
|
||||||
import { registerErpRoutes } from "./erp/routes";
|
import { registerSoeRoutes, registerErpRoutes } from "./erp/routes";
|
||||||
import { registerInternalChatRoutes } from "./chat/routes";
|
import { registerInternalChatRoutes } from "./chat/routes";
|
||||||
import { setupChatSocket } from "./chat/socket";
|
import { setupChatSocket } from "./chat/socket";
|
||||||
import { setupCommunitySocket } from "./communities/socket";
|
import { setupCommunitySocket } from "./communities/socket";
|
||||||
|
|
@ -57,6 +57,7 @@ import autonomousRoutes from "./autonomous/routes";
|
||||||
import blackboardRoutes from "./blackboard/routes";
|
import blackboardRoutes from "./blackboard/routes";
|
||||||
import pipelineRoutes from "./blackboard/pipelineRoutes";
|
import pipelineRoutes from "./blackboard/pipelineRoutes";
|
||||||
import { startAllAgents } from "./blackboard/agents";
|
import { startAllAgents } from "./blackboard/agents";
|
||||||
|
import { loadModuleRoutes } from "./modules/loader";
|
||||||
|
|
||||||
export async function registerRoutes(
|
export async function registerRoutes(
|
||||||
httpServer: Server,
|
httpServer: Server,
|
||||||
|
|
@ -72,7 +73,7 @@ export async function registerRoutes(
|
||||||
setupMetabaseProxy(app);
|
setupMetabaseProxy(app);
|
||||||
|
|
||||||
registerChatRoutes(app);
|
registerChatRoutes(app);
|
||||||
registerErpRoutes(app);
|
registerSoeRoutes(app);
|
||||||
registerInternalChatRoutes(app);
|
registerInternalChatRoutes(app);
|
||||||
setupChatSocket(httpServer);
|
setupChatSocket(httpServer);
|
||||||
setupCommunitySocket(httpServer);
|
setupCommunitySocket(httpServer);
|
||||||
|
|
@ -120,6 +121,9 @@ export async function registerRoutes(
|
||||||
app.use("/api/blackboard", blackboardRoutes);
|
app.use("/api/blackboard", blackboardRoutes);
|
||||||
app.use("/api/xos/pipeline", pipelineRoutes);
|
app.use("/api/xos/pipeline", pipelineRoutes);
|
||||||
|
|
||||||
|
// Auto-loader de módulos criados pelo Dev Center
|
||||||
|
await loadModuleRoutes(app);
|
||||||
|
|
||||||
// Iniciar os 6 agentes do Blackboard
|
// Iniciar os 6 agentes do Blackboard
|
||||||
startAllAgents();
|
startAllAgents();
|
||||||
|
|
||||||
|
|
|
||||||
112
shared/schema.ts
112
shared/schema.ts
|
|
@ -3,6 +3,9 @@ import { pgTable, text, varchar, primaryKey, serial, integer, timestamp, numeric
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Re-exportar schemas modulares criados pelo Dev Center
|
||||||
|
export * from "./schemas/index";
|
||||||
|
|
||||||
export const users = pgTable("users", {
|
export const users = pgTable("users", {
|
||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
username: text("username").notNull().unique(),
|
username: text("username").notNull().unique(),
|
||||||
|
|
@ -832,6 +835,14 @@ export type TenantFeatures = {
|
||||||
biblioteca: boolean;
|
biblioteca: boolean;
|
||||||
bibliotecaPublish: boolean;
|
bibliotecaPublish: boolean;
|
||||||
suporteN3: boolean;
|
suporteN3: boolean;
|
||||||
|
retail: boolean;
|
||||||
|
plus: boolean;
|
||||||
|
fisco: boolean;
|
||||||
|
cockpit: boolean;
|
||||||
|
compass: boolean;
|
||||||
|
production: boolean;
|
||||||
|
support: boolean;
|
||||||
|
xosCrm: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tenants (Master/Parceiros/Clientes)
|
// Tenants (Master/Parceiros/Clientes)
|
||||||
|
|
@ -863,6 +874,49 @@ export const tenants = pgTable("tenants", {
|
||||||
// Contato comercial
|
// Contato comercial
|
||||||
commercialContact: text("commercial_contact"),
|
commercialContact: text("commercial_contact"),
|
||||||
commercialPhone: text("commercial_phone"),
|
commercialPhone: text("commercial_phone"),
|
||||||
|
// Dados empresariais (unificado com CRM)
|
||||||
|
cnpj: text("cnpj"),
|
||||||
|
tradeName: text("trade_name"),
|
||||||
|
address: text("address"),
|
||||||
|
city: text("city"),
|
||||||
|
state: text("state"),
|
||||||
|
segment: text("segment"),
|
||||||
|
notes: text("notes"),
|
||||||
|
source: text("source"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empresas do Tenant (Matriz + Filiais)
|
||||||
|
export const tenantEmpresas = pgTable("tenant_empresas", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
tenantId: integer("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
razaoSocial: text("razao_social").notNull(),
|
||||||
|
nomeFantasia: text("nome_fantasia"),
|
||||||
|
cnpj: text("cnpj").notNull(),
|
||||||
|
ie: text("ie"),
|
||||||
|
im: text("im"),
|
||||||
|
email: text("email"),
|
||||||
|
phone: text("phone"),
|
||||||
|
tipo: text("tipo").default("filial"), // matriz, filial
|
||||||
|
status: text("status").default("active"), // active, inactive
|
||||||
|
cep: text("cep"),
|
||||||
|
logradouro: text("logradouro"),
|
||||||
|
numero: text("numero"),
|
||||||
|
complemento: text("complemento"),
|
||||||
|
bairro: text("bairro"),
|
||||||
|
cidade: text("cidade"),
|
||||||
|
uf: text("uf"),
|
||||||
|
codigoIbge: text("codigo_ibge"),
|
||||||
|
// Fiscal
|
||||||
|
regimeTributario: text("regime_tributario"), // simples, presumido, real
|
||||||
|
certificadoDigitalId: integer("certificado_digital_id"),
|
||||||
|
ambienteFiscal: text("ambiente_fiscal").default("homologacao"), // producao, homologacao
|
||||||
|
serieNfe: integer("serie_nfe").default(1),
|
||||||
|
serieNfce: integer("serie_nfce").default(1),
|
||||||
|
// Plus ERP link
|
||||||
|
plusEmpresaId: integer("plus_empresa_id"),
|
||||||
|
// Metadata
|
||||||
|
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tenant Users (Membership)
|
// Tenant Users (Membership)
|
||||||
|
|
@ -927,6 +981,7 @@ export const insertTenantUserSchema = createInsertSchema(tenantUsers).omit({ id:
|
||||||
export const insertTenantPlanSchema = createInsertSchema(tenantPlans).omit({ id: true, createdAt: true });
|
export const insertTenantPlanSchema = createInsertSchema(tenantPlans).omit({ id: true, createdAt: true });
|
||||||
export const insertPartnerClientSchema = createInsertSchema(partnerClients).omit({ id: true, startedAt: true });
|
export const insertPartnerClientSchema = createInsertSchema(partnerClients).omit({ id: true, startedAt: true });
|
||||||
export const insertPartnerCommissionSchema = createInsertSchema(partnerCommissions).omit({ id: true, createdAt: true });
|
export const insertPartnerCommissionSchema = createInsertSchema(partnerCommissions).omit({ id: true, createdAt: true });
|
||||||
|
export const insertTenantEmpresaSchema = createInsertSchema(tenantEmpresas).omit({ id: true, createdAt: true, updatedAt: true });
|
||||||
|
|
||||||
export type Tenant = typeof tenants.$inferSelect;
|
export type Tenant = typeof tenants.$inferSelect;
|
||||||
export type InsertTenant = z.infer<typeof insertTenantSchema>;
|
export type InsertTenant = z.infer<typeof insertTenantSchema>;
|
||||||
|
|
@ -938,6 +993,8 @@ export type PartnerClient = typeof partnerClients.$inferSelect;
|
||||||
export type InsertPartnerClient = z.infer<typeof insertPartnerClientSchema>;
|
export type InsertPartnerClient = z.infer<typeof insertPartnerClientSchema>;
|
||||||
export type PartnerCommission = typeof partnerCommissions.$inferSelect;
|
export type PartnerCommission = typeof partnerCommissions.$inferSelect;
|
||||||
export type InsertPartnerCommission = z.infer<typeof insertPartnerCommissionSchema>;
|
export type InsertPartnerCommission = z.infer<typeof insertPartnerCommissionSchema>;
|
||||||
|
export type TenantEmpresa = typeof tenantEmpresas.$inferSelect;
|
||||||
|
export type InsertTenantEmpresa = z.infer<typeof insertTenantEmpresaSchema>;
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// PROCESS COMPASS - CONSULTING BACK-OFFICE
|
// PROCESS COMPASS - CONSULTING BACK-OFFICE
|
||||||
|
|
@ -5078,6 +5135,7 @@ export const serviceOrders = pgTable("service_orders", {
|
||||||
export const serviceOrderItems = pgTable("service_order_items", {
|
export const serviceOrderItems = pgTable("service_order_items", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
serviceOrderId: integer("service_order_id").references(() => serviceOrders.id).notNull(),
|
serviceOrderId: integer("service_order_id").references(() => serviceOrders.id).notNull(),
|
||||||
|
productId: integer("product_id").references(() => products.id),
|
||||||
itemType: varchar("item_type", { length: 20 }).default("part"), // part, labor, accessory
|
itemType: varchar("item_type", { length: 20 }).default("part"), // part, labor, accessory
|
||||||
itemCode: varchar("item_code", { length: 50 }),
|
itemCode: varchar("item_code", { length: 50 }),
|
||||||
itemName: varchar("item_name", { length: 200 }).notNull(),
|
itemName: varchar("item_name", { length: 200 }).notNull(),
|
||||||
|
|
@ -5137,6 +5195,12 @@ export const posSales = pgTable("pos_sales", {
|
||||||
status: varchar("status", { length: 20 }).default("completed"), // pending, completed, cancelled, refunded
|
status: varchar("status", { length: 20 }).default("completed"), // pending, completed, cancelled, refunded
|
||||||
soldBy: varchar("sold_by"),
|
soldBy: varchar("sold_by"),
|
||||||
notes: text("notes"),
|
notes: text("notes"),
|
||||||
|
plusVendaId: integer("plus_venda_id"),
|
||||||
|
plusNfeChave: varchar("plus_nfe_chave", { length: 60 }),
|
||||||
|
plusSyncStatus: varchar("plus_sync_status", { length: 20 }).default("pending"), // pending, synced, error, not_applicable
|
||||||
|
plusSyncError: text("plus_sync_error"),
|
||||||
|
plusSyncedAt: timestamp("plus_synced_at"),
|
||||||
|
empresaId: integer("empresa_id").references(() => tenantEmpresas.id),
|
||||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -5900,6 +5964,51 @@ export const insertTradeInTransferDocumentSchema = createInsertSchema(tradeInTra
|
||||||
export type TradeInTransferDocument = typeof tradeInTransferDocuments.$inferSelect;
|
export type TradeInTransferDocument = typeof tradeInTransferDocuments.$inferSelect;
|
||||||
export type InsertTradeInTransferDocument = z.infer<typeof insertTradeInTransferDocumentSchema>;
|
export type InsertTradeInTransferDocument = z.infer<typeof insertTradeInTransferDocumentSchema>;
|
||||||
|
|
||||||
|
// POS Cash Movements - Sangria e Reforço de Caixa
|
||||||
|
export const posCashMovements = pgTable("pos_cash_movements", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
tenantId: integer("tenant_id").references(() => tenants.id),
|
||||||
|
sessionId: integer("session_id").references(() => posSessions.id).notNull(),
|
||||||
|
storeId: integer("store_id").references(() => retailStores.id).notNull(),
|
||||||
|
type: varchar("type", { length: 20 }).notNull(), // withdrawal (sangria), reinforcement (reforço)
|
||||||
|
amount: numeric("amount", { precision: 12, scale: 2 }).notNull(),
|
||||||
|
reason: text("reason"),
|
||||||
|
performedBy: varchar("performed_by"),
|
||||||
|
performedByName: varchar("performed_by_name", { length: 200 }),
|
||||||
|
authorizedBy: varchar("authorized_by"),
|
||||||
|
authorizedByName: varchar("authorized_by_name", { length: 200 }),
|
||||||
|
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertPosCashMovementSchema = createInsertSchema(posCashMovements).omit({ id: true, createdAt: true });
|
||||||
|
export type PosCashMovement = typeof posCashMovements.$inferSelect;
|
||||||
|
export type InsertPosCashMovement = z.infer<typeof insertPosCashMovementSchema>;
|
||||||
|
|
||||||
|
// Service Warranties - Garantias de Serviço vinculadas à OS e IMEI
|
||||||
|
export const serviceWarranties = pgTable("service_warranties", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
tenantId: integer("tenant_id").references(() => tenants.id),
|
||||||
|
storeId: integer("store_id").references(() => retailStores.id),
|
||||||
|
serviceOrderId: integer("service_order_id").references(() => serviceOrders.id).notNull(),
|
||||||
|
deviceId: integer("device_id").references(() => mobileDevices.id),
|
||||||
|
imei: varchar("imei", { length: 20 }),
|
||||||
|
serviceType: varchar("service_type", { length: 50 }).notNull(),
|
||||||
|
warrantyDays: integer("warranty_days").notNull(),
|
||||||
|
startDate: date("start_date").notNull(),
|
||||||
|
endDate: date("end_date").notNull(),
|
||||||
|
customerName: varchar("customer_name", { length: 200 }),
|
||||||
|
customerPhone: varchar("customer_phone", { length: 20 }),
|
||||||
|
description: text("description"),
|
||||||
|
status: varchar("status", { length: 20 }).default("active"), // active, expired, claimed, voided
|
||||||
|
claimedAt: timestamp("claimed_at"),
|
||||||
|
claimNotes: text("claim_notes"),
|
||||||
|
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertServiceWarrantySchema = createInsertSchema(serviceWarranties).omit({ id: true, createdAt: true });
|
||||||
|
export type ServiceWarranty = typeof serviceWarranties.$inferSelect;
|
||||||
|
export type InsertServiceWarranty = z.infer<typeof insertServiceWarrantySchema>;
|
||||||
|
|
||||||
// Customer Credits - Créditos de Cliente (Trade-In, Devoluções, etc.)
|
// Customer Credits - Créditos de Cliente (Trade-In, Devoluções, etc.)
|
||||||
export const customerCredits = pgTable("customer_credits", {
|
export const customerCredits = pgTable("customer_credits", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
|
|
@ -5956,6 +6065,9 @@ export const persons = pgTable("persons", {
|
||||||
erpnextCustomerId: varchar("erpnext_customer_id", { length: 140 }),
|
erpnextCustomerId: varchar("erpnext_customer_id", { length: 140 }),
|
||||||
erpnextSupplierId: varchar("erpnext_supplier_id", { length: 140 }),
|
erpnextSupplierId: varchar("erpnext_supplier_id", { length: 140 }),
|
||||||
erpnextEmployeeId: varchar("erpnext_employee_id", { length: 140 }),
|
erpnextEmployeeId: varchar("erpnext_employee_id", { length: 140 }),
|
||||||
|
// Plus Sync Fields
|
||||||
|
plusClienteId: integer("plus_cliente_id"),
|
||||||
|
plusFornecedorId: integer("plus_fornecedor_id"),
|
||||||
lastSyncAt: timestamp("last_sync_at"),
|
lastSyncAt: timestamp("last_sync_at"),
|
||||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue