merge: une Servidor local + origin/Servidor priorizando local
- Mantém Fase 4 Sprint 1+2 do local (OpenClaw backend + frontend) - Incorpora do remoto: Fase 3 MiroFlow, Fase 4 melhorias, Fase 5 Automation Fabric - Resolve conflito openclaw~origin_Servidor (mantido local) - Histórias unidas com --allow-unrelated-histories -X ours Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
acf375a1c3
|
|
@ -0,0 +1,38 @@
|
|||
uploads = []
|
||||
generated = []
|
||||
|
||||
[[outputs]]
|
||||
id = "28NDwt4UT3HPglsSw-IQk"
|
||||
uri = "file://MAPA_MICROSERVICOS_COOLIFY.md"
|
||||
type = "text"
|
||||
title = "Mapa de Microserviços - Arcádia Suite para Coolify/Docker"
|
||||
|
||||
[[outputs]]
|
||||
id = "YTVCn39myPk5kzF8smbQz"
|
||||
uri = "file://MAPA_SOE_ARCADIA.md"
|
||||
type = "text"
|
||||
title = "Mapa SOE — Sistema Operacional Empresarial (Deploy)"
|
||||
|
||||
[[outputs]]
|
||||
id = "pQjx1X6m9hp2j-sZa0_Rg"
|
||||
uri = "file://MAPA_BI_ARCADIA.md"
|
||||
type = "text"
|
||||
title = "Mapa BI - Business Intelligence da Arcádia Suite (com Metabase)"
|
||||
|
||||
[[outputs]]
|
||||
id = "gpyYTqB0ytdU1zAJij6KK"
|
||||
uri = "file://MAPA_CRM_ARCADIA.md"
|
||||
type = "text"
|
||||
title = "Mapa CRM - Sistema de Relacionamento da Arcádia Suite"
|
||||
|
||||
[[outputs]]
|
||||
id = "zal1KfcZdP7OqKEXu67Pp"
|
||||
uri = "file://MAPA_TENANTS_ARCADIA.md"
|
||||
type = "text"
|
||||
title = "Mapa Multi-Tenant - Gestão de Tenants da Arcádia Suite"
|
||||
|
||||
[[outputs]]
|
||||
id = "GCMgcBAHQ1aIDjHxoXniQ"
|
||||
uri = "file://MAPA_AUTOMACOES_ARCADIA.md"
|
||||
type = "text"
|
||||
title = "Mapa Automações - Motor de Automação da Arcádia Suite"
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
metabase/
|
||||
attached_assets/
|
||||
.git/
|
||||
.cache/
|
||||
.upm/
|
||||
.config/
|
||||
.local/
|
||||
.pythonlibs/
|
||||
uploads/
|
||||
plus/
|
||||
whatsapp-sessions/
|
||||
migrations/
|
||||
docs/
|
||||
scripts/
|
||||
server/python/
|
||||
python-service/
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.sql
|
||||
*.log
|
||||
*.docx
|
||||
plus_backup_*/
|
||||
RELATORIO_TECNICO_RETAIL.md
|
||||
MAPA_GERAL_RETAIL.md
|
||||
PLANO_EVOLUCAO_ARCADIA.docx
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
node_modules
|
||||
dist
|
||||
.git
|
||||
*.zip
|
||||
*.log
|
||||
.env
|
||||
.replit
|
||||
replit.nix
|
||||
.cache
|
||||
.config
|
||||
.upm
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[submodule "server/modules/miroflow"]
|
||||
path = server/modules/miroflow
|
||||
url = https://github.com/MiroMindAI/MiroFlow.git
|
||||
[submodule "server/modules/openclaw"]
|
||||
path = server/modules/openclaw
|
||||
url = https://github.com/miaoxworld/OpenClawInstaller.git
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
**Fase:** 4
|
||||
**Data Início:** 2026-03-26
|
||||
**Prazo:** 2026-04-08 (Semanas 7-8)
|
||||
**Status:** 🔄 Em Planejamento
|
||||
**Status:** 🔄 Sprint 3 Pendente (Sprint 1 e 2 concluídos)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -92,9 +92,9 @@ client/src/components/openclaw/
|
|||
|
||||
## 4. TASKS DETALHADAS
|
||||
|
||||
### SPRINT 1 (26-27/03): PatternDetector Backend
|
||||
### SPRINT 1 (26-27/03): PatternDetector Backend — ✅ COMPLETO
|
||||
|
||||
#### Task 1.1: Criar PatternDetector.ts
|
||||
#### Task 1.1: Criar PatternDetector.ts ✅
|
||||
**Arquivo:** `server/modules/openclaw/PatternDetector.ts`
|
||||
**Objetivo:** Analisar eventos de usuário e detectar padrões
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ export class PatternDetector {
|
|||
**Saída:** Pattern objects armazenados em Neo4j + Socket.IO event
|
||||
**Dependência:** Learning API funcionando ✅
|
||||
|
||||
#### Task 1.2: Criar OpenClawEngine.ts
|
||||
#### Task 1.2: Criar OpenClawEngine.ts ✅
|
||||
**Arquivo:** `server/modules/openclaw/OpenClawEngine.ts`
|
||||
**Objetivo:** Orquestração central de OpenClaw
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ export class OpenClawEngine {
|
|||
**Saída:** Sugestões + Skills gerados
|
||||
**Evento:** Socket.IO broadcast `/openclaw/pattern-detected`
|
||||
|
||||
#### Task 1.3: Criar SkillEmergence.ts
|
||||
#### Task 1.3: Criar SkillEmergence.ts ✅
|
||||
**Arquivo:** `server/modules/openclaw/SkillEmergence.ts`
|
||||
**Objetivo:** Integração com Blackboard para codegen
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ export class SkillEmergence {
|
|||
**Integração:** POST `/api/blackboard/generate-skill`
|
||||
**Database:** skills table (com status: 'draft')
|
||||
|
||||
#### Task 1.4: Criar rotas OpenClaw
|
||||
#### Task 1.4: Criar rotas OpenClaw ✅
|
||||
**Arquivo:** `server/modules/openclaw/routes.ts`
|
||||
**Rotas:**
|
||||
```
|
||||
|
|
@ -168,7 +168,7 @@ POST /api/openclaw/confirm-skill ← Usuário confirma sugestão
|
|||
GET /api/openclaw/suggestions ← Histórico de sugestões
|
||||
```
|
||||
|
||||
#### Task 1.5: Integrar em server/index.ts
|
||||
#### Task 1.5: Integrar em server/index.ts ✅
|
||||
**Arquivo:** `server/index.ts`
|
||||
**Ação:** Importar rotas OpenClaw e carregá-las
|
||||
|
||||
|
|
@ -177,9 +177,9 @@ import openclawRoutes from "./modules/openclaw/routes";
|
|||
app.use("/api/openclaw", openclawRoutes);
|
||||
```
|
||||
|
||||
### SPRINT 2 (28-29/03): Frontend Widgets
|
||||
### SPRINT 2 (28-29/03): Frontend Widgets — ✅ COMPLETO (commit 5231166)
|
||||
|
||||
#### Task 2.1: Criar OpenClawWidget.tsx
|
||||
#### Task 2.1: Criar OpenClawWidget.tsx ✅
|
||||
**Arquivo:** `client/src/components/openclaw/OpenClawWidget.tsx`
|
||||
**Objetivo:** Widget flutuante que recebe notificações de padrões
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ export function OpenClawWidget() {
|
|||
**Socket.IO:** Escuta evento `/openclaw/pattern-detected`
|
||||
**Trigger:** Modal SkillSuggestion
|
||||
|
||||
#### Task 2.2: Criar SkillSuggestion.tsx
|
||||
#### Task 2.2: Criar SkillSuggestion.tsx ✅
|
||||
**Arquivo:** `client/src/components/openclaw/SkillSuggestion.tsx`
|
||||
**Objetivo:** Modal com preview da skill sugerida
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ export function SkillSuggestion({ pattern, suggestion }) {
|
|||
**Integração:** Chama `POST /api/openclaw/confirm-skill`
|
||||
**Feedback:** Toast notification
|
||||
|
||||
#### Task 2.3: Hook useAgentEmergence.ts
|
||||
#### Task 2.3: Hook useAgentEmergence.ts ✅
|
||||
**Arquivo:** `client/src/hooks/useAgentEmergence.ts`
|
||||
**Objetivo:** Lógica de emergência de agentes
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ export function useAgentEmergence(userId: string) {
|
|||
}
|
||||
```
|
||||
|
||||
#### Task 2.4: Integrar Widget em layout principal
|
||||
#### Task 2.4: Integrar Widget em layout principal ✅
|
||||
**Arquivo:** `client/src/App.tsx` ou `client/src/layouts/MainLayout.tsx`
|
||||
**Ação:** Adicionar OpenClawWidget ao layout persistente
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ export default function App() {
|
|||
}
|
||||
```
|
||||
|
||||
### SPRINT 3 (30-31/03): Configuração e Integração
|
||||
### SPRINT 3 (30-31/03): Configuração e Integração — ⏳ PRÓXIMO
|
||||
|
||||
#### Task 3.1: Criar arcadia.config.yaml
|
||||
**Arquivo:** `server/modules/openclaw/config/arcadia.config.yaml`
|
||||
|
|
@ -402,11 +402,13 @@ CREATE INDEX idx_detected_patterns_user_tenant
|
|||
|
||||
| Data | Sprint | Tasks | Status |
|
||||
|------|--------|-------|--------|
|
||||
| 26-27/03 | 1 | PatternDetector backend | ⏳ Próximo |
|
||||
| 28-29/03 | 2 | OpenClawWidget frontend | ⏳ Próximo |
|
||||
| 30-31/03 | 3 | Configuração + testes | ⏳ Próximo |
|
||||
| 26-27/03 | 1 | PatternDetector backend | ✅ Completo |
|
||||
| 26/03 | 2 | OpenClawWidget frontend | ✅ Completo (commit 5231166) |
|
||||
| 26/03 | 3 | Migration DB + testes | ⏳ Próximo |
|
||||
| 01-08/04 | Buffer | Refinamento, testes E2E | ⏳ Próximo |
|
||||
|
||||
**Nota:** Trabalho em `/opt/arcadia_merged/` (diretório canônico do projeto).
|
||||
|
||||
---
|
||||
|
||||
## 6. CRITÉRIOS DE SUCESSO
|
||||
|
|
@ -461,11 +463,10 @@ docs(openclaw): Documentação e testes
|
|||
|
||||
## 9. PRÓXIMAS AÇÕES
|
||||
|
||||
1. ✅ **Hoje (26/03):** Aprovar este plano
|
||||
2. ⏳ **Amanhã (27/03):** Começar Task 1.1 (PatternDetector.ts)
|
||||
3. ⏳ **28/03:** Começar Sprint 2 (Frontend)
|
||||
4. ⏳ **30/03:** Sprint 3 (Config + testes)
|
||||
5. ⏳ **01/04:** Refinamento e E2E tests
|
||||
1. ✅ **26/03:** Sprint 1 concluído (PatternDetector, OpenClawEngine, SkillEmergence, routes, config)
|
||||
2. ✅ **26/03:** Sprint 2 concluído (OpenClawWidget, SkillSuggestion, useAgentEmergence, App.tsx — commit 5231166)
|
||||
3. ⏳ **Agora:** Sprint 3 — migration SQL + vitest + testes unitários em `/opt/arcadia_merged/`
|
||||
4. ⏳ **01-08/04:** Refinamento e testes E2E
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
# Roadmap: Arcádia Agentic Suite
|
||||
|
||||
## Overview
|
||||
|
||||
Evolução do Arcádia Suite de ERP tradicional para Sistema Agêntico Orientado a Objetos. Skills são objetos reutilizáveis (POO), Agentes instanciam Skills, Automações são composições orquestradas, Dev Center é a fábrica de agentes.
|
||||
|
||||
## Phases
|
||||
|
||||
- [x] **Phase 1: Fundação** - Infraestrutura base: submodules MiroFlow/OpenClaw, tabelas skills, Neo4j, ReferenceParser
|
||||
- [x] **Phase 2: Skills Engine** - Skills criáveis e executáveis com editor Monaco e Marketplace
|
||||
- [x] **Phase 3: MiroFlow Embutido** - Análises científicas via agentes especializados + bridge Superset
|
||||
- [ ] **Phase 4: OpenClaw Embutido** - Skills emergentes com detecção de padrões
|
||||
- [ ] **Phase 5: Automation Fabric** - Automações unificadas (XOS + Central)
|
||||
- [ ] **Phase 6: Dev Center Completo** - Fábrica de agentes: Design → Assemble → Deploy
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Fundação
|
||||
**Goal**: Infraestrutura base funcionando com submodules, banco, KG e parser de referências
|
||||
**Depends on**: Nothing
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Submodules MiroFlow e OpenClaw clonados e inicializados
|
||||
2. Tabelas arcadia_skills e skill_executions existem no banco
|
||||
3. Neo4j rodando via docker-compose
|
||||
4. ReferenceParser parseia referências /skill/, /kg/, /file/ etc.
|
||||
|
||||
Plans:
|
||||
- [x] 01-01: Submodules MiroFlow + OpenClaw
|
||||
- [x] 01-02: Schema tabelas skills + migration
|
||||
- [x] 01-03: Neo4j docker-compose + ReferenceParser
|
||||
|
||||
### Phase 2: Skills Engine
|
||||
**Goal**: Skills criáveis, editáveis e executáveis com marketplace
|
||||
**Depends on**: Phase 1
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. SkillEngine.ts suporta herança, composição e polimorfismo
|
||||
2. API REST /skills CRUD funcionando
|
||||
3. Editor Monaco com autocomplete de referências (/)
|
||||
4. Skill Marketplace lista e filtra skills disponíveis
|
||||
5. Versionamento Git-like de skills implementado
|
||||
|
||||
Plans:
|
||||
- [x] 02-01: SkillEngine + API REST
|
||||
- [x] 02-02: Editor Monaco + autocomplete + rota /skills
|
||||
- [x] 02-03: Skill Marketplace (Biblioteca)
|
||||
- [x] 02-04: Versionamento Git-like de skills
|
||||
|
||||
### Phase 3: MiroFlow Embutido
|
||||
**Goal**: Análises científicas disponíveis via agentes especializados integrados ao Superset
|
||||
**Depends on**: Phase 2
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. MiroFlow configurado para Ollama local com modelos até 14B
|
||||
2. Agente Statistician analisa dados SQL com deepseek-r1:14b
|
||||
3. Agente Fiscal Auditor valida NFe/SPED com deepseek-r1:14b
|
||||
4. Agente Researcher consulta KG com llama3.1:8b
|
||||
5. Endpoint POST /api/miroflow/analyze retorna análise estruturada
|
||||
6. MiroFlowControl.tsx toggle "Modo Científico" aparece no Superset
|
||||
7. Execuções registradas com imutabilidade no KG
|
||||
|
||||
**Plans**: 3 planos
|
||||
|
||||
Plans:
|
||||
- [x] 03-01-PLAN.md — Setup (ollama pull llama3.1:8b) + miroflow_service.py FastAPI porta 8006 com 3 agentes
|
||||
- [x] 03-02-PLAN.md — Node bridge (engine-proxy.ts + routes.ts) + KG logging SHA-256
|
||||
- [x] 03-03-PLAN.md — Frontend MiroFlowControl.tsx + tab "Científico" em BiWorkspace.tsx
|
||||
|
||||
### Phase 4: OpenClaw Embutido
|
||||
**Goal**: Skills emergentes criadas automaticamente a partir de padrões detectados
|
||||
**Depends on**: Phase 3
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. PatternDetector detecta padrões (min 3 ocorrências, 30 dias, 80% confiança)
|
||||
2. Skills emergentes criadas como DRAFT aguardando aprovação
|
||||
3. Widget flutuante notifica usuário de sugestões
|
||||
4. Fluxo completo: Padrão → DRAFT → Dev Center aprovação
|
||||
**Plans**: TBD
|
||||
|
||||
### Phase 5: Automation Fabric
|
||||
**Goal**: Automações unificadas sob runtime único substituindo XOS + Central
|
||||
**Depends on**: Phase 4
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. 5 runtimes funcionando: WorkflowEngine, RuleEngine, AgentExecutor, ScheduleEngine, EventEngine
|
||||
2. Automações existentes do XOS migradas sem perda de dados
|
||||
3. Automações existentes do /automations Central migradas
|
||||
4. AutomationCenter.tsx lista todas as automações unificadas
|
||||
**Plans**: TBD
|
||||
|
||||
### Phase 6: Dev Center Completo
|
||||
**Goal**: Fábrica completa de agentes: Design → Assemble → Deploy
|
||||
**Depends on**: Phase 5
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. DesignStudio com modos UML, Visual Flow, Markdown Spec, Code Editor
|
||||
2. AssembleLine gera código via Blackboard (GeneratorAgent)
|
||||
3. OrchestrateCenter faz deploy, versionamento e monitoramento
|
||||
4. Galeria de agentes por tenant funcional
|
||||
**Plans**: TBD
|
||||
|
||||
## Progress
|
||||
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Fundação | 3/3 | Complete | 2026-03-25 |
|
||||
| 2. Skills Engine | 4/4 | Complete | 2026-03-26 |
|
||||
| 3. MiroFlow Embutido | 3/3 | Complete | 2026-03-26 |
|
||||
| 4. OpenClaw Embutido | 0/TBD | Not started | - |
|
||||
| 5. Automation Fabric | 0/TBD | Not started | - |
|
||||
| 6. Dev Center Completo | 0/TBD | Not started | - |
|
||||
|
||||
### Phase 7: Skill Fabric Expandido: Compiladores, Sandbox Executor, Visual/Code/Markdown Editors com Validation Pipeline
|
||||
|
||||
**Goal:** [To be planned]
|
||||
**Requirements**: TBD
|
||||
**Depends on:** Phase 6
|
||||
**Plans:** 0 plans
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (run /gsd:plan-phase 7 to break down)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# State
|
||||
|
||||
## Current Phase: 5 — Automation Fabric
|
||||
|
||||
## Completed
|
||||
- Phase 1: submodules, tabelas, Neo4j, ReferenceParser
|
||||
- Phase 2: SkillEngine, API REST, Monaco Editor, /skills, autocomplete, Marketplace, versionamento Git-like
|
||||
- Phase 3: miroflow_service.py, bridge TS + KG logging, MiroFlowControl.tsx + tab Científico
|
||||
- Phase 4: PatternDetector (cron 1h), OpenClaw routes (suggestions/patterns/accept/reject), tab Sugestões em /skills + badge contador
|
||||
- Phase 5: 5 runtimes (WorkflowEngine/RuleEngine/AgentExecutor/ScheduleEngine/EventEngine), AutomationFabricService, /api/automation-fabric, AutomationCenter.tsx em /automations-center
|
||||
|
||||
## In Progress
|
||||
- (nenhum — Phase 5 concluída, iniciar Phase 6)
|
||||
|
||||
## Roadmap Evolution
|
||||
- Phase 7 added: Skill Fabric Expandido (compiladores, sandbox, 3 editor modes, validation pipeline)
|
||||
|
||||
## Notes
|
||||
- Superset em produção com RLS configurado — não alterar sem confirmação
|
||||
- Branch de deploy: `Servidor`
|
||||
- Modelos Ollama precisam ser baixados: deepseek-r1:14b, llama3.1:8b (máximo 14B)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
plan: "03-01"
|
||||
phase: "03-miroflow-embutido"
|
||||
status: complete
|
||||
completed: 2026-03-25
|
||||
---
|
||||
|
||||
# Summary: 03-01 — Python MiroFlow Microservice
|
||||
|
||||
## What was built
|
||||
|
||||
Microserviço FastAPI porta 8006 com 3 agentes científicos MiroFlow configurados para Ollama local, mais testes pytest cobrindo REQ-3.1 a REQ-3.4.
|
||||
|
||||
## Files created/modified
|
||||
|
||||
### Created
|
||||
- `server/python/miroflow_service.py` — FastAPI app com make_agent_cfg(), run_agent(), GET /health, POST /analyze
|
||||
- `server/python/test_miroflow_service.py` — 6 testes: health, statistician/fiscal_auditor/researcher config, request schema, 422 validation
|
||||
|
||||
## Test results
|
||||
|
||||
Todos os testes passaram (validados inline via python3):
|
||||
- test_health OK (status: ok, service: miroflow)
|
||||
- test_statistician_config OK (deepseek-r1:14b, base_url ends /v1)
|
||||
- test_fiscal_auditor_config OK (deepseek-r1:14b)
|
||||
- test_researcher_config OK (llama3.1:8b ou llama3.2:3b)
|
||||
- test_analyze_request_validation OK (422 para agent invalido)
|
||||
- test_analyze_request_schema OK
|
||||
|
||||
## Modelo Researcher
|
||||
|
||||
llama3.1:8b nao disponivel no Ollama local — fallback automatico para llama3.2:3b (ja instalado). Configuravel via MIROFLOW_RESEARCHER_MODEL no .env.
|
||||
|
||||
## Deps instaladas
|
||||
|
||||
omegaconf 2.3.0, hydra-core 1.3.2, mcp 1.26.0 instalados via pip3 --break-system-packages.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
## Commits (03-01 execution)
|
||||
|
||||
- 60f1c5c: feat(03-01): criar miroflow_service.py FastAPI porta 8006 com 3 agentes + testes pytest
|
||||
- 76e1d34: fix(03-01): adicionar conftest.py para resolver imports em pytest do root
|
||||
|
||||
## Deviations
|
||||
|
||||
- [Rule 2] conftest.py adicionado: pytest da raiz falhava sem sys.path fix
|
||||
- [Documentado] llama3.2:3b usado como fallback para researcher (llama3.1:8b nao instalado)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
plan: "03-02"
|
||||
phase: "03-miroflow-embutido"
|
||||
status: complete
|
||||
completed: 2026-03-25
|
||||
---
|
||||
|
||||
# Summary: 03-02 — Node.js MiroFlow Bridge
|
||||
|
||||
## What was built
|
||||
|
||||
Bridge Node.js → Python para o MiroFlow: proxy TypeScript com timeout 300s, rotas Express autenticadas e registro imutável de execuções no KG via auditHash SHA-256.
|
||||
|
||||
## Files created/modified
|
||||
|
||||
### Created
|
||||
- `server/miroflow/engine-proxy.ts` — proxy para :8006, MIROFLOW_TIMEOUT=300_000ms, registerExecutionInKG com SHA-256
|
||||
- `server/miroflow/routes.ts` — re-export de registerMiroFlowRoutes
|
||||
|
||||
### Modified
|
||||
- `server/routes.ts` — import + chamada registerMiroFlowRoutes(app) após registerBiEngineRoutes
|
||||
|
||||
## Key decisions
|
||||
|
||||
- Timeout de 300_000ms (5 min) para POST /analyze — adequado para deepseek-r1:14b
|
||||
- Health check usa timeout curto (5_000ms)
|
||||
- KG failure não bloqueia resposta ao cliente (try/catch com console.error)
|
||||
- auditHash cobre: execution_id + agent + model + input + output
|
||||
|
||||
## TypeScript compilation
|
||||
|
||||
Erros nos novos arquivos: **0**
|
||||
(Erros pré-existentes em App.tsx e server/modules/miroflow/ não relacionados)
|
||||
|
||||
## Commits
|
||||
|
||||
- `d53be76` — feat(03-02): criar MiroFlow proxy TypeScript com timeout 300s e KG audit logging
|
||||
- `1a70e87` — feat(03-02): registrar MiroFlow routes em server/routes.ts
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Phase 4 Context — OpenClaw Embutido
|
||||
|
||||
> Generated in --auto mode on 2026-03-26. All decisions auto-selected with recommended defaults.
|
||||
|
||||
## Phase Goal
|
||||
|
||||
Skills emergentes criadas automaticamente a partir de padrões detectados em `skill_executions`.
|
||||
|
||||
## Prior Context Applied
|
||||
|
||||
- **Stack de IA:** Ollama local via LiteLLM — usar `llama3.1:8b` para geração de drafts (leve, rápido)
|
||||
- **Backend-first:** API definida antes do frontend
|
||||
- **Auditoria imutável:** todas as execuções registradas com SHA-256
|
||||
- **Schema já existe:** `detected_patterns` e `skill_suggestions` em `shared/schema.ts` — usar sem alterar
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. O que constitui um "padrão"?
|
||||
|
||||
**[auto] Mesma skill executada ≥3 vezes em 30 dias com confiança ≥80%**
|
||||
|
||||
- Fonte de dados: tabela `skill_executions`
|
||||
- Agrupamento: `skillId + userId` (por usuário, não global)
|
||||
- Confiança calculada como: `(frequency / max_frequency_in_window) * 100`
|
||||
- Janela: `lastSeenAt - firstSeenAt ≤ 30 dias`
|
||||
- Thresholds alinhados com o roadmap: min 3 ocorrências, 30 dias, 80% confiança
|
||||
|
||||
### 2. Trigger do PatternDetector
|
||||
|
||||
**[auto] Cron job a cada hora (setInterval no servidor Node.js)**
|
||||
|
||||
- Sem dependência de infraestrutura externa (sem Redis, sem Bull)
|
||||
- Ao iniciar, PatternDetector registra no startup do servidor
|
||||
- Execuções recentes analisadas em lote; padrões gravados em `detected_patterns`
|
||||
- Se padrão já existe (mesmo skillId + userId): atualiza frequency + lastSeenAt
|
||||
|
||||
### 3. Geração do body da skill emergente
|
||||
|
||||
**[auto] AI via llama3.1:8b (LiteLLM) gera o body do DRAFT**
|
||||
|
||||
- Ao atingir threshold: PatternDetector cria entrada em `skill_suggestions` (status: `pending`)
|
||||
- Skill DRAFT gerada automaticamente em `arcadia_skills` (status: `draft`, source: `openclaw`)
|
||||
- Body gerado via prompt para llama3.1:8b descrevendo o padrão detectado
|
||||
- DRAFT aguarda aprovação — não é executável até ser `published`
|
||||
|
||||
### 4. Widget de notificação
|
||||
|
||||
**[auto] Badge no header global + painel slide-in**
|
||||
|
||||
- Badge no ícone de notificações existente no header (sem criar novo componente de header)
|
||||
- Ao clicar: slide-in panel lateral mostrando lista de sugestões pendentes
|
||||
- Cada sugestão mostra: nome sugerido, padrão detectado, frequência, confiança, body preview
|
||||
- Ações inline: "Aceitar" → promove para skill publicada | "Rejeitar" → status `rejected`
|
||||
|
||||
### 5. Aprovação — onde?
|
||||
|
||||
**[auto] Tab "Sugestões" em `/skills` (página existente) — Dev Center completo na Phase 6**
|
||||
|
||||
- Sem criar nova rota — adicionar tab na página `/skills` já existente
|
||||
- Tab lista `skill_suggestions` com status `pending`
|
||||
- Fluxo: aceitar → cria skill publicada a partir do DRAFT | rejeitar → arquiva sugestão
|
||||
- Dev Center completo (Phase 6) herdará esse fluxo
|
||||
|
||||
---
|
||||
|
||||
## Reusable Assets Identified
|
||||
|
||||
- `shared/schema.ts` — `detectedPatterns`, `skillSuggestions` já definidos (Phase 1)
|
||||
- `server/skills/engine.ts` — SkillEngine para criar DRAFT skills programaticamente
|
||||
- `server/skills/routes.ts` — rota `/api/skills` existente, adicionar sub-rotas de sugestões
|
||||
- `server/skills/versioning.ts` — SHA-256 audit logging (reusar padrão)
|
||||
- `client/src/pages/BiWorkspace.tsx` — referência de como adicionar tabs a uma página existente
|
||||
- `docker/litellm-config.yaml` — llama3.1:8b já configurado como Tier 2
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
**Incluído nesta fase:**
|
||||
- PatternDetector backend (cron + análise de skill_executions)
|
||||
- Geração de DRAFT via AI
|
||||
- Widget de notificação + slide-in
|
||||
- Tab "Sugestões" em /skills com aprovação/rejeição
|
||||
|
||||
**Fora de escopo (phases futuras):**
|
||||
- Dev Center completo (Phase 6)
|
||||
- Detecção de padrões em outras fontes além de skill_executions
|
||||
- Pattern sharing entre tenants
|
||||
|
||||
---
|
||||
|
||||
## Plan Breakdown Suggested
|
||||
|
||||
- **04-01:** PatternDetector service (backend) — cron, análise, gravação em detected_patterns + skill_suggestions + DRAFT creation via AI
|
||||
- **04-02:** API routes + frontend widget (badge + slide-in) + tab "Sugestões" em /skills
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Phase 5 Context — Automation Fabric
|
||||
|
||||
> Generated in --auto mode on 2026-03-26. All decisions auto-selected with recommended defaults.
|
||||
|
||||
## Phase Goal
|
||||
|
||||
Automações unificadas sob runtime único — 5 runtimes tipados substituindo a fragmentação entre XOS (`xos_automations`) e Central (`automations`), com `AutomationCenter.tsx` como visão unificada.
|
||||
|
||||
## Prior Context Applied
|
||||
|
||||
- **Stack Node.js/TypeScript:** runtimes ficam no servidor Express, sem novos microserviços Python
|
||||
- **Backend-first:** API definida antes do frontend
|
||||
- **DB imutável:** tabelas `automations` e `xos_automations` não são alteradas — migração virtual
|
||||
- **Padrão de phases anteriores:** wrappers leves sobre código existente, sem reescritas
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Estratégia de migração
|
||||
|
||||
**[auto] Unificação virtual — zero migração de dados**
|
||||
|
||||
- Tabelas `automations` e `xos_automations` permanecem intactas
|
||||
- Nova camada `server/automation-fabric/` agrega os dois via API layer
|
||||
- `AutomationCenter.tsx` consome endpoint unificado `/api/automation-fabric/list`
|
||||
- Endpoint retorna rows de ambas as tabelas com campo `source: "central" | "xos"`
|
||||
- Dados existentes: sem risco de perda, sem downtime, sem migration script
|
||||
|
||||
### 2. Os 5 runtimes
|
||||
|
||||
**[auto] Classes TypeScript leves — roteamento para serviços existentes**
|
||||
|
||||
Implementados em `server/automation-fabric/runtimes/`:
|
||||
|
||||
| Runtime | Responsabilidade | Delega para |
|
||||
|---|---|---|
|
||||
| `WorkflowEngine` | Executa ações sequenciais com condições | `AutomationService` existente |
|
||||
| `RuleEngine` | Avalia condições JSONB e dispara ações | Lógica inline, sem dependência |
|
||||
| `AgentExecutor` | Executa automações via agente Manus | `ManusService` |
|
||||
| `ScheduleEngine` | Gerencia cron/interval | `scheduledTasks` + setInterval existente |
|
||||
| `EventEngine` | Webhook/event triggers | Registra listeners no Express |
|
||||
|
||||
- `AutomationFabricService` orquestra: dado tipo de trigger → escolhe runtime correto
|
||||
- Trigger routing: `schedule` → ScheduleEngine, `event`/`webhook` → EventEngine, `agent` → AgentExecutor, default → WorkflowEngine
|
||||
|
||||
### 3. Rota do AutomationCenter
|
||||
|
||||
**[auto] Nova rota `/automations-center` — existentes intocadas**
|
||||
|
||||
- `/automations` (Central) e `/xos/automations` (XOS) permanecem sem alteração
|
||||
- Nova página `client/src/pages/AutomationCenter.tsx` em `/automations-center`
|
||||
- Adicionada ao nav/router sem remover rotas existentes
|
||||
|
||||
### 4. UI do AutomationCenter
|
||||
|
||||
**[auto] Lista unificada com badge de fonte + filtros inline**
|
||||
|
||||
- Lista flat com cards: nome, descrição, trigger type, última execução, status ativo/inativo
|
||||
- Badge colorido: `"Central"` (azul) | `"XOS"` (roxo)
|
||||
- Filtros: por fonte, por status (ativo/inativo), por trigger type
|
||||
- Toggle inline enable/disable via PATCH ao respectivo endpoint original
|
||||
- Execução manual via botão "Executar" → POST `/api/automation-fabric/{id}/run`
|
||||
- Empty state informativo
|
||||
|
||||
---
|
||||
|
||||
## Reusable Assets Identified
|
||||
|
||||
- `server/automations/service.ts` — `AutomationService.runAutomation()` (reusar no WorkflowEngine)
|
||||
- `server/automations/routes.ts` — padrão de rota com auth check
|
||||
- `server/manus/service.ts` — `manusService` para AgentExecutor
|
||||
- `shared/schema.ts` — `automations`, `xos_automations`, `automationLogs` já existentes
|
||||
- `client/src/pages/Automations.tsx` — referência de UX/layout para a nova página
|
||||
- `client/src/pages/XosAutomations.tsx` — referência de cards e filtros
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
**Incluído nesta fase:**
|
||||
- `server/automation-fabric/` — 5 runtimes + `AutomationFabricService` + rotas unificadas
|
||||
- `GET /api/automation-fabric/list` — lista unificada (central + xos)
|
||||
- `POST /api/automation-fabric/:id/run` — execução via fabric
|
||||
- `AutomationCenter.tsx` — página `/automations-center`
|
||||
|
||||
**Fora de escopo (phases futuras):**
|
||||
- Editor visual de automações (Phase 6 / Dev Center)
|
||||
- Migração física de dados para tabela unificada
|
||||
- Novos tipos de trigger não existentes
|
||||
|
||||
---
|
||||
|
||||
## Plan Breakdown Suggested
|
||||
|
||||
- **05-01:** `server/automation-fabric/` — 5 runtimes + `AutomationFabricService` + routes (`/api/automation-fabric`)
|
||||
- **05-02:** `AutomationCenter.tsx` — página unificada `/automations-center` com lista, filtros e execução inline
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Quick Task 260326-kmz: Implementar Skill Fabric
|
||||
|
||||
**Description:** Implementar Skill Fabric — gerador e gestor de skills da Arcádia Suite
|
||||
**Date:** 2026-03-26
|
||||
**Mode:** quick
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Backend — Skill Fabric Module
|
||||
|
||||
**Files:**
|
||||
- `server/modules/skill-fabric/src/types/index.ts`
|
||||
- `server/modules/skill-fabric/src/core/skill/Skill.entity.ts`
|
||||
- `server/modules/skill-fabric/src/core/compiler/VisualCompiler.ts`
|
||||
- `server/modules/skill-fabric/src/core/compiler/CodeCompiler.ts`
|
||||
- `server/modules/skill-fabric/src/core/compiler/MarkdownCompiler.ts`
|
||||
- `server/modules/skill-fabric/src/core/validator/ValidationPipeline.ts`
|
||||
- `server/modules/skill-fabric/src/core/executor/SkillExecutor.ts`
|
||||
- `server/modules/skill-fabric/src/core/executor/Sandbox.ts`
|
||||
- `server/modules/skill-fabric/src/core/lifecycle/LifecycleManager.ts`
|
||||
- `server/modules/skill-fabric/src/api/routes/skills.routes.ts`
|
||||
- `server/modules/skill-fabric/src/api/controllers/skills.controller.ts`
|
||||
- `server/modules/skill-fabric/index.ts`
|
||||
|
||||
**Action:** Create the full backend Skill Fabric module with types, entity, compilers, validator pipeline, executor/sandbox, lifecycle manager, and API routes. The project uses TypeScript + Express + Drizzle ORM. Reuse existing `db` and `skills` schema from `../../shared/schema`. No new DB migration needed — extend existing skills table logic.
|
||||
|
||||
**Context:**
|
||||
- Existing `server/skills/routes.ts` has basic CRUD. The new module adds compilation, validation, execution, versioning, lifecycle.
|
||||
- DB import: `import { db } from "@/db"` or `import { db } from "../../../db"` — check what works
|
||||
- Existing schema: `skills`, `skillExecutions` from `../../shared/schema`
|
||||
- Do NOT use vm2 (needs install) — use Node.js built-in `vm` module for sandbox
|
||||
- Do NOT use ReactFlow/Monaco yet (frontend task) — backend only here
|
||||
|
||||
**Done:** Files created, module compiles without errors
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Frontend — Skill Fabric UI
|
||||
|
||||
**Files:**
|
||||
- `client/src/modules/skill-fabric/index.ts`
|
||||
- `client/src/modules/skill-fabric/types.ts`
|
||||
- `client/src/modules/skill-fabric/api.ts`
|
||||
- `client/src/modules/skill-fabric/code-ide/components/SkillCodeEditor.tsx`
|
||||
- `client/src/modules/skill-fabric/markdown-studio/components/MarkdownEditor.tsx`
|
||||
- `client/src/modules/skill-fabric/shared/components/SkillToolbar.tsx`
|
||||
- `client/src/modules/skill-fabric/shared/components/VersionSelector.tsx`
|
||||
- `client/src/modules/skill-fabric/shared/components/ValidationPanel.tsx`
|
||||
- `client/src/pages/SkillFabricPage.tsx`
|
||||
|
||||
**Action:** Create frontend Skill Fabric module. Use textarea-based code editor (Monaco not installed — don't add it). Use Tailwind CSS + shadcn/ui components matching the existing project style. Create a SkillFabricPage that lists skills, allows creating/editing (code + markdown modes), compiling, validating, and executing. Wire to the backend API routes from Task 1.
|
||||
|
||||
**Context:**
|
||||
- Check `client/src/components/` for existing UI patterns (shadcn components available)
|
||||
- Check `client/src/App.tsx` to see how routes are registered — add the new page there
|
||||
- The visual canvas (ReactFlow) is a stretch goal — skip it, implement code + markdown modes only
|
||||
- API base: `/api/skills` (existing) + `/api/skill-fabric` (new from Task 1)
|
||||
|
||||
**Done:** SkillFabricPage renders, skills can be listed and created
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Register Routes & Integration
|
||||
|
||||
**Files:**
|
||||
- `server/modules/loader.ts`
|
||||
- `client/src/App.tsx` (or router file)
|
||||
|
||||
**Action:** Register the new skill-fabric backend routes in `server/modules/loader.ts`. Add the SkillFabricPage route in the frontend router. Verify existing skills route still works.
|
||||
|
||||
**Done:** `/api/skill-fabric/*` routes registered, frontend page accessible
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,24 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar MiroFlow e suas dependências direto do submodule
|
||||
COPY server/modules/miroflow/ ./server/modules/miroflow/
|
||||
RUN pip install --no-cache-dir -e ./server/modules/miroflow/
|
||||
|
||||
# Copiar o service script
|
||||
COPY server/python/miroflow_service.py ./server/python/miroflow_service.py
|
||||
|
||||
ENV MIROFLOW_PORT=8006
|
||||
ENV OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
|
||||
EXPOSE 8006
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8006/health')" || exit 1
|
||||
|
||||
CMD ["python", "server/python/miroflow_service.py"]
|
||||
Binary file not shown.
|
|
@ -0,0 +1,167 @@
|
|||
# Sessão de Desenvolvimento — 2026-03-18
|
||||
|
||||
**Branch:** `claude/analyze-project-0mXjP`
|
||||
**Commits desta sessão:** `8d14fc0`, `20a0237`
|
||||
**Contexto:** Continuação do ciclo iniciado em 2026-03-13 (auditoria A-Z + desenvolvimento das fases XOS)
|
||||
|
||||
---
|
||||
|
||||
## 1. O que foi feito nesta sessão
|
||||
|
||||
### 1.1 Correção de erros TypeScript críticos no `App.tsx`
|
||||
|
||||
Três classes de erros impediam o build limpo do frontend:
|
||||
|
||||
| Erro | Arquivo | Correção |
|
||||
|---|---|---|
|
||||
| `Cannot find module '@/contexts/SoeMotorContext'` | `App.tsx:8` | Criado `client/src/contexts/SoeMotorContext.tsx` |
|
||||
| `Cannot find module '@/pages/SOE'` | `App.tsx:39` | Criado `client/src/pages/SOE.tsx` |
|
||||
| `LazyExoticComponent` não atribuível a `() => Element` (20x) | `protected-route.tsx` | Tipo alterado para `React.ComponentType<any>` |
|
||||
|
||||
**Arquivos criados/modificados:**
|
||||
- `client/src/contexts/SoeMotorContext.tsx` — context global com `SoeMotorProvider` e hook `useSoeMotor`
|
||||
- `client/src/pages/SOE.tsx` — página SOE (Sistema de Operações Empresariais), re-export do módulo Plus/ERP
|
||||
- `client/src/lib/protected-route.tsx` — tipo do prop `component` corrigido
|
||||
|
||||
**Resultado:** `npx tsc --noEmit` zerou todos os erros do `App.tsx`. Erros remanescentes são pré-existentes em outros arquivos e não relacionados às correções.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Guia de deploy no Coolify (`DEPLOY_COOLIFY.md`)
|
||||
|
||||
Documento completo de 359 linhas cobrindo:
|
||||
|
||||
- **Arquitetura de serviços** — mapa visual de todos os containers e redes
|
||||
- **Variáveis de ambiente** — obrigatórias e opcionais por módulo (Core, IA, Superset, Plus, ERPNext)
|
||||
- **Profiles Docker** — como habilitar `ai`, `bi`, `plus`, `erpnext` via `COMPOSE_PROFILES`
|
||||
- **Sequência de inicialização** — ordem garantida pelos `depends_on` e healthchecks
|
||||
- **Traefik + HTTPS** — labels prontas para Let's Encrypt via Coolify
|
||||
- **Volumes e persistência** — tabela de criticidade + comandos de backup PostgreSQL
|
||||
- **Stack de IA** — 3 tiers LiteLLM (LLMFit → Ollama → externos), como baixar modelos, como habilitar LLMFit
|
||||
- **Pós-deploy** — migrations, verificação de saúde
|
||||
- **Checklist final** — antes, durante e após o deploy
|
||||
- **Comandos de referência rápida**
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Relatório de comparação Auditoria vs Desenvolvimento
|
||||
|
||||
Análise completa cruzando o `ARCADIA_AUDIT_EXECUCAO.md` (auditoria de 13/Mar) com todos os 18 commits do branch:
|
||||
|
||||
**Resultado geral: 44% concluído, 17% parcial, 38% pendente**
|
||||
|
||||
| Sprint | ✅ | ⚠️ | ❌ |
|
||||
|---|---|---|---|
|
||||
| S — Segurança (10 itens) | 7 | 3 | 0 |
|
||||
| 0 — Deploy (6 itens) | 2 | 1 | 3 |
|
||||
| 1 — Inteligência (6 itens) | 2 | 2 | 2 |
|
||||
| 2 — App Store (4 itens) | 3 | 0 | 1 |
|
||||
| 3 — Compass IA (4 itens) | 1 | 1 | 2 |
|
||||
| 4 — ERP Real (6 itens) | 0 | 0 | 6 |
|
||||
| 5 — Soberania IA (4 itens) | 3 | 1 | 0 |
|
||||
| 6 — Qualidade (7 itens) | 3 | 0 | 4 |
|
||||
| 7 — ERPNext (5 itens) | 2 | 1 | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Estado do Superset BI (análise técnica)
|
||||
|
||||
Levantamento completo da integração Apache Superset:
|
||||
|
||||
**Funcionando:**
|
||||
- Gateway Node.js com 4 endpoints (`/guest-token`, `/dashboards`, `/health`, proxy `/superset/*`)
|
||||
- Guest Token JWT via service account com cache de 50 min
|
||||
- Componente React `<SupersetDashboard>` reutilizável com fallback iframe
|
||||
- Docker: init automático, banco `arcadia` registrado somente leitura, admin criado
|
||||
- Feature flags: embed, drill-to-detail, cross-filters, alerts, templates Jinja
|
||||
|
||||
**Pendente / stub:**
|
||||
- RLS por tenant — `rls: []` vazio, todos veem todos os dados
|
||||
- Nenhum dashboard pré-criado — criação manual pelo usuário
|
||||
- Cache Redis — usando `SimpleCache` em memória
|
||||
- SSO/OAuth — auth local apenas
|
||||
|
||||
---
|
||||
|
||||
## 3. Contexto acumulado do ciclo completo (desde 13/Mar)
|
||||
|
||||
### O que foi construído no branch (18 commits, 71 arquivos, +9.399 linhas)
|
||||
|
||||
| Área | Principais entregas |
|
||||
|---|---|
|
||||
| **Segurança** | Auth em todas as rotas XOS, CORS Python corrigido, credentials removidas, timeout+retry, approval guard Manus |
|
||||
| **XOS CRM** | Fases 1–4 completas: automações, SLA, horário comercial, campanhas, Socket.IO tempo real |
|
||||
| **Novas páginas React** | `XosSupervisor`, `XosReports`, `XosProtocols` |
|
||||
| **SOE Rule Engine** | Motor de regras contábil/fiscal + importador ERPNext |
|
||||
| **Superset BI** | Integração completa embed + proxy + guest token |
|
||||
| **Infra Docker** | `docker-compose.prod.yml` com profiles: `ai`, `bi`, `plus`, `erpnext` |
|
||||
| **IA Soberania** | LiteLLM 3 tiers + Ollama + slot LLMFit pronto |
|
||||
| **Qualidade** | Rate limiting, logging estruturado (`server/logger.ts`), paginação |
|
||||
| **App Store** | `GET /api/marketplace/my-apps` + AppCenter por subscription |
|
||||
| **Docs** | `DEPLOY_COOLIFY.md`, `INTEGRACAO_IA.md`, `PLANO_BI_SUPERSET.md`, `PLANO_SOE_CENTRAL_v2.md` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Demandas prioritárias pendentes
|
||||
|
||||
### Críticas (bloqueia produção estável)
|
||||
|
||||
1. **LMS e Quality** — auth ainda incompleta (SEC-02, SEC-03)
|
||||
- `server/lms/routes.ts` — 8 rotas ainda públicas
|
||||
- `server/quality/routes.ts` — ~50 rotas revisão insuficiente
|
||||
|
||||
2. **WhatsApp auto-reply** — configuração ainda em `Map` na memória
|
||||
- Reiniciar servidor = perda total da configuração sem aviso
|
||||
- Persistir em tabela `whatsapp_auto_reply_config` (migration já existe)
|
||||
|
||||
3. **`pg_dump` do Replit** — exportar banco antes de encerrar o plano
|
||||
- Risco de perda de dados de produção atual
|
||||
|
||||
### Altas (produto incompleto)
|
||||
|
||||
4. **Ciclo de aprendizado Manus** — embeddings não são populados automaticamente
|
||||
- Busca semântica retorna vazio, sistema não aprende das interações
|
||||
|
||||
5. **Páginas placeholder** — `Agent.tsx`, `CentralApis.tsx`, `ApiHub.tsx`
|
||||
- Três das páginas mais estratégicas ainda sem implementação
|
||||
|
||||
6. **Sprint 4 inteiro — Integrações ERP reais**
|
||||
- Adaptadores Omie e TOTVS: zero implementado
|
||||
- Todos os conectores da Central de API ainda mockados
|
||||
- Estrutura fragmentada em 4 lugares (`server/erpnext/`, `server/crm/frappe-service.ts`, `server/api-central/`, `server/migration/`)
|
||||
|
||||
### Médias (qualidade de engenharia)
|
||||
|
||||
7. **Error boundaries globais no React** — sem captura de erros em cascata
|
||||
8. **Testes automatizados** — zero no projeto inteiro
|
||||
9. **Connection pooling Python** — nova conexão DB por request nos 6 microserviços
|
||||
10. **Consolidar sistemas de comunicação** — legacy (`whatsapp_*`, `chat_*`) e moderno (`comm_*`, `xosConversations`) duplicados
|
||||
11. **36 tabelas sem insert schema** — risco de runtime errors no Drizzle
|
||||
12. **40+ tabelas sem `createdAt`/`updatedAt`** — auditoria e ordenação impossíveis
|
||||
|
||||
### Decisões arquiteturais abertas
|
||||
|
||||
| # | Decisão |
|
||||
|---|---|
|
||||
| D1 | BI padrão: Metabase (existente) vs Superset (integrado)? |
|
||||
| D2 | LMS: tabelas dinâmicas vs schema.ts fixo? |
|
||||
| D3 | Sistema de comunicação canônico: legacy vs moderno vs XOS? |
|
||||
| D4 | Marketplace público ou autenticado (hoje público — intencional)? |
|
||||
|
||||
---
|
||||
|
||||
## 5. Próxima sessão recomendada
|
||||
|
||||
**Sequência sugerida por impacto/risco:**
|
||||
|
||||
```
|
||||
1. Corrigir auth LMS + Quality (2–3h) → fecha todas as vulnerabilidades SEC
|
||||
2. Persistir WhatsApp auto-reply no banco (1h) → estabilidade em produção
|
||||
3. Fechar ciclo embeddings Manus (2h) → IA começa a aprender de verdade
|
||||
4. Implementar Agent.tsx com UI real (3–4h) → página mais estratégica
|
||||
5. Adaptador Omie — primeira integração ERP real (4–6h)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Sessão registrada em 2026-03-18 | Branch: `claude/analyze-project-0mXjP`*
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
|
|
@ -0,0 +1,217 @@
|
|||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Brain, Loader2 } from "lucide-react";
|
||||
|
||||
interface AnalyzeRequest {
|
||||
agent: "statistician" | "fiscal_auditor" | "researcher";
|
||||
task: string;
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface AnalyzeResponse {
|
||||
agent: string;
|
||||
model: string;
|
||||
result: string;
|
||||
execution_id: string;
|
||||
duration_ms: number;
|
||||
}
|
||||
|
||||
const AGENTS = [
|
||||
{
|
||||
value: "statistician",
|
||||
label: "Statistician",
|
||||
model: "deepseek-r1:14b",
|
||||
placeholder:
|
||||
"Ex: Analise a distribuição de vendas por região no último trimestre",
|
||||
},
|
||||
{
|
||||
value: "fiscal_auditor",
|
||||
label: "Fiscal Auditor",
|
||||
model: "deepseek-r1:14b",
|
||||
placeholder:
|
||||
"Ex: Verifique inconsistências nos registros NFe do CNPJ 12.345.678/0001-90",
|
||||
},
|
||||
{
|
||||
value: "researcher",
|
||||
label: "Researcher",
|
||||
model: "llama3.1:8b",
|
||||
placeholder:
|
||||
"Ex: Quais fornecedores têm maior correlação com atrasos de entrega?",
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface MiroFlowControlProps {
|
||||
currentDashboardData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function MiroFlowControl({
|
||||
currentDashboardData,
|
||||
}: MiroFlowControlProps) {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [selectedAgent, setSelectedAgent] = useState<
|
||||
"statistician" | "fiscal_auditor" | "researcher"
|
||||
>("statistician");
|
||||
const [task, setTask] = useState("");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (request: AnalyzeRequest) => {
|
||||
const response = await apiRequest("POST", "/api/miroflow/analyze", request);
|
||||
return response.json() as Promise<AnalyzeResponse>;
|
||||
},
|
||||
});
|
||||
|
||||
const handleAnalyze = () => {
|
||||
if (!task.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutation.mutate({
|
||||
agent: selectedAgent,
|
||||
task,
|
||||
context: currentDashboardData,
|
||||
});
|
||||
};
|
||||
|
||||
const currentAgentConfig = AGENTS.find((a) => a.value === selectedAgent);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-[#1f334d] border border-[#c89b3c]/20">
|
||||
<CardHeader className="border-b border-[#c89b3c]/20 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Brain className="w-6 h-6 text-[#c89b3c]" />
|
||||
<CardTitle className="text-white">Modo Científico</CardTitle>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||||
enabled ? "bg-[#c89b3c]" : "bg-gray-600"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||||
enabled ? "translate-x-7" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{enabled && (
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
{/* Agent Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
Selecione o Agente
|
||||
</label>
|
||||
<Select value={selectedAgent} onValueChange={(value: any) => setSelectedAgent(value)}>
|
||||
<SelectTrigger className="bg-[#2a4466] border border-[#c89b3c]/20 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#2a4466] border border-[#c89b3c]/20">
|
||||
{AGENTS.map((agent) => (
|
||||
<SelectItem key={agent.value} value={agent.value} className="text-white">
|
||||
<div>
|
||||
<div>{agent.label}</div>
|
||||
<div className="text-xs text-gray-400">{agent.model}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Task Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
Digite a Tarefa
|
||||
</label>
|
||||
<Textarea
|
||||
placeholder={currentAgentConfig?.placeholder || "Digite sua tarefa..."}
|
||||
value={task}
|
||||
onChange={(e) => setTask(e.target.value)}
|
||||
disabled={mutation.isPending}
|
||||
className="bg-[#2a4466] border border-[#c89b3c]/20 text-white placeholder:text-gray-500 min-h-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Analyze Button */}
|
||||
<Button
|
||||
onClick={handleAnalyze}
|
||||
disabled={!task.trim() || mutation.isPending}
|
||||
className="w-full bg-[#c89b3c] hover:bg-[#d4a94a] text-[#1f334d] font-semibold"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Analisando...
|
||||
</>
|
||||
) : (
|
||||
"Analisar"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Error Display */}
|
||||
{mutation.isError && (
|
||||
<div className="rounded-lg p-4 bg-red-900/20 border border-red-500/50">
|
||||
<Badge className="mb-2 bg-red-600 text-white">Erro</Badge>
|
||||
<p className="text-sm text-red-100">
|
||||
{mutation.error instanceof Error
|
||||
? mutation.error.message
|
||||
: "Erro ao processar a análise"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result Display */}
|
||||
{mutation.isSuccess && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge className="bg-green-600 text-white">Sucesso</Badge>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg p-4 bg-[#2a4466] border border-[#c89b3c]/20">
|
||||
<ScrollArea className="h-64 w-full pr-4">
|
||||
<p className="text-white text-sm whitespace-pre-wrap">
|
||||
{mutation.data?.result}
|
||||
</p>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="grid grid-cols-3 gap-2 text-xs text-gray-400">
|
||||
<div>
|
||||
<span className="font-semibold text-[#c89b3c]">Modelo:</span>{" "}
|
||||
{mutation.data?.model}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-[#c89b3c]">Tempo:</span>{" "}
|
||||
{mutation.data?.duration_ms}ms
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-[#c89b3c]">ID:</span>{" "}
|
||||
{mutation.data?.execution_id?.slice(0, 8)}...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,245 @@
|
|||
/**
|
||||
* OpenClawPanel — Phase 4
|
||||
*
|
||||
* Painel de visualização de Skills Emergentes detectadas pelo PatternDetector.
|
||||
* Exibido como aba na sidebar do /development.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Sparkles, CheckCircle, XCircle, ChevronDown, ChevronRight,
|
||||
Loader2, Activity, Clock, BarChart2
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface Suggestion {
|
||||
id: string;
|
||||
suggestedSkillName: string;
|
||||
suggestedDescription: string | null;
|
||||
estimatedAutomation: string | null;
|
||||
confidence: string;
|
||||
draftBody: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Pattern {
|
||||
id: string;
|
||||
actionType: string;
|
||||
description: string | null;
|
||||
frequency: number;
|
||||
confidence: string;
|
||||
firstSeenAt: string;
|
||||
lastSeenAt: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function OpenClawPanel() {
|
||||
const [tab, setTab] = useState<"suggestions" | "patterns">("suggestions");
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const { toast } = useToast();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const { data: sugData, isLoading: sugLoading } = useQuery<{ suggestions: Suggestion[]; total: number }>({
|
||||
queryKey: ["/api/openclaw/suggestions"],
|
||||
refetchInterval: 2 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: patData, isLoading: patLoading } = useQuery<{ patterns: Pattern[]; total: number }>({
|
||||
queryKey: ["/api/openclaw/patterns"],
|
||||
enabled: tab === "patterns",
|
||||
});
|
||||
|
||||
const accept = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const res = await fetch(`/api/openclaw/suggestions/${id}/accept`, { method: "POST" });
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["/api/openclaw/suggestions"] });
|
||||
qc.invalidateQueries({ queryKey: ["/api/skills"] });
|
||||
toast({ title: "Skill publicada!" });
|
||||
},
|
||||
onError: () => toast({ title: "Erro ao aceitar", variant: "destructive" }),
|
||||
});
|
||||
|
||||
const reject = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const res = await fetch(`/api/openclaw/suggestions/${id}/reject`, { method: "POST" });
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["/api/openclaw/suggestions"] });
|
||||
toast({ title: "Sugestão rejeitada" });
|
||||
},
|
||||
onError: () => toast({ title: "Erro ao rejeitar", variant: "destructive" }),
|
||||
});
|
||||
|
||||
const suggestions = sugData?.suggestions ?? [];
|
||||
const patterns = patData?.patterns ?? [];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-gray-900 text-white">
|
||||
{/* Header */}
|
||||
<div className="px-5 py-4 border-b border-white/10 flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-yellow-400" />
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">Skills Emergentes</h2>
|
||||
<p className="text-[11px] text-white/40">OpenClaw — detecção automática de padrões</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-white/10 text-xs">
|
||||
<button
|
||||
onClick={() => setTab("suggestions")}
|
||||
className={`px-4 py-2.5 flex items-center gap-1.5 transition-colors border-b-2 ${tab === "suggestions" ? "border-yellow-400 text-yellow-400" : "border-transparent text-white/50 hover:text-white"}`}
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
Sugestões
|
||||
{suggestions.length > 0 && (
|
||||
<span className="ml-1 rounded-full bg-yellow-400 text-black text-[9px] font-bold w-4 h-4 flex items-center justify-center">
|
||||
{suggestions.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("patterns")}
|
||||
className={`px-4 py-2.5 flex items-center gap-1.5 transition-colors border-b-2 ${tab === "patterns" ? "border-yellow-400 text-yellow-400" : "border-transparent text-white/50 hover:text-white"}`}
|
||||
>
|
||||
<Activity className="w-3 h-3" />
|
||||
Padrões
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
{/* Sugestões */}
|
||||
{tab === "suggestions" && (
|
||||
<div>
|
||||
{sugLoading && (
|
||||
<div className="flex justify-center py-10">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-white/30" />
|
||||
</div>
|
||||
)}
|
||||
{!sugLoading && suggestions.length === 0 && (
|
||||
<div className="px-5 py-12 text-center">
|
||||
<Sparkles className="w-8 h-8 text-white/10 mx-auto mb-3" />
|
||||
<p className="text-sm text-white/30">Nenhuma sugestão pendente</p>
|
||||
<p className="text-xs text-white/20 mt-1">O detector roda a cada hora</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="divide-y divide-white/5">
|
||||
{suggestions.map((s) => {
|
||||
const conf = Math.round(Number(s.confidence) * 100);
|
||||
const isExp = expanded === s.id;
|
||||
const isAccepting = accept.isPending && accept.variables === s.id;
|
||||
const isRejecting = reject.isPending && reject.variables === s.id;
|
||||
|
||||
return (
|
||||
<div key={s.id} className="px-4 py-4 space-y-2.5">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{s.suggestedSkillName}</p>
|
||||
{s.suggestedDescription && (
|
||||
<p className="text-xs text-white/50 mt-0.5 line-clamp-2">{s.suggestedDescription}</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge className="shrink-0 bg-yellow-400/10 text-yellow-400 border-yellow-400/20 text-[10px]">
|
||||
{conf}%
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{s.draftBody && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setExpanded(isExp ? null : s.id)}
|
||||
className="flex items-center gap-1 text-[11px] text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
{isExp ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||
{isExp ? "Ocultar body" : "Ver body gerado"}
|
||||
</button>
|
||||
{isExp && (
|
||||
<pre className="mt-2 rounded bg-black/40 p-3 text-[11px] text-white/60 overflow-x-auto whitespace-pre-wrap font-mono max-h-40 border border-white/5">
|
||||
{s.draftBody}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs bg-emerald-900/40 text-emerald-400 border border-emerald-700/40 hover:bg-emerald-900/60"
|
||||
onClick={() => accept.mutate(s.id)}
|
||||
disabled={isAccepting || isRejecting}
|
||||
>
|
||||
{isAccepting ? <Loader2 className="w-3 h-3 animate-spin" /> : <CheckCircle className="w-3 h-3" />}
|
||||
<span className="ml-1">Aceitar</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
onClick={() => reject.mutate(s.id)}
|
||||
disabled={isAccepting || isRejecting}
|
||||
>
|
||||
{isRejecting ? <Loader2 className="w-3 h-3 animate-spin" /> : <XCircle className="w-3 h-3" />}
|
||||
<span className="ml-1">Rejeitar</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Padrões */}
|
||||
{tab === "patterns" && (
|
||||
<div>
|
||||
{patLoading && (
|
||||
<div className="flex justify-center py-10">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-white/30" />
|
||||
</div>
|
||||
)}
|
||||
{!patLoading && patterns.length === 0 && (
|
||||
<div className="px-5 py-12 text-center">
|
||||
<BarChart2 className="w-8 h-8 text-white/10 mx-auto mb-3" />
|
||||
<p className="text-sm text-white/30">Nenhum padrão detectado</p>
|
||||
<p className="text-xs text-white/20 mt-1">Execute skills manualmente para gerar dados</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="divide-y divide-white/5">
|
||||
{patterns.map((p) => {
|
||||
const conf = Math.round(Number(p.confidence) * 100);
|
||||
const last = new Date(p.lastSeenAt).toLocaleDateString("pt-BR");
|
||||
return (
|
||||
<div key={p.id} className="px-4 py-3 space-y-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-mono text-white/70 truncate">{p.actionType}</p>
|
||||
<Badge className="shrink-0 bg-white/5 text-white/50 border-white/10 text-[10px]">
|
||||
{conf}%
|
||||
</Badge>
|
||||
</div>
|
||||
{p.description && (
|
||||
<p className="text-[11px] text-white/40">{p.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 text-[10px] text-white/30">
|
||||
<span className="flex items-center gap-1"><Activity className="w-3 h-3" />{p.frequency}x</span>
|
||||
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{last}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
RefreshCw, CheckCircle, XCircle, Loader2, Users, Package,
|
||||
ShoppingCart, FileText, ExternalLink, Building2, Link2
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface Empresa {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
razaoSocial: string;
|
||||
nomeFantasia: string | null;
|
||||
cnpj: string;
|
||||
tipo: string;
|
||||
plusEmpresaId: number | null;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
success: boolean;
|
||||
created?: number;
|
||||
updated?: number;
|
||||
failed?: number;
|
||||
errors?: string[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface PlusStatus {
|
||||
connected: boolean;
|
||||
url?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export default function PlusSyncPanel({ tenantId }: { tenantId?: number }) {
|
||||
const { toast } = useToast();
|
||||
const [empresas, setEmpresas] = useState<Empresa[]>([]);
|
||||
const [selectedEmpresa, setSelectedEmpresa] = useState<string>("");
|
||||
const [plusStatus, setPlusStatus] = useState<PlusStatus | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [syncingCustomers, setSyncingCustomers] = useState(false);
|
||||
const [syncingProducts, setSyncingProducts] = useState(false);
|
||||
const [syncingSales, setSyncingSales] = useState(false);
|
||||
const [importingCustomers, setImportingCustomers] = useState(false);
|
||||
const [importingProducts, setImportingProducts] = useState(false);
|
||||
const [bindPlusId, setBindPlusId] = useState("");
|
||||
const [lastResults, setLastResults] = useState<Record<string, SyncResult>>({});
|
||||
|
||||
const currentTenant = tenantId || 1;
|
||||
|
||||
const loadEmpresas = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/retail/plus/empresas?tenantId=${currentTenant}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEmpresas(data);
|
||||
if (data.length > 0 && !selectedEmpresa) {
|
||||
setSelectedEmpresa(data[0].id.toString());
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading empresas:", error);
|
||||
}
|
||||
}, [currentTenant, selectedEmpresa]);
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const empresaId = selectedEmpresa ? `?empresaId=${selectedEmpresa}` : "";
|
||||
const res = await fetch(`/api/retail/plus/status${empresaId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setPlusStatus(data);
|
||||
}
|
||||
} catch (error) {
|
||||
setPlusStatus({ connected: false, message: "Erro ao verificar conexão" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedEmpresa]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmpresas();
|
||||
}, [loadEmpresas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEmpresa) checkStatus();
|
||||
}, [selectedEmpresa, checkStatus]);
|
||||
|
||||
const handleSync = async (action: string, setLoading: (v: boolean) => void) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const body: any = { tenantId: currentTenant };
|
||||
if (selectedEmpresa) body.empresaId = parseInt(selectedEmpresa);
|
||||
|
||||
const res = await fetch(`/api/retail/plus/${action}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
setLastResults(prev => ({ ...prev, [action]: data }));
|
||||
|
||||
if (data.success) {
|
||||
toast({ title: "Sincronização concluída", description: `Criados: ${data.created || 0}, Atualizados: ${data.updated || 0}` });
|
||||
} else {
|
||||
toast({ title: "Erro na sincronização", description: data.message || "Falha na operação", variant: "destructive" });
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: "Erro", description: "Falha na comunicação com o servidor", variant: "destructive" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBindEmpresa = async () => {
|
||||
if (!selectedEmpresa || !bindPlusId) return;
|
||||
try {
|
||||
const res = await fetch(`/api/retail/plus/empresas/${selectedEmpresa}/bind`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ plusEmpresaId: parseInt(bindPlusId) }),
|
||||
});
|
||||
if (res.ok) {
|
||||
toast({ title: "Empresa vinculada", description: "Empresa vinculada ao Plus com sucesso" });
|
||||
loadEmpresas();
|
||||
setBindPlusId("");
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: "Erro", description: "Falha ao vincular empresa", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const currentEmpresa = empresas.find(e => e.id.toString() === selectedEmpresa);
|
||||
|
||||
const SyncResultBadge = ({ result }: { result?: SyncResult }) => {
|
||||
if (!result) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs mt-1">
|
||||
{result.success ? (
|
||||
<Badge variant="outline" className="text-green-600 border-green-300">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
OK: {result.created || 0} criados, {result.updated || 0} atualizados
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
{result.message || "Falha"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6" data-testid="plus-sync-panel">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Integração Plus ERP</h3>
|
||||
<p className="text-sm text-muted-foreground">Sincronização bidirecional com o Arcádia Plus (Laravel ERP)</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{plusStatus && (
|
||||
<Badge variant={plusStatus.connected ? "default" : "destructive"} data-testid="plus-connection-status">
|
||||
{plusStatus.connected ? (
|
||||
<><CheckCircle className="h-3 w-3 mr-1" /> Conectado</>
|
||||
) : (
|
||||
<><XCircle className="h-3 w-3 mr-1" /> Desconectado</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={checkStatus} disabled={loading} data-testid="btn-check-plus-status">
|
||||
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? "animate-spin" : ""}`} />
|
||||
Testar Conexão
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4" />
|
||||
Empresa Ativa
|
||||
</CardTitle>
|
||||
<CardDescription>Selecione a empresa para operações de sincronização</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-end gap-3">
|
||||
<div className="flex-1">
|
||||
<Label>Empresa</Label>
|
||||
<Select value={selectedEmpresa} onValueChange={setSelectedEmpresa}>
|
||||
<SelectTrigger data-testid="select-empresa-plus">
|
||||
<SelectValue placeholder="Selecione uma empresa" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{empresas.map(emp => (
|
||||
<SelectItem key={emp.id} value={emp.id.toString()}>
|
||||
{emp.nomeFantasia || emp.razaoSocial} ({emp.cnpj}) {emp.tipo === "matriz" ? "📍" : "🔗"}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentEmpresa && (
|
||||
<div className="rounded-lg border p-3 bg-muted/50 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">{currentEmpresa.razaoSocial}</span>
|
||||
<Badge variant="outline">{currentEmpresa.tipo === "matriz" ? "Matriz" : "Filial"}</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">CNPJ: {currentEmpresa.cnpj}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-3 w-3" />
|
||||
<span className="text-xs">Plus ID: {currentEmpresa.plusEmpresaId || "Não vinculada"}</span>
|
||||
{!currentEmpresa.plusEmpresaId && (
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="ID da empresa no Plus"
|
||||
value={bindPlusId}
|
||||
onChange={e => setBindPlusId(e.target.value)}
|
||||
className="h-7 w-36 text-xs"
|
||||
data-testid="input-bind-plus-id"
|
||||
/>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={handleBindEmpresa} data-testid="btn-bind-empresa">
|
||||
Vincular
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
Clientes / Fornecedores
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={syncingCustomers}
|
||||
onClick={() => handleSync("sync/customers", setSyncingCustomers)}
|
||||
data-testid="btn-sync-customers-to-plus"
|
||||
>
|
||||
{syncingCustomers ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <ExternalLink className="h-4 w-4 mr-1" />}
|
||||
Enviar → Plus
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={importingCustomers}
|
||||
onClick={() => handleSync("import/customers", setImportingCustomers)}
|
||||
data-testid="btn-import-customers-from-plus"
|
||||
>
|
||||
{importingCustomers ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <ExternalLink className="h-4 w-4 mr-1 rotate-180" />}
|
||||
Plus → Importar
|
||||
</Button>
|
||||
</div>
|
||||
<SyncResultBadge result={lastResults["sync/customers"] || lastResults["import/customers"]} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Package className="h-4 w-4" />
|
||||
Produtos / Estoque
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={importingProducts}
|
||||
onClick={() => handleSync("import/products", setImportingProducts)}
|
||||
data-testid="btn-import-products-from-plus"
|
||||
>
|
||||
{importingProducts ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Package className="h-4 w-4 mr-1" />}
|
||||
Plus → Importar Produtos
|
||||
</Button>
|
||||
</div>
|
||||
<SyncResultBadge result={lastResults["import/products"]} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
Vendas PDV
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
As vendas são sincronizadas automaticamente ao finalizar no PDV.
|
||||
Use o botão abaixo para reenviar vendas pendentes.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={syncingSales}
|
||||
onClick={() => {
|
||||
toast({ title: "Em breve", description: "Sincronização em lote será implementada" });
|
||||
}}
|
||||
data-testid="btn-sync-pending-sales"
|
||||
>
|
||||
{syncingSales ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1" />}
|
||||
Reenviar Pendentes
|
||||
</Button>
|
||||
<SyncResultBadge result={lastResults["sync/sales"]} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
Documentos Fiscais
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Emissão de NF-e e NFC-e via Plus ERP. Selecione a venda na aba PDV
|
||||
e use a opção "Emitir NF-e" para gerar o documento fiscal.
|
||||
</p>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
Integrado via Plus API
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export type TenantFeatures = {
|
||||
ide: boolean;
|
||||
ideMode: 'none' | 'no-code' | 'low-code' | 'pro-code';
|
||||
whatsapp: boolean;
|
||||
whatsappSessions: number;
|
||||
crm: boolean;
|
||||
erp: boolean;
|
||||
bi: boolean;
|
||||
manus: boolean;
|
||||
manusTools: string[];
|
||||
centralApis: boolean;
|
||||
centralApisManage: boolean;
|
||||
comunidades: boolean;
|
||||
maxChannels: number;
|
||||
biblioteca: boolean;
|
||||
bibliotecaPublish: boolean;
|
||||
suporteN3: boolean;
|
||||
retail: boolean;
|
||||
plus: boolean;
|
||||
fisco: boolean;
|
||||
cockpit: boolean;
|
||||
compass: boolean;
|
||||
production: boolean;
|
||||
support: boolean;
|
||||
xosCrm: boolean;
|
||||
};
|
||||
|
||||
const defaultFeatures: TenantFeatures = {
|
||||
ide: true, ideMode: 'pro-code', whatsapp: false, whatsappSessions: 0,
|
||||
crm: true, erp: true, bi: false, manus: true, manusTools: [],
|
||||
centralApis: false, centralApisManage: false, comunidades: false,
|
||||
maxChannels: 5, biblioteca: false, bibliotecaPublish: false,
|
||||
suporteN3: false, retail: false, plus: false, fisco: false,
|
||||
cockpit: false, compass: true, production: false, support: false, xosCrm: false
|
||||
};
|
||||
|
||||
export function useTenantFeatures() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["tenant-modules"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/soe/tenant/modules", { credentials: "include" });
|
||||
if (!res.ok) throw new Error("Failed to fetch tenant modules");
|
||||
return res.json();
|
||||
},
|
||||
staleTime: 60000,
|
||||
retry: 1
|
||||
});
|
||||
|
||||
const features: TenantFeatures = { ...defaultFeatures, ...(data?.features || {}) };
|
||||
|
||||
return {
|
||||
features,
|
||||
isLoading,
|
||||
plan: data?.plan as string | undefined,
|
||||
isEnabled: (key: keyof TenantFeatures) => !!features[key],
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
/**
|
||||
* AutomationCenter — Phase 5: Automation Fabric
|
||||
*
|
||||
* Visão unificada de todas as automações: Central + XOS.
|
||||
* Rota: /automations-center
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Zap, Search, Play, Loader2, CheckCircle, XCircle,
|
||||
Clock, Webhook, Bot, Calendar, Activity, Filter
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface UnifiedAutomation {
|
||||
id: string;
|
||||
sourceId: number;
|
||||
source: "central" | "xos";
|
||||
name: string;
|
||||
description: string | null;
|
||||
triggerType: string;
|
||||
isActive: boolean;
|
||||
executionCount: number;
|
||||
lastExecutedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const TRIGGER_ICONS: Record<string, React.ReactNode> = {
|
||||
schedule: <Clock className="w-3.5 h-3.5" />,
|
||||
webhook: <Webhook className="w-3.5 h-3.5" />,
|
||||
agent: <Bot className="w-3.5 h-3.5" />,
|
||||
event: <Activity className="w-3.5 h-3.5" />,
|
||||
contact_created: <Zap className="w-3.5 h-3.5" />,
|
||||
deal_stage_changed: <Zap className="w-3.5 h-3.5" />,
|
||||
form_submitted: <Zap className="w-3.5 h-3.5" />,
|
||||
};
|
||||
|
||||
const TRIGGER_LABEL: Record<string, string> = {
|
||||
schedule: "Agendada",
|
||||
webhook: "Webhook",
|
||||
manual: "Manual",
|
||||
event: "Evento",
|
||||
agent: "Agente",
|
||||
contact_created: "Novo Contato",
|
||||
deal_stage_changed: "Mudança de Etapa",
|
||||
form_submitted: "Formulário",
|
||||
};
|
||||
|
||||
export default function AutomationCenter() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [filterSource, setFilterSource] = useState<"all" | "central" | "xos">("all");
|
||||
const [filterStatus, setFilterStatus] = useState<"all" | "active" | "inactive">("all");
|
||||
const { toast } = useToast();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery<{ automations: UnifiedAutomation[]; total: number }>({
|
||||
queryKey: ["/api/automation-fabric/list"],
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
const runMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const res = await fetch(`/api/automation-fabric/${encodeURIComponent(id)}/run`, { method: "POST" });
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: (_, id) => {
|
||||
toast({ title: "Automação executada" });
|
||||
qc.invalidateQueries({ queryKey: ["/api/automation-fabric/list"] });
|
||||
},
|
||||
onError: () => toast({ title: "Erro ao executar", variant: "destructive" }),
|
||||
});
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
|
||||
const res = await fetch(`/api/automation-fabric/${encodeURIComponent(id)}/toggle`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isActive }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["/api/automation-fabric/list"] }),
|
||||
onError: () => toast({ title: "Erro ao atualizar status", variant: "destructive" }),
|
||||
});
|
||||
|
||||
const automations = data?.automations ?? [];
|
||||
|
||||
const filtered = automations.filter((a) => {
|
||||
if (filterSource !== "all" && a.source !== filterSource) return false;
|
||||
if (filterStatus === "active" && !a.isActive) return false;
|
||||
if (filterStatus === "inactive" && a.isActive) return false;
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
a.name.toLowerCase().includes(q) ||
|
||||
(a.description ?? "").toLowerCase().includes(q) ||
|
||||
a.triggerType.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const activeCount = automations.filter((a) => a.isActive).length;
|
||||
const centralCount = automations.filter((a) => a.source === "central").length;
|
||||
const xosCount = automations.filter((a) => a.source === "xos").length;
|
||||
|
||||
return (
|
||||
<BrowserFrame>
|
||||
<div className="flex flex-col h-full bg-[#0a0f1a] text-white">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-[#c89b3c]" />
|
||||
Automation Center
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{automations.length} automações · {activeCount} ativas · Central ({centralCount}) + XOS ({xosCount})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-2 p-3 border-b border-white/10 flex-wrap">
|
||||
<div className="relative flex-1 min-w-48">
|
||||
<Search className="absolute left-2.5 top-2.5 w-3.5 h-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar automações..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 h-8 text-sm bg-white/5 border-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Source filter */}
|
||||
<div className="flex rounded-md border border-white/10 overflow-hidden">
|
||||
{(["all", "central", "xos"] as const).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setFilterSource(s)}
|
||||
className={`px-2.5 py-1.5 text-xs transition-colors ${
|
||||
filterSource === s ? "bg-[#c89b3c] text-black font-medium" : "text-white/50 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{s === "all" ? "Todas" : s === "central" ? "Central" : "XOS"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Status filter */}
|
||||
<div className="flex rounded-md border border-white/10 overflow-hidden">
|
||||
{(["all", "active", "inactive"] as const).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setFilterStatus(s)}
|
||||
className={`px-2.5 py-1.5 text-xs transition-colors ${
|
||||
filterStatus === s ? "bg-[#c89b3c] text-black font-medium" : "text-white/50 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{s === "all" ? "Todas" : s === "active" ? "Ativas" : "Inativas"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<ScrollArea className="flex-1">
|
||||
{isLoading && (
|
||||
<div className="flex justify-center py-16">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-white/30" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && filtered.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-white/30 gap-3">
|
||||
<Filter className="w-10 h-10 opacity-30" />
|
||||
<p className="text-sm">Nenhuma automação encontrada</p>
|
||||
{search && <p className="text-xs">Tente outro filtro ou busca</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="divide-y divide-white/5">
|
||||
{filtered.map((a) => {
|
||||
const isRunning = runMutation.isPending && runMutation.variables === a.id;
|
||||
const isToggling = toggleMutation.isPending && toggleMutation.variables?.id === a.id;
|
||||
const triggerIcon = TRIGGER_ICONS[a.triggerType] ?? <Zap className="w-3.5 h-3.5" />;
|
||||
const triggerLabel = TRIGGER_LABEL[a.triggerType] ?? a.triggerType;
|
||||
const lastRun = a.lastExecutedAt
|
||||
? new Date(a.lastExecutedAt).toLocaleDateString("pt-BR")
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key={a.id} className="px-4 py-3 flex items-center gap-3 hover:bg-white/3 transition-colors">
|
||||
|
||||
{/* Icon */}
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${a.isActive ? "bg-[#c89b3c]/15 text-[#c89b3c]" : "bg-white/5 text-white/30"}`}>
|
||||
{triggerIcon}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{a.name}</span>
|
||||
<Badge
|
||||
className={`text-[9px] px-1.5 py-0 shrink-0 ${
|
||||
a.source === "central"
|
||||
? "bg-blue-500/15 text-blue-400 border-blue-500/20"
|
||||
: "bg-purple-500/15 text-purple-400 border-purple-500/20"
|
||||
}`}
|
||||
>
|
||||
{a.source === "central" ? "Central" : "XOS"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-0.5">
|
||||
<span className="text-[11px] text-white/40 flex items-center gap-1">
|
||||
{triggerIcon} {triggerLabel}
|
||||
</span>
|
||||
{a.executionCount > 0 && (
|
||||
<span className="text-[11px] text-white/30">{a.executionCount}x</span>
|
||||
)}
|
||||
{lastRun && (
|
||||
<span className="text-[11px] text-white/30 flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" /> {lastRun}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0 text-white/40 hover:text-white hover:bg-white/10"
|
||||
onClick={() => runMutation.mutate(a.id)}
|
||||
disabled={isRunning || !a.isActive}
|
||||
title="Executar agora"
|
||||
>
|
||||
{isRunning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Play className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
|
||||
<Switch
|
||||
checked={a.isActive}
|
||||
disabled={isToggling}
|
||||
onCheckedChange={(v) => toggleMutation.mutate({ id: a.id, isActive: v })}
|
||||
className="scale-75"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</BrowserFrame>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import React, { useState } from "react";
|
||||
import { TradeInFiltersWidget } from "../components/retail-reports/TradeInFiltersWidget";
|
||||
import { TradeInDetailsModal } from "../components/retail-reports/TradeInDetailsModal";
|
||||
import { TradeInPrintButton } from "../components/retail-reports/TradeInPrintButton";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const fetchTradeIns = async (filters) => {
|
||||
const query = new URLSearchParams(filters).toString();
|
||||
const response = await fetch(`/api/modules/retail-reports/trade-ins?${query}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch trade-ins.");
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const RetailReportsPage: React.FC = () => {
|
||||
const [filters, setFilters] = useState({});
|
||||
const [selectedTradeIn, setSelectedTradeIn] = useState(null);
|
||||
|
||||
const { data: tradeIns, isLoading, error } = useQuery(["tradeIns", filters], () => fetchTradeIns(filters));
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Retail Trade-In Reports</h1>
|
||||
<TradeInFiltersWidget onApplyFilters={setFilters} data-testid="filters-widget" />
|
||||
|
||||
{isLoading && <p>Loading trade-ins...</p>}
|
||||
{error && <p className="text-red-500">Failed to load trade-ins.</p>}
|
||||
|
||||
<table className="table-auto w-full border-collapse border border-gray-300 mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-300 px-4 py-2">ID</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Seller</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Company</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Client</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Date</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Value</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Status</th>
|
||||
<th className="border border-gray-300 px-4 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tradeIns?.map((tradeIn) => (
|
||||
<tr key={tradeIn.id}>
|
||||
<td className="border border-gray-300 px-4 py-2">{tradeIn.id}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">{tradeIn.sellerId}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">{tradeIn.companyId}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">{tradeIn.clientId}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">{new Date(tradeIn.tradeInDate).toLocaleDateString()}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">{tradeIn.tradeInValue}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">{tradeIn.status}</td>
|
||||
<td className="border border-gray-300 px-4 py-2">
|
||||
<button
|
||||
className="text-blue-500 hover:underline"
|
||||
onClick={() => setSelectedTradeIn(tradeIn)}
|
||||
data-testid="view-details-button"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<TradeInPrintButton tradeIns={tradeIns} data-testid="print-button" />
|
||||
|
||||
{selectedTradeIn && (
|
||||
<TradeInDetailsModal
|
||||
tradeIn={selectedTradeIn}
|
||||
onClose={() => setSelectedTradeIn(null)}
|
||||
data-testid="details-modal"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RetailReportsPage;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/bash
|
||||
# ==============================================
|
||||
# Arcádia Suite - Script de Deploy para VPS
|
||||
# ==============================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo " Arcádia Suite - Deploy VPS"
|
||||
echo "=========================================="
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Docker não encontrado. Instalando..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker $USER
|
||||
echo "Docker instalado! Faça logout e login novamente para aplicar as permissões."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
echo "Docker Compose não encontrado. Instalando..."
|
||||
sudo apt-get update && sudo apt-get install -y docker-compose-plugin
|
||||
fi
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Arquivo .env não encontrado. Criando a partir do .env.example..."
|
||||
cp .env.example .env
|
||||
echo ""
|
||||
echo "IMPORTANTE: Edite o arquivo .env com suas configurações antes de continuar!"
|
||||
echo " nano .env"
|
||||
echo ""
|
||||
echo "Depois execute este script novamente."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "1/4 - Parando containers existentes..."
|
||||
docker compose down 2>/dev/null || docker-compose down 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "2/5 - Construindo a aplicação..."
|
||||
docker compose build --no-cache 2>/dev/null || docker-compose build --no-cache
|
||||
|
||||
echo ""
|
||||
echo "3/5 - Iniciando o banco de dados..."
|
||||
docker compose up -d db 2>/dev/null || docker-compose up -d db
|
||||
echo "Aguardando banco ficar pronto..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "4/5 - Aplicando migrations do banco de dados..."
|
||||
docker compose run --rm app npx drizzle-kit push 2>/dev/null || docker-compose run --rm app npx drizzle-kit push
|
||||
|
||||
echo ""
|
||||
echo "5/5 - Iniciando a aplicação..."
|
||||
docker compose up -d 2>/dev/null || docker-compose up -d
|
||||
sleep 10
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Deploy concluído!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " App: http://$(hostname -I | awk '{print $1}'):5000"
|
||||
echo " DB: PostgreSQL na porta 5432"
|
||||
echo ""
|
||||
echo " Comandos úteis:"
|
||||
echo " Ver logs: docker compose logs -f app"
|
||||
echo " Parar tudo: docker compose down"
|
||||
echo " Reiniciar: docker compose restart app"
|
||||
echo " Status: docker compose ps"
|
||||
echo ""
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
|
|
@ -0,0 +1,58 @@
|
|||
# ===========================================
|
||||
# Arcádia Suite - Variáveis de Ambiente
|
||||
# ===========================================
|
||||
# Copie este arquivo para .env na raiz do projeto
|
||||
# e preencha com seus valores
|
||||
|
||||
# -----------------------------
|
||||
# Banco de Dados PostgreSQL
|
||||
# -----------------------------
|
||||
DATABASE_URL=postgresql://arcadia:SUA_SENHA_AQUI@localhost:5432/arcadia_suite
|
||||
PGHOST=localhost
|
||||
PGPORT=5432
|
||||
PGUSER=arcadia
|
||||
PGPASSWORD=SUA_SENHA_AQUI
|
||||
PGDATABASE=arcadia_suite
|
||||
|
||||
# -----------------------------
|
||||
# Ambiente
|
||||
# -----------------------------
|
||||
NODE_ENV=production
|
||||
PORT=5000
|
||||
|
||||
# -----------------------------
|
||||
# Sessão e Segurança
|
||||
# -----------------------------
|
||||
# Gere uma chave aleatória: openssl rand -base64 32
|
||||
SESSION_SECRET=GERE_UMA_CHAVE_ALEATORIA_LONGA_AQUI
|
||||
|
||||
# -----------------------------
|
||||
# ERPNext (opcional)
|
||||
# -----------------------------
|
||||
ERPNEXT_URL=https://seu-erpnext.com
|
||||
ERPNEXT_API_KEY=sua_api_key
|
||||
ERPNEXT_API_SECRET=seu_api_secret
|
||||
|
||||
# -----------------------------
|
||||
# OpenAI (opcional - para IA)
|
||||
# -----------------------------
|
||||
OPENAI_API_KEY=sk-sua-chave-openai
|
||||
|
||||
# -----------------------------
|
||||
# WhatsApp Baileys (opcional)
|
||||
# -----------------------------
|
||||
# As sessões são armazenadas automaticamente em ./baileys_sessions
|
||||
|
||||
# -----------------------------
|
||||
# Fiscal - SEFAZ (opcional)
|
||||
# -----------------------------
|
||||
# Caminho para o certificado digital A1 (.pfx)
|
||||
CERT_PATH=/path/to/certificado.pfx
|
||||
CERT_PASSWORD=senha_do_certificado
|
||||
SEFAZ_AMBIENTE=2 # 1=Produção, 2=Homologação
|
||||
|
||||
# -----------------------------
|
||||
# ChromaDB (opcional)
|
||||
# -----------------------------
|
||||
CHROMA_HOST=localhost
|
||||
CHROMA_PORT=8000
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
# Análise Atendeon → XOS
|
||||
|
||||
> **Projeto:** https://github.com/JonasRodriguesPachceo/atendeon
|
||||
> **Stack:** Node.js (TypeScript) + React + PostgreSQL + Baileys
|
||||
> **Data:** 29/01/2026
|
||||
|
||||
---
|
||||
|
||||
## Resumo do Projeto Atendeon
|
||||
|
||||
O Atendeon é uma **central de atendimento WhatsApp multi-empresa** (SaaS) completa com:
|
||||
|
||||
- **477 arquivos TypeScript** no backend
|
||||
- **41 Models** (Sequelize/PostgreSQL)
|
||||
- **35+ Services** organizados por domínio
|
||||
- Integração completa com **Baileys** (WhatsApp não-oficial)
|
||||
- Suporte a **Typebot**, **N8N** e **OpenAI**
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidades do Atendeon vs XOS Atual
|
||||
|
||||
### ✅ O que o Atendeon tem que NOS FALTA
|
||||
|
||||
| Funcionalidade | Atendeon | XOS Atual | Prioridade |
|
||||
|----------------|----------|-----------|------------|
|
||||
| **Filas de Atendimento (Queues)** | ✅ Completo | ⚪ Não existe | 🔴 Alta |
|
||||
| **Distribuição Round-Robin** | ✅ Por fila/equipe | ⚪ Não existe | 🔴 Alta |
|
||||
| **Notas Internas (TicketNote)** | ✅ Model dedicado | ⚪ Não existe | 🔴 Alta |
|
||||
| **Mensagens Rápidas (QuickMessage)** | ✅ Com atalhos | ⚪ Não existe | 🟠 Média |
|
||||
| **Agendamento de Mensagens** | ✅ Schedule model | ⚪ Não existe | 🟠 Média |
|
||||
| **Tracking de Ticket** | ✅ TicketTraking | ⚪ Não existe | 🟠 Média |
|
||||
| **Horário de Funcionamento** | ✅ outOfHoursMessage | ⚪ Não existe | 🟡 Baixa |
|
||||
| **Rating/Avaliação** | ✅ UserRating | ⚪ Não existe | 🟡 Baixa |
|
||||
| **Campos Customizados** | ✅ ContactCustomField | ✅ customFields JSONB | ✅ OK |
|
||||
| **Multi-WhatsApp** | ✅ Whatsapp model | ⚪ Não integrado | 🔴 Alta |
|
||||
| **Chatbot/Typebot** | ✅ Integração nativa | ⚪ Não existe | 🟠 Média |
|
||||
| **Integração N8N** | ✅ QueueIntegrations | ⚪ Não existe | 🟠 Média |
|
||||
| **Prompts OpenAI** | ✅ Prompt model | ✅ Manus Agent | ✅ OK |
|
||||
| **Campanhas com 5 msgs** | ✅ message1-5 | 🟡 content único | 🟠 Média |
|
||||
| **Disparo em massa** | ✅ CampaignShipping | 🟡 Estrutura só | 🔴 Alta |
|
||||
| **Listas de Contatos** | ✅ ContactList/Item | ⚪ Não existe | 🟠 Média |
|
||||
| **Tags em Tickets** | ✅ TicketTag M:N | 🟡 Array no ticket | ✅ OK |
|
||||
| **Importação de Contatos** | ✅ ImportService | ⚪ Não existe | 🟠 Média |
|
||||
|
||||
---
|
||||
|
||||
## Models do Atendeon que Devemos Criar
|
||||
|
||||
### 1. **Filas de Atendimento** (Queue)
|
||||
```typescript
|
||||
// Atendeon: Queue.ts
|
||||
{
|
||||
name: string,
|
||||
color: string,
|
||||
greetingMessage: string, // Mensagem de boas-vindas
|
||||
outOfHoursMessage: string, // Fora do expediente
|
||||
schedules: JSONB, // Horários de funcionamento
|
||||
orderQueue: number, // Ordem na distribuição
|
||||
}
|
||||
// Relacionamentos:
|
||||
// - BelongsToMany → Users (UserQueue)
|
||||
// - BelongsToMany → Whatsapps (WhatsappQueue)
|
||||
```
|
||||
|
||||
**Sugestão para XOS:**
|
||||
```sql
|
||||
CREATE TABLE xos_queues (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
color VARCHAR(20),
|
||||
greeting_message TEXT,
|
||||
out_of_hours_message TEXT,
|
||||
schedules JSONB, -- [{dayOfWeek: 1, startTime: "09:00", endTime: "18:00"}]
|
||||
order_priority INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
CREATE TABLE xos_queue_users (
|
||||
queue_id INTEGER REFERENCES xos_queues(id),
|
||||
user_id VARCHAR REFERENCES users(id),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
PRIMARY KEY (queue_id, user_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 2. **Notas Internas** (TicketNote)
|
||||
```typescript
|
||||
// Atendeon: TicketNote.ts
|
||||
{
|
||||
note: string,
|
||||
userId: number, // Quem escreveu
|
||||
contactId: number, // Sobre qual contato
|
||||
ticketId: number, // Em qual ticket
|
||||
}
|
||||
```
|
||||
|
||||
**Sugestão para XOS:**
|
||||
```sql
|
||||
CREATE TABLE xos_internal_notes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER REFERENCES xos_conversations(id),
|
||||
contact_id INTEGER REFERENCES xos_contacts(id),
|
||||
user_id VARCHAR REFERENCES users(id),
|
||||
content TEXT NOT NULL,
|
||||
is_pinned BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3. **Mensagens Rápidas** (QuickMessage)
|
||||
```typescript
|
||||
// Atendeon: QuickMessage.ts
|
||||
{
|
||||
shortcode: string, // Ex: "/preco", "/horario"
|
||||
message: string,
|
||||
mediaPath: string, // Anexo opcional
|
||||
mediaName: string,
|
||||
userId: number, // Dono da msg rápida
|
||||
}
|
||||
```
|
||||
|
||||
**Sugestão para XOS:**
|
||||
```sql
|
||||
CREATE TABLE xos_quick_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
shortcode VARCHAR(50) NOT NULL, -- "/preco"
|
||||
title VARCHAR(200),
|
||||
content TEXT NOT NULL,
|
||||
media_url TEXT,
|
||||
media_type VARCHAR(30), -- image, file, audio
|
||||
scope VARCHAR(20) DEFAULT 'personal', -- personal, team, company
|
||||
user_id VARCHAR REFERENCES users(id),
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 4. **Agendamento de Mensagens** (Schedule)
|
||||
```typescript
|
||||
// Atendeon: Schedule.ts
|
||||
{
|
||||
body: string, // Conteúdo da mensagem
|
||||
sendAt: Date, // Quando enviar
|
||||
sentAt: Date, // Quando foi enviada
|
||||
status: string, // pending, sent, failed
|
||||
contactId: number,
|
||||
ticketId: number,
|
||||
mediaPath: string,
|
||||
}
|
||||
```
|
||||
|
||||
**Sugestão para XOS:**
|
||||
```sql
|
||||
CREATE TABLE xos_scheduled_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
contact_id INTEGER REFERENCES xos_contacts(id),
|
||||
conversation_id INTEGER REFERENCES xos_conversations(id),
|
||||
content TEXT NOT NULL,
|
||||
media_url TEXT,
|
||||
media_type VARCHAR(30),
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
sent_at TIMESTAMP,
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, sent, failed, cancelled
|
||||
error_message TEXT,
|
||||
created_by VARCHAR REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 5. **Tracking de Atendimento** (TicketTraking)
|
||||
```typescript
|
||||
// Atendeon: TicketTraking.ts
|
||||
{
|
||||
ticketId: number,
|
||||
startedAt: Date, // Início do atendimento
|
||||
queuedAt: Date, // Quando entrou na fila
|
||||
finishedAt: Date, // Quando finalizou
|
||||
ratingAt: Date, // Quando foi avaliado
|
||||
chatbotAt: Date, // Quando passou pelo bot
|
||||
rated: boolean,
|
||||
}
|
||||
```
|
||||
|
||||
**Sugestão para XOS:**
|
||||
```sql
|
||||
CREATE TABLE xos_conversation_tracking (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER REFERENCES xos_conversations(id) UNIQUE,
|
||||
queued_at TIMESTAMP,
|
||||
first_response_at TIMESTAMP,
|
||||
assigned_at TIMESTAMP,
|
||||
chatbot_ended_at TIMESTAMP,
|
||||
human_started_at TIMESTAMP,
|
||||
resolved_at TIMESTAMP,
|
||||
rated_at TIMESTAMP,
|
||||
rating_score INTEGER, -- 1-5
|
||||
rating_comment TEXT,
|
||||
total_duration_seconds INTEGER, -- calculado
|
||||
response_time_seconds INTEGER -- calculado
|
||||
);
|
||||
```
|
||||
|
||||
### 6. **Conexões WhatsApp** (Whatsapp)
|
||||
```typescript
|
||||
// Atendeon: Whatsapp.ts
|
||||
{
|
||||
name: string,
|
||||
session: string, // Dados da sessão Baileys
|
||||
qrcode: string, // QR code atual
|
||||
status: string, // CONNECTED, DISCONNECTED, QR
|
||||
greetingMessage: string,
|
||||
farewellMessage: string,
|
||||
complationMessage: string, // Ao finalizar ticket
|
||||
ratingMessage: string,
|
||||
expiresTicket: number, // Minutos para expirar
|
||||
expiresInactiveMessage: string,
|
||||
maxUseBotQueues: number,
|
||||
}
|
||||
```
|
||||
|
||||
**Sugestão para XOS:**
|
||||
```sql
|
||||
CREATE TABLE xos_whatsapp_connections (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
phone_number VARCHAR(20),
|
||||
status VARCHAR(30) DEFAULT 'disconnected', -- connected, disconnected, qr_pending
|
||||
qr_code TEXT,
|
||||
session_data JSONB, -- Dados Baileys
|
||||
greeting_message TEXT,
|
||||
farewell_message TEXT,
|
||||
completion_message TEXT,
|
||||
rating_message TEXT,
|
||||
out_of_hours_message TEXT,
|
||||
ticket_expires_minutes INTEGER DEFAULT 1440, -- 24h
|
||||
inactive_message TEXT,
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
last_seen_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE xos_whatsapp_queue_links (
|
||||
whatsapp_id INTEGER REFERENCES xos_whatsapp_connections(id),
|
||||
queue_id INTEGER REFERENCES xos_queues(id),
|
||||
PRIMARY KEY (whatsapp_id, queue_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Services do Atendeon para Referência
|
||||
|
||||
### Distribuição de Tickets
|
||||
O Atendeon usa o conceito de **Filas** para distribuir leads:
|
||||
|
||||
```typescript
|
||||
// FindOrCreateTicketService.ts (simplificado)
|
||||
const ticket = await Ticket.create({
|
||||
contactId,
|
||||
whatsappId,
|
||||
status: "pending", // Aguardando na fila
|
||||
queueId: defaultQueue.id,
|
||||
// NÃO atribui userId ainda - fica na fila
|
||||
});
|
||||
|
||||
// UpdateTicketService.ts - Quando atendente pega o ticket
|
||||
ticket.userId = attendantId;
|
||||
ticket.status = "open";
|
||||
```
|
||||
|
||||
### Lógica de Round-Robin
|
||||
```typescript
|
||||
// Distribui entre usuários da fila
|
||||
const usersInQueue = await UserQueue.findAll({
|
||||
where: { queueId },
|
||||
include: [{ model: User, where: { online: true }}]
|
||||
});
|
||||
|
||||
// Pega o próximo da lista (round-robin)
|
||||
const nextUser = usersInQueue[currentIndex % usersInQueue.length];
|
||||
```
|
||||
|
||||
### Chatbot com Opções
|
||||
```typescript
|
||||
// QueueOption.ts - Menu do chatbot
|
||||
{
|
||||
title: string, // "Vendas"
|
||||
option: string, // "1"
|
||||
message: string, // Resposta quando selecionar
|
||||
parentId: number, // Para sub-menus
|
||||
queueId: number,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plano de Implementação
|
||||
|
||||
### Fase 1: Estrutura Base (1-2 semanas)
|
||||
1. ✅ Criar tabelas: `xos_queues`, `xos_queue_users`
|
||||
2. ✅ Criar tabelas: `xos_internal_notes`, `xos_quick_messages`
|
||||
3. ✅ Criar tabelas: `xos_scheduled_messages`
|
||||
4. ✅ Criar tabelas: `xos_conversation_tracking`
|
||||
5. ✅ Atualizar APIs XOS com novos endpoints
|
||||
|
||||
### Fase 2: Distribuição de Leads (1 semana)
|
||||
1. Implementar lógica de filas
|
||||
2. Round-robin entre atendentes
|
||||
3. Status: pending → open → resolved
|
||||
|
||||
### Fase 3: Integração WhatsApp (2-3 semanas)
|
||||
1. Criar `xos_whatsapp_connections`
|
||||
2. Integrar Baileys existente do projeto
|
||||
3. Sincronizar mensagens com `xos_messages`
|
||||
4. QR Code e gestão de sessões
|
||||
|
||||
### Fase 4: Features Avançadas (2 semanas)
|
||||
1. Chatbot com menu de opções
|
||||
2. Horário de funcionamento
|
||||
3. Mensagens automáticas
|
||||
4. Sistema de avaliação
|
||||
|
||||
---
|
||||
|
||||
## Código que Podemos Reutilizar
|
||||
|
||||
### 1. `wbotMessageListener.ts` (62KB)
|
||||
- Lógica completa de recebimento de mensagens
|
||||
- Tratamento de mídia (áudio, imagem, vídeo)
|
||||
- Integração com OpenAI
|
||||
- Fluxo de chatbot
|
||||
|
||||
### 2. `providers.ts` (79KB)
|
||||
- Abstração do Baileys
|
||||
- Gestão de sessões
|
||||
- Envio de mensagens
|
||||
|
||||
### 3. `UpdateTicketService.ts`
|
||||
- Lógica de transferência entre filas
|
||||
- Tracking de tempos
|
||||
- Notificações
|
||||
|
||||
### 4. `CampaignService/`
|
||||
- Disparo em massa
|
||||
- Agendamento
|
||||
- Estatísticas
|
||||
|
||||
---
|
||||
|
||||
## Comparativo Final
|
||||
|
||||
| Aspecto | Atendeon | XOS |
|
||||
|---------|----------|-----|
|
||||
| **Foco** | WhatsApp CRM puro | Plataforma multi-propósito |
|
||||
| **Multi-tenant** | Por company | Por tenant |
|
||||
| **ORM** | Sequelize | Drizzle |
|
||||
| **Real-time** | Socket.IO | Socket.IO |
|
||||
| **WhatsApp** | Baileys integrado | Baileys separado |
|
||||
| **AI** | OpenAI básico | Manus Agent completo |
|
||||
| **Código** | ~500 arquivos | Integrado na Suite |
|
||||
|
||||
### Vantagens do Atendeon
|
||||
- Sistema maduro e testado
|
||||
- Lógica de filas pronta
|
||||
- Chatbot funcional
|
||||
|
||||
### Vantagens do XOS
|
||||
- Arquitetura mais moderna
|
||||
- IA mais avançada (Manus)
|
||||
- Integração com ERP
|
||||
- Multi-canal nativo
|
||||
|
||||
---
|
||||
|
||||
## Recomendação
|
||||
|
||||
**Estratégia:** Não copiar código diretamente, mas **usar como referência** para:
|
||||
|
||||
1. **Schema de dados** → Adaptar models do Atendeon para Drizzle/PostgreSQL
|
||||
2. **Lógica de negócio** → Implementar distribuição de leads similar
|
||||
3. **Fluxos de chatbot** → Replicar QueueOption para menus
|
||||
4. **Tracking** → Copiar conceito de TicketTraking
|
||||
|
||||
**Estimativa de Desenvolvimento:**
|
||||
- Estrutura básica: 2-3 semanas
|
||||
- Integração WhatsApp: 2-3 semanas
|
||||
- Features completas: 4-6 semanas
|
||||
|
||||
**Total:** ~2 meses para paridade com Atendeon
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
# Análise de Requisitos CRM - XOS vs Documento
|
||||
|
||||
> **Data:** 29/01/2026
|
||||
> **Documento Base:** CRM_1769651134584.pdf
|
||||
|
||||
---
|
||||
|
||||
## Resumo Executivo
|
||||
|
||||
| Categoria | Status Geral | Cobertura |
|
||||
|-----------|--------------|-----------|
|
||||
| **Estrutura de Dados** | ✅ Implementado | 90% |
|
||||
| **Live Chat + CRM** | 🟡 Parcial | 50% |
|
||||
| **Distribuição de Leads** | 🟡 Estrutura | 30% |
|
||||
| **Carteirização** | 🟡 Estrutura | 40% |
|
||||
| **Automações Low-Code** | 🟡 Estrutura | 35% |
|
||||
| **Follow-up Lists** | ⚪ Não impl. | 10% |
|
||||
| **Disparos em Massa** | 🟡 Estrutura | 25% |
|
||||
| **Gestão Meta/Templates** | ⚪ Não impl. | 0% |
|
||||
| **Multi-Conexões API** | ⚪ Não impl. | 0% |
|
||||
| **API/Webhook** | ⚪ Não impl. | 10% |
|
||||
| **Controle de Acessos** | 🟡 Parcial | 40% |
|
||||
| **Aba de Contatos** | ✅ Implementado | 85% |
|
||||
| **Métricas/Dashboards** | 🟡 Parcial | 45% |
|
||||
|
||||
**Cobertura Geral Estimada: ~40%**
|
||||
|
||||
---
|
||||
|
||||
## Análise Detalhada por Requisito
|
||||
|
||||
### 1. Live Chat (WhatsApp API) + CRM
|
||||
|
||||
**Requisito:** Chat com lead onde pode conversar, mandar audio, documento, imagens.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Chat em tempo real | 🟡 | Estrutura de conversas e mensagens existe (`xos_conversations`, `xos_messages`) |
|
||||
| Envio de áudio/doc/imagem | 🟡 | Campo `content_type` e `attachments` no schema |
|
||||
| Notas internas no chat | ⚪ | Não implementado - precisa campo `is_internal` |
|
||||
| Mini CRM dentro do chat | 🟡 | Dados do contato vinculados via `contact_id` |
|
||||
| Troca de etapas do pipeline | ✅ | Pipeline e stages implementados |
|
||||
| Transferir lead p/ outro membro | 🟡 | Campo `assigned_to` existe, UI não implementada |
|
||||
| Finalizar lead (ganho/perda) | ✅ | Campos `status`, `won_reason`, `lost_reason` no Deal |
|
||||
| Informações de contato padrões | ✅ | Nome, email, telefone, tags, etc. |
|
||||
| Campos personalizados | ✅ | Campo `custom_fields` (JSONB) |
|
||||
| Rastreamento UTM | ✅ | Campos `source`, `source_details` |
|
||||
| Filtros avançados | 🟡 | Filtro básico por tipo/status/busca, **falta:** tag, etapa funil, status Meta, sem resposta, data |
|
||||
| Ordenação múltipla | ⚪ | Apenas por data, falta opções avançadas |
|
||||
|
||||
**Cobertura: 50%** - Estrutura existe, falta UI de chat real e filtros avançados.
|
||||
|
||||
---
|
||||
|
||||
### 2. Distribuição Automática de Leads
|
||||
|
||||
**Requisito:** Lead chega e é atribuído automaticamente via fila.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Fila round-robin | ⚪ | Não implementado |
|
||||
| Distribuição por percentual | ⚪ | Não implementado |
|
||||
| Configuração de regras | ⚪ | Não implementado |
|
||||
|
||||
**O que temos:**
|
||||
- Campo `assigned_to` em Contacts e Deals
|
||||
- Estrutura de automações (`xos_automations`) que poderia acionar distribuição
|
||||
|
||||
**Cobertura: 30%** - Estrutura permite, lógica não implementada.
|
||||
|
||||
---
|
||||
|
||||
### 3. Carteirização
|
||||
|
||||
**Requisito:** Lead "pertence" a um consultor por X período.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Ownership por período | ⚪ | Não tem campo de expiração |
|
||||
| Redirecionamento automático | ⚪ | Não implementado |
|
||||
| Histórico de ownership | ⚪ | Não implementado |
|
||||
|
||||
**O que temos:**
|
||||
- Campo `assigned_to` no contato
|
||||
- Campo `last_contact_at` para tracking
|
||||
|
||||
**Cobertura: 40%** - Precisa adicionar `ownership_expires_at` e lógica.
|
||||
|
||||
---
|
||||
|
||||
### 4. Automações Low-Code
|
||||
|
||||
**Requisito:** Automações no-code/low-code com opções pré-dispostas.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Triggers (mensagem, clique, etc.) | ✅ | Campo `trigger_type` e `trigger_config` |
|
||||
| Ações (tag, mensagem, distribuir) | ✅ | Campo `actions` (array de ações) |
|
||||
| Condições | ✅ | Campo `conditions` (array) |
|
||||
| UI visual de criação | 🟡 | Tela básica criada, não funcional |
|
||||
| Execução real | ⚪ | Não implementado no backend |
|
||||
|
||||
**O que temos:**
|
||||
```sql
|
||||
xos_automations (
|
||||
trigger_type: "contact_created", "deal_stage_changed", "form_submitted"
|
||||
trigger_config: { ... }
|
||||
actions: [{ type: "add_tag", config: {...} }, { type: "send_message", config: {...} }]
|
||||
conditions: [{ field: "...", operator: "...", value: "..." }]
|
||||
)
|
||||
```
|
||||
|
||||
**Cobertura: 35%** - Schema pronto, execução não implementada.
|
||||
|
||||
---
|
||||
|
||||
### 5. Listas de Follow-up
|
||||
|
||||
**Requisito:** Lista de mensagens automáticas com delays.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Criação de lista | ⚪ | Não existe tabela específica |
|
||||
| Sequência com delays | ⚪ | Não implementado |
|
||||
| Saída automática ao responder | ⚪ | Não implementado |
|
||||
| Integração com automações | ⚪ | Poderia usar `xos_automations` |
|
||||
|
||||
**Cobertura: 10%** - Precisa nova tabela `xos_follow_up_sequences`.
|
||||
|
||||
---
|
||||
|
||||
### 6. Central de Disparos em Massa (WhatsApp API)
|
||||
|
||||
**Requisito:** Fluxo para disparos em massa com agendamento.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Campanhas WhatsApp | ✅ | `xos_campaigns` com type "whatsapp" |
|
||||
| Segmentação de contatos | ✅ | Campo `segment_query` (JSONB) |
|
||||
| Agendamento | ✅ | Campo `scheduled_at` |
|
||||
| Estatísticas | ✅ | Campo `stats` (sent, delivered, opened, etc.) |
|
||||
| Fluxo de automação | 🟡 | Básico, não visual |
|
||||
| Execução real | ⚪ | Não implementado |
|
||||
|
||||
**Cobertura: 25%** - Estrutura completa, execução não implementada.
|
||||
|
||||
---
|
||||
|
||||
### 7. Gestão de Templates Meta (WhatsApp)
|
||||
|
||||
**Requisito:** Criar/gerenciar templates da Meta Business.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Listagem de templates | ⚪ | Não implementado |
|
||||
| Criação de templates | ⚪ | Não implementado |
|
||||
| Aprovação Meta | ⚪ | Não implementado |
|
||||
| Integração API Meta | ⚪ | Não implementado |
|
||||
|
||||
**Cobertura: 0%** - Requer integração com Meta Business API.
|
||||
|
||||
---
|
||||
|
||||
### 8. Múltiplas Conexões de API WhatsApp
|
||||
|
||||
**Requisito:** Conectar mais de um número WhatsApp.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Multi-número | ⚪ | Não implementado |
|
||||
| Gestão de conexões | ⚪ | Não implementado |
|
||||
| Configuração por equipe | ⚪ | Não implementado |
|
||||
|
||||
**O que temos:**
|
||||
- Sistema WhatsApp via Baileys existe no projeto (pasta `whatsapp/`)
|
||||
- Falta integração com XOS
|
||||
|
||||
**Cobertura: 0%** - Precisa integrar WhatsApp existente ao XOS.
|
||||
|
||||
---
|
||||
|
||||
### 9. API do CRM + Webhook
|
||||
|
||||
**Requisito:** API externa e webhooks para integrações.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| API REST | ✅ | Rotas `/api/xos/*` completas |
|
||||
| Webhooks de entrada | ⚪ | Não implementado |
|
||||
| Webhooks de saída | ⚪ | Não implementado |
|
||||
| Documentação API | ⚪ | Não documentado |
|
||||
|
||||
**Cobertura: 10%** - API existe, webhooks não.
|
||||
|
||||
---
|
||||
|
||||
### 10. Gerenciar Acessos (Hierarquia)
|
||||
|
||||
**Requisito:** Diferenciação de acessos por perfil.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Perfis de usuário | ✅ | Tabela `users` com `role` |
|
||||
| Permissões por recurso | 🟡 | Sistema de roles básico existe |
|
||||
| Visibilidade filtrada | ⚪ | Não implementado por tenant/equipe |
|
||||
| Configuração de acessos | ⚪ | Não tem UI |
|
||||
|
||||
**O que temos:**
|
||||
- Multi-tenancy (`tenant_id` em todas tabelas XOS)
|
||||
- Usuários com roles
|
||||
|
||||
**Cobertura: 40%** - Estrutura existe, UI e lógica fina não.
|
||||
|
||||
---
|
||||
|
||||
### 11. Aba de Contatos
|
||||
|
||||
**Requisito:** Listagem com busca, filtros, import/export.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Listagem completa | ✅ | Implementado |
|
||||
| Busca por nome/email | ✅ | Implementado |
|
||||
| Filtro por tipo/status | ✅ | Implementado |
|
||||
| Filtro por tag | ⚪ | Não implementado |
|
||||
| Detalhes do contato | ✅ | Implementado com relacionamentos |
|
||||
| Exportação | ⚪ | Não implementado |
|
||||
| Importação em massa | ⚪ | Não implementado |
|
||||
|
||||
**Cobertura: 85%** - Falta import/export e filtro por tags.
|
||||
|
||||
---
|
||||
|
||||
### 12. Métricas e Dashboards
|
||||
|
||||
**Requisito:** Dashboards personalizáveis com KPIs.
|
||||
|
||||
| Sub-requisito | Status | Detalhe |
|
||||
|---------------|--------|---------|
|
||||
| Dashboard geral | ✅ | XosCentral com stats |
|
||||
| Métricas de vendas | ✅ | Deals por stage, valor total |
|
||||
| Métricas de atendimento | 🟡 | Conversas, tempo resposta básico |
|
||||
| Métricas por vendedor | ⚪ | Não filtrado por usuário |
|
||||
| Personalização | ⚪ | Layout fixo |
|
||||
| Filtro de período | ⚪ | Não implementado |
|
||||
|
||||
**O que temos:**
|
||||
```javascript
|
||||
// Endpoint /api/xos/stats retorna:
|
||||
{
|
||||
contacts: { total, leads, customers },
|
||||
deals: { total, open, won, lost, totalValue },
|
||||
conversations: { total, open, resolved },
|
||||
campaigns: { total, active }
|
||||
}
|
||||
```
|
||||
|
||||
**Cobertura: 45%** - Básico funciona, falta personalização e filtros.
|
||||
|
||||
---
|
||||
|
||||
## Comparativo com CRMs de Referência
|
||||
|
||||
| Recurso | Kommo | Clint | Front CRM | **XOS Atual** |
|
||||
|---------|-------|-------|-----------|---------------|
|
||||
| Live Chat WhatsApp | ✅ | ✅ | ✅ | 🟡 |
|
||||
| Visual limpo | ⚪ | ✅ | ✅ | ✅ |
|
||||
| Distribuição leads | ✅ | ✅ | ✅ | 🟡 |
|
||||
| Automações | ✅ | 💰 | ✅ | 🟡 |
|
||||
| Follow-ups | ✅ | ✅ | ✅ | ⚪ |
|
||||
| Disparos massa | ✅ | ✅ | ✅ | 🟡 |
|
||||
| Templates Meta | ✅ | ✅ | ✅ | ⚪ |
|
||||
| Dashboards custom | ✅ | ✅ | ⚪ | ⚪ |
|
||||
| API/Webhooks | ✅ | ✅ | ✅ | 🟡 |
|
||||
| Multi-número | ✅ | ✅ | ✅ | ⚪ |
|
||||
|
||||
---
|
||||
|
||||
## Plano de Ação Prioritário
|
||||
|
||||
### Prioridade 1 (Crítico para Operação)
|
||||
|
||||
1. **Integrar WhatsApp ao XOS**
|
||||
- Conectar sistema Baileys existente
|
||||
- Criar canal `whatsapp` funcional em `xos_conversations`
|
||||
- UI de chat real-time
|
||||
|
||||
2. **Filtros Avançados no Chat**
|
||||
- Por tag, etapa, status, data
|
||||
- Ordenação múltipla
|
||||
|
||||
3. **Distribuição Automática de Leads**
|
||||
- Criar tabela `xos_lead_distribution_rules`
|
||||
- Implementar lógica round-robin
|
||||
|
||||
### Prioridade 2 (Diferencial Competitivo)
|
||||
|
||||
4. **Listas de Follow-up**
|
||||
- Criar tabela `xos_follow_up_sequences`
|
||||
- Engine de execução com delays
|
||||
|
||||
5. **Execução Real de Automações**
|
||||
- Worker para processar triggers
|
||||
- Ações reais (enviar mensagem, add tag, etc.)
|
||||
|
||||
6. **Carteirização**
|
||||
- Campo `ownership_expires_at`
|
||||
- Lógica de redirecionamento
|
||||
|
||||
### Prioridade 3 (Escala e Profissionalização)
|
||||
|
||||
7. **Gestão de Templates Meta**
|
||||
- Integração Meta Business API
|
||||
|
||||
8. **Multi-Número WhatsApp**
|
||||
- Tabela de conexões
|
||||
- Seletor de número por equipe
|
||||
|
||||
9. **Dashboards Personalizáveis**
|
||||
- Widgets configuráveis
|
||||
- Filtros por período/vendedor
|
||||
|
||||
10. **Import/Export de Contatos**
|
||||
- CSV/Excel upload
|
||||
- Exportação com filtros
|
||||
|
||||
---
|
||||
|
||||
## Schema Sugerido para Expansão
|
||||
|
||||
```sql
|
||||
-- Follow-up Sequences
|
||||
CREATE TABLE xos_follow_up_sequences (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
trigger_event VARCHAR(50), -- 'contact_created', 'deal_created'
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE xos_follow_up_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sequence_id INTEGER REFERENCES xos_follow_up_sequences(id),
|
||||
step_order INTEGER,
|
||||
delay_hours INTEGER,
|
||||
action_type VARCHAR(30), -- 'send_template', 'send_message', 'add_tag'
|
||||
action_config JSONB,
|
||||
exit_on_reply BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
CREATE TABLE xos_follow_up_enrollments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sequence_id INTEGER REFERENCES xos_follow_up_sequences(id),
|
||||
contact_id INTEGER REFERENCES xos_contacts(id),
|
||||
current_step INTEGER DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'active', -- 'active', 'completed', 'exited'
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
next_action_at TIMESTAMP,
|
||||
exited_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Lead Distribution
|
||||
CREATE TABLE xos_distribution_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
name VARCHAR(100),
|
||||
rule_type VARCHAR(30), -- 'round_robin', 'percentage', 'load_balanced'
|
||||
config JSONB, -- { users: [{ user_id, percentage }] }
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
-- WhatsApp Connections
|
||||
CREATE TABLE xos_whatsapp_connections (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
name VARCHAR(100),
|
||||
phone_number VARCHAR(20),
|
||||
status VARCHAR(20), -- 'connected', 'disconnected', 'qr_pending'
|
||||
session_data JSONB,
|
||||
assigned_teams JSONB, -- array of team IDs
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Meta Templates
|
||||
CREATE TABLE xos_whatsapp_templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
connection_id INTEGER REFERENCES xos_whatsapp_connections(id),
|
||||
meta_template_id VARCHAR(100),
|
||||
name VARCHAR(100),
|
||||
category VARCHAR(50),
|
||||
language VARCHAR(10),
|
||||
status VARCHAR(30), -- 'APPROVED', 'PENDING', 'REJECTED'
|
||||
components JSONB,
|
||||
synced_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusão
|
||||
|
||||
O XOS tem uma **base sólida** com ~40% dos requisitos cobertos estruturalmente. Os principais gaps são:
|
||||
|
||||
1. **Integração real com WhatsApp** (estrutura existe, execução não)
|
||||
2. **Listas de Follow-up** (não existe)
|
||||
3. **Execução de automações** (schema existe, engine não)
|
||||
4. **Templates Meta** (não existe)
|
||||
5. **Dashboards personalizáveis** (básico existe)
|
||||
|
||||
A boa notícia é que a arquitetura multi-tenant e os schemas JSONB flexíveis facilitam a expansão. Recomendo focar primeiro na integração WhatsApp + Filtros Avançados, que são os requisitos mais críticos para operação comercial diária.
|
||||
|
|
@ -0,0 +1,600 @@
|
|||
│
|
||||
│ 2️⃣ ANÁLISE TRIBUTÁRIA (Estruturante) │
|
||||
│ • Simulações de regime │
|
||||
│ • Comparativos │
|
||||
│ • Identificação de créditos │
|
||||
│ • Alertas de risco │
|
||||
│ │
|
||||
│ 3️⃣ PLANEJAMENTO (Governança) │
|
||||
│ • Dashboard de impostos │
|
||||
│ • Projeções │
|
||||
│ • Compliance check │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.5 RH
|
||||
|
||||
```
|
||||
RECEBE AJUSTA ANALISA SIMULA
|
||||
─────────────────────────────────────────────────────────────────────
|
||||
• Estrutura org. • Alocação custo • Custo real • CLT × PJ
|
||||
• Colaboradores • Tipo despesa • Produtividade • Cenários
|
||||
• Eventos • Aprovar férias • Competências • Headcount
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. O MANUS (AGENTE CENTRAL)
|
||||
|
||||
### 5.1 Posição na Arquitetura
|
||||
|
||||
O Manus **não está em uma camada**. Ele **permeia todas**:
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ MANUS │
|
||||
│ (vê tudo) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
▼ ▼ ▼
|
||||
Apps Core Estruturantes Execução
|
||||
```
|
||||
|
||||
### 5.2 Capacidades
|
||||
|
||||
| Capacidade | Descrição |
|
||||
|------------|-----------|
|
||||
| **Observação** | Acesso a todos os dados via Knowledge Graph |
|
||||
| **Raciocínio** | Conecta pontos, identifica padrões |
|
||||
| **Ação** | Alerta, sugere, prepara, executa (com limites) |
|
||||
| **Aprendizado** | Melhora com cada interação |
|
||||
|
||||
### 5.3 Níveis de Autonomia
|
||||
|
||||
| Nível | O Que Faz | Exemplo |
|
||||
|-------|-----------|---------|
|
||||
| Informar | Notifica | "Estoque crítico de iPhone 13" |
|
||||
| Sugerir | Propõe ação | "Sugiro pedir 10 unidades" |
|
||||
| Preparar | Cria rascunho | "Preparei o pedido, quer enviar?" |
|
||||
| Executar | Age (se autorizado) | "Enviei o pedido automaticamente" |
|
||||
|
||||
### 5.4 Limites do Manus
|
||||
|
||||
| Ação | Autonomia |
|
||||
|------|-----------|
|
||||
| Alertar | ✅ Sempre |
|
||||
| Sugerir | ✅ Sempre |
|
||||
| Criar rascunho | ✅ Sempre |
|
||||
| Enviar mensagem | 🔶 Configurável |
|
||||
| Criar pedido | 🔶 Configurável |
|
||||
| Aprovar pagamento | ❌ Nunca |
|
||||
| Excluir dados | ❌ Nunca |
|
||||
|
||||
---
|
||||
|
||||
## 6. SISTEMA DE AUTOMAÇÕES
|
||||
|
||||
### 6.1 Relação com o Manus
|
||||
|
||||
```
|
||||
MANUS (Cérebro)
|
||||
│
|
||||
▼
|
||||
AUTOMAÇÕES (Sistema Nervoso)
|
||||
│
|
||||
▼
|
||||
MÓDULOS (Órgãos)
|
||||
```
|
||||
|
||||
### 6.2 Três Níveis de Ação
|
||||
|
||||
| Nível | Quem Decide | Quem Executa |
|
||||
|-------|-------------|--------------|
|
||||
| Manual | Humano | Humano |
|
||||
| Automação | Regra pré-definida | Sistema |
|
||||
| Agêntico | Manus (IA) | Sistema |
|
||||
|
||||
### 6.3 Exemplo de Fluxo
|
||||
|
||||
```
|
||||
EVENTO: Cliente comprou iPhone 14 por R$ 4.500
|
||||
|
||||
1. AUTOMAÇÃO (regra fixa)
|
||||
IF venda > R$1.000 THEN enviar_email_agradecimento
|
||||
→ Executa automaticamente
|
||||
|
||||
2. MANUS (inteligência)
|
||||
"Este cliente comprou 3x nos últimos 60 dias.
|
||||
Sugestão: Criar programa VIP."
|
||||
→ Sugere ao vendedor
|
||||
|
||||
3. MANUS + AUTOMAÇÃO (híbrido)
|
||||
Manus cria nova automação:
|
||||
"Clientes com 3+ compras em 60 dias → convite VIP"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. CONCEITO DE APPS
|
||||
|
||||
### 7.1 Analogia
|
||||
|
||||
| Smartphone | Arcádia Suite |
|
||||
|------------|---------------|
|
||||
| iOS/Android | Arcádia Core |
|
||||
| Apps nativos | Módulos Core |
|
||||
| Apps instaláveis | Apps complementares |
|
||||
| App Store | Arcádia Marketplace |
|
||||
| Siri/Assistant | Manus |
|
||||
|
||||
### 7.2 Tipos de Apps
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIPOS DE APPS │
|
||||
│ │
|
||||
│ CORE (Sempre presentes) │
|
||||
│ └── Financeiro, Contábil, Fiscal, RH, Comunicação, BI │
|
||||
│ │
|
||||
│ EXECUÇÃO (Por modelo de negócio) │
|
||||
│ └── Retail, Serviços, Produção, Logística, Food │
|
||||
│ │
|
||||
│ COMPLEMENTARES (Funções extras) │
|
||||
│ └── LMS, Marketing, CRM, Documentos, Agenda, OKRs │
|
||||
│ │
|
||||
│ SEGMENTO (Verticais específicas) │
|
||||
│ └── Eng. Ambiental, Jurídico, Saúde, Agro, Construção │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.3 Comunicação Entre Apps (Event Bus)
|
||||
|
||||
```
|
||||
App Retail ──────┐
|
||||
│
|
||||
App Marketing ───┼───► EVENTO: "sale.completed"
|
||||
│ │
|
||||
App LMS ─────────┘ ▼
|
||||
┌──────────────┐
|
||||
│ Quem escuta: │
|
||||
│ • Financeiro │ → Gera recebível
|
||||
│ • Fiscal │ → Emite NF-e
|
||||
│ • CRM │ → Atualiza cliente
|
||||
│ • Marketing │ → Envia email
|
||||
│ • Manus │ → Analisa padrão
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. CAMADA DE CONSULTORIA
|
||||
|
||||
### 8.1 Módulos
|
||||
|
||||
| Módulo | Função | Usuários |
|
||||
|--------|--------|----------|
|
||||
| **Process Compass** | Diagnóstico, mapeamento, roadmap | Consultores, gestores |
|
||||
| **Produção** | Gestão de projetos de implementação | Gestores de projeto |
|
||||
| **Suporte** | Atendimento e resolução | Equipe de suporte |
|
||||
|
||||
### 8.2 Modelo de Acesso
|
||||
|
||||
| Perfil | Vê Consultoria? | Vê Cliente? |
|
||||
|--------|-----------------|-------------|
|
||||
| Usuário final | ❌ | ✅ Só seu tenant |
|
||||
| Gestor empresa | ❌ | ✅ Só seu tenant |
|
||||
| Consultor Arcádia | ✅ | ✅ Todos os clientes |
|
||||
| Parceiro | ✅ | ✅ Seus clientes |
|
||||
| Gestor avançado | ✅ (Process Compass) | ✅ Seu tenant |
|
||||
|
||||
---
|
||||
|
||||
## 9. CAMADA DE PLATAFORMA
|
||||
|
||||
### 9.1 Componentes
|
||||
|
||||
| Componente | Direção | Função |
|
||||
|------------|---------|--------|
|
||||
| **Central API** | Arcádia → Mundo | Orquestra conexões de saída |
|
||||
| **Hub API** | Mundo → Arcádia | Expõe dados para parceiros |
|
||||
| **MCP/A2A** | IAs ↔ Arcádia | Protocolo agêntico |
|
||||
| **Ciência Dados** | Interno | Cria modelos de IA |
|
||||
| **IDE** | Interno | Desenvolve extensões |
|
||||
|
||||
### 9.2 Protocolos Agênticos (Já Implementados)
|
||||
|
||||
| Protocolo | Endpoint | Status |
|
||||
|-----------|----------|--------|
|
||||
| MCP | `/api/mcp/v1/tools` | ✅ 56 ferramentas |
|
||||
| A2A | `/.well-known/agent.json` | ✅ Funcionando |
|
||||
| Knowledge Graph | Interno | ✅ Funcionando |
|
||||
| Learning System | Interno | ✅ Funcionando |
|
||||
|
||||
---
|
||||
|
||||
## 10. INTEGRAÇÃO COM ESCRITÓRIO CONTÁBIL
|
||||
|
||||
### 10.1 Modelo de Três Níveis
|
||||
|
||||
```
|
||||
EMPRESA (Operação)
|
||||
↓ gera fatos
|
||||
ARCÁDIA (Retaguarda Inteligente)
|
||||
↓ dados pré-classificados
|
||||
ESCRITÓRIO PARCEIRO (Execução Legal)
|
||||
```
|
||||
|
||||
### 10.2 Fluxo Bidirecional
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ ARCÁDIA │ ──API──→│ ESCRITÓRIO │
|
||||
│ │ │ │
|
||||
│ Envia: │ │ Recebe: │
|
||||
│ - Lançamentos │ │ - Dados prontos│
|
||||
│ - Notas │ │ - Classificados│
|
||||
│ - Eventos RH │ │ │
|
||||
│ │ ←─API── │ Retorna: │
|
||||
│ Recebe: │ │ - Status │
|
||||
│ - Validações │ │ - Alertas │
|
||||
│ - Obrigações OK│ │ - Pendências │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 10.3 Integrações Prioritárias
|
||||
|
||||
1. Domínio Sistemas (Thomson Reuters)
|
||||
2. Alterdata
|
||||
3. Fortes
|
||||
4. Prosoft
|
||||
5. Questor
|
||||
|
||||
---
|
||||
|
||||
## 11. ARCÁDIA PLUS (Motor ERP Laravel)
|
||||
|
||||
### 11.1 Visão Geral
|
||||
|
||||
O **Arcádia Plus** é o ERP canônico desenvolvido em PHP/Laravel, funcionando como um "lote dentro da cidade" Arcádia Suite.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🏙️ CIDADE ARCÁDIA SUITE │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ Retail │ │ Fisco │ │ Manus │ │ BI │ │
|
||||
│ │ 🏠 │ │ 🏠 │ │ 🏠 │ │ 🏠 │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │
|
||||
│ ═══════════════════════════════════════════════════ │
|
||||
│ 🛣️ AVENIDA API │
|
||||
│ ═══════════════════════════════════════════════════ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 🏭 LOTE ARCÁDIA PLUS │ │
|
||||
│ │ (Laravel/PHP) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
|
||||
│ │ │Comp │ │Estoq│ │Finan│ │ RH │ │ NF-e│ │ │
|
||||
│ │ │ras │ │ ue │ │eiro│ │ │ │ │ │ │
|
||||
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 11.2 Características do Plus
|
||||
|
||||
| Aspecto | Detalhe |
|
||||
|---------|---------|
|
||||
| **Stack** | PHP 8 + Laravel 10+ |
|
||||
| **Banco** | MySQL (migração para PostgreSQL planejada) |
|
||||
| **Fiscal** | phpnfe (NF-e/NFC-e) |
|
||||
| **Multi-tenant** | SaaS pronto |
|
||||
| **Origem** | GitHub (repositório existente) |
|
||||
|
||||
### 11.3 Modelo de Integração
|
||||
|
||||
```
|
||||
USUÁRIO
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Arcádia Suite │
|
||||
│ (Frontend) │
|
||||
└───────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Central API │
|
||||
│ (Orquestrador)│
|
||||
└───────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Retail │ │ PLUS │ │ Fisco │
|
||||
│ (Node) │ │(Laravel)│ │(Python) │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
PostgreSQL MySQL PostgreSQL
|
||||
```
|
||||
|
||||
### 11.4 Acesso via Rota `/plus`
|
||||
|
||||
O Plus é acessado transparentemente dentro do Suite:
|
||||
|
||||
```
|
||||
arcadia.app/ → Cockpit
|
||||
arcadia.app/retail → Arcádia Retail
|
||||
arcadia.app/erp → Arcádia ERP
|
||||
arcadia.app/plus → Arcádia Plus (Laravel embutido)
|
||||
arcadia.app/fisco → Arcádia Fisco
|
||||
```
|
||||
|
||||
**Implementação via Proxy Reverso:**
|
||||
|
||||
```typescript
|
||||
// server/routes.ts
|
||||
app.use('/plus', createProxyMiddleware({
|
||||
target: process.env.ARCADIA_PLUS_URL,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/plus': '' }
|
||||
}));
|
||||
```
|
||||
|
||||
### 11.5 Comunicação Suite ↔ Plus
|
||||
|
||||
```
|
||||
SUITE (Orquestrador) PLUS (Motor ERP)
|
||||
────────────────────────────────────────────────────
|
||||
|
||||
1. Suite solicita dados
|
||||
│
|
||||
▼
|
||||
GET /api/v1/customers ────────► Plus retorna JSON
|
||||
│
|
||||
2. Plus executa operação │
|
||||
│ │
|
||||
▼ ▼
|
||||
POST /api/v1/orders ◄──────── Suite envia pedido
|
||||
|
||||
3. Plus emite evento
|
||||
│
|
||||
▼
|
||||
webhook: order.created ───────► Suite captura
|
||||
│
|
||||
▼
|
||||
Knowledge Graph armazena
|
||||
│
|
||||
▼
|
||||
Manus analisa
|
||||
```
|
||||
|
||||
### 11.6 SSO (Login Único)
|
||||
|
||||
```
|
||||
1. Usuário loga no Suite
|
||||
2. Suite gera token JWT
|
||||
3. Token é passado para o Plus via header
|
||||
4. Plus valida e cria sessão Laravel
|
||||
```
|
||||
|
||||
### 11.7 Desenvolvimento Unificado (Monorepo)
|
||||
|
||||
Após importação do GitHub, estrutura do workspace:
|
||||
|
||||
```
|
||||
/arcadia-workspace
|
||||
├── /suite # Node.js (atual)
|
||||
│ ├── /client
|
||||
│ ├── /server
|
||||
│ └── package.json
|
||||
│
|
||||
├── /plus # Laravel (importado)
|
||||
│ ├── /app
|
||||
│ ├── /routes
|
||||
│ ├── artisan
|
||||
│ └── composer.json
|
||||
│
|
||||
├── /fisco # Python/FastAPI
|
||||
│ └── main.py
|
||||
│
|
||||
└── workflows.json # Orquestra todos os serviços
|
||||
```
|
||||
|
||||
**Workflows Múltiplos:**
|
||||
|
||||
```yaml
|
||||
Suite: cd suite && npm run dev # :5000
|
||||
Plus: cd plus && php artisan serve # :8080
|
||||
Fisco: cd fisco && uvicorn main:app # :8002
|
||||
```
|
||||
|
||||
### 11.8 Roadmap de Integração
|
||||
|
||||
| Fase | Ação | Status |
|
||||
|------|------|--------|
|
||||
| 1 | Expor API REST no Plus | 🔶 Pendente |
|
||||
| 2 | Criar conector na Central API | 🔶 Pendente |
|
||||
| 3 | Implementar proxy `/plus` | 🔶 Pendente |
|
||||
| 4 | SSO com JWT compartilhado | 🔶 Pendente |
|
||||
| 5 | Importar código para Replit | 🔶 Pendente |
|
||||
| 6 | Migração MySQL → PostgreSQL | 🔶 Futuro |
|
||||
|
||||
### 11.9 Benefícios da Abordagem
|
||||
|
||||
| Aspecto | Benefício |
|
||||
|---------|-----------|
|
||||
| **Risco** | Zero - Plus continua funcionando |
|
||||
| **Tempo** | Rápido - Só precisa de API |
|
||||
| **Custo** | Baixo - Não reescreve nada |
|
||||
| **Evolução** | Gradual - Migra quando quiser |
|
||||
| **Manus** | Vê tudo via Knowledge Graph |
|
||||
| **IDE** | Desenvolve tudo no mesmo lugar |
|
||||
|
||||
---
|
||||
|
||||
## 12. REGRAS DE PROTEÇÃO (Não Quebrar)
|
||||
|
||||
| # | Regra |
|
||||
|---|-------|
|
||||
| 1 | Retail nunca vira ERP completo |
|
||||
| 2 | Governança não executa |
|
||||
| 3 | ERP não decide |
|
||||
| 4 | Arcádia não lança (contabilmente) |
|
||||
| 5 | Fonte da verdade é única (ERP) |
|
||||
| 6 | Segmentação fora da Suite |
|
||||
| 7 | Apps conversam por eventos |
|
||||
| 8 | Manus nunca deleta dados |
|
||||
|
||||
---
|
||||
|
||||
## 13. DECISÕES PENDENTES
|
||||
|
||||
### 13.1 Arquitetura
|
||||
|
||||
| Decisão | Opções | Status |
|
||||
|---------|--------|--------|
|
||||
| Estoque: Arcádia ou ERPNext? | Híbrido sugerido | 🔶 Pendente |
|
||||
| Fonte da verdade em conflito | ERPNext sempre | 🔶 Pendente |
|
||||
| Offline para PDV | PWA com sync | 🔶 Pendente |
|
||||
|
||||
### 13.2 Produto
|
||||
|
||||
| Decisão | Opções | Status |
|
||||
|---------|--------|--------|
|
||||
| App mobile nativo | Sim/PWA | 🔶 Pendente |
|
||||
| White label | Sim/Não | 🔶 Pendente |
|
||||
| Modelo de pricing | Híbrido sugerido | 🔶 Pendente |
|
||||
|
||||
### 13.3 Compliance
|
||||
|
||||
| Decisão | Opções | Status |
|
||||
|---------|--------|--------|
|
||||
| LGPD | Implementar | 🔶 Pendente |
|
||||
| Backup/DR | Definir SLA | 🔶 Pendente |
|
||||
|
||||
---
|
||||
|
||||
## 14. ROADMAP SUGERIDO
|
||||
|
||||
### Fase 1: Fundação ✅
|
||||
|
||||
- [x] Identity & Permissions
|
||||
- [x] Event Bus básico
|
||||
- [x] Retail Core
|
||||
- [x] Integração ERPNext
|
||||
- [x] MCP/A2A
|
||||
|
||||
### Fase 2: Estruturantes (Em Andamento)
|
||||
|
||||
- [ ] Financeiro completo
|
||||
- [ ] Contábil completo
|
||||
- [ ] Fiscal híbrido
|
||||
- [ ] RH completo
|
||||
- [ ] Integração escritório contábil
|
||||
|
||||
### Fase 3: Inteligência
|
||||
|
||||
- [ ] Knowledge Graph avançado
|
||||
- [ ] Manus com autonomia configurável
|
||||
- [ ] Ciência de Dados
|
||||
- [ ] Automações visuais
|
||||
|
||||
### Fase 4: Escala
|
||||
|
||||
- [ ] Marketplace de Apps
|
||||
- [ ] Apps de segmento
|
||||
- [ ] Parceiros desenvolvedores
|
||||
- [ ] White label
|
||||
|
||||
---
|
||||
|
||||
## 15. DIFERENCIAL COMPETITIVO
|
||||
|
||||
| ERP Tradicional | Arcádia Suite |
|
||||
|-----------------|---------------|
|
||||
| IA é add-on | IA é o núcleo |
|
||||
| Módulos isolados | Tudo conectado (Knowledge Graph) |
|
||||
| Executa operações | Governa decisões |
|
||||
| Usuário navega módulos | Usuário trabalha em UX única |
|
||||
| Automações manuais | Manus cria automações |
|
||||
| Relatórios estáticos | Insights proativos |
|
||||
|
||||
---
|
||||
|
||||
## 16. GLOSSÁRIO
|
||||
|
||||
| Termo | Definição |
|
||||
|-------|-----------|
|
||||
| **Arcádia Suite** | Plataforma completa (sistema operacional empresarial) |
|
||||
| **Arcádia Plus** | ERP canônico em Laravel, funciona como "lote" dentro da Suite |
|
||||
| **Arcádia Retail** | Motor de varejo nativo (Node.js) |
|
||||
| **Arcádia Fisco** | Motor fiscal híbrido (Python/FastAPI) |
|
||||
| **Manus** | Agente autônomo central (consciência) |
|
||||
| **Knowledge Graph** | Grafo de conhecimento que conecta todas as entidades |
|
||||
| **Estruturante** | Módulo com autonomia para ajustar, não criar |
|
||||
| **Motor** | Sistema que executa operações (Retail, ERP, Plus) |
|
||||
| **App** | Funcionalidade modular instalável |
|
||||
| **Event Bus** | Barramento de comunicação entre apps |
|
||||
| **MCP** | Model Context Protocol (protocolo agêntico) |
|
||||
| **A2A** | Agent to Agent Protocol |
|
||||
| **Proxy Reverso** | Técnica para rotear `/plus` ao Laravel transparentemente |
|
||||
| **Monorepo** | Estrutura onde Suite, Plus e Fisco ficam no mesmo workspace |
|
||||
|
||||
---
|
||||
|
||||
## 17. ANEXOS
|
||||
|
||||
### 17.1 Diagrama de Eventos Principais
|
||||
|
||||
```
|
||||
sale.completed
|
||||
→ Financeiro: gera recebível
|
||||
→ Fiscal: emite NF-e
|
||||
→ Estoque: baixa
|
||||
→ CRM: atualiza cliente
|
||||
→ Manus: analisa
|
||||
|
||||
service.completed
|
||||
→ Financeiro: gera recebível
|
||||
→ Comunicação: notifica cliente
|
||||
→ Manus: aprende padrão
|
||||
|
||||
tradein.approved
|
||||
→ Estoque: cria dispositivo
|
||||
→ Financeiro: gera crédito cliente
|
||||
→ Serviços: cria O.S. preparação
|
||||
|
||||
payment.received
|
||||
→ Financeiro: baixa recebível
|
||||
→ Contábil: lançamento
|
||||
→ Manus: atualiza score cliente
|
||||
```
|
||||
|
||||
### 17.2 Matriz de Responsabilidades
|
||||
|
||||
```
|
||||
│ Decide │ Ajusta │ Executa │ Registra │
|
||||
────────────────────┼────────┼────────┼─────────┼──────────┤
|
||||
Suite (Cockpit) │ ✅ │ ❌ │ ❌ │ ❌ │
|
||||
Estruturantes │ ✅ │ ✅ │ ❌ │ ❌ │
|
||||
Retail │ ❌ │ ❌ │ ✅ │ ❌ │
|
||||
Fiscal (Emissão) │ ❌ │ ❌ │ ✅ │ ❌ │
|
||||
ERP │ ❌ │ ❌ │ ✅ │ ✅ │
|
||||
Manus │ ✅ │ ❌ │ 🔶* │ ❌ │
|
||||
|
||||
* Manus executa apenas ações configuradas como automáticas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Documento mantido por:** Equipe Arcádia
|
||||
**Última atualização:** Janeiro 2026
|
||||
**Próxima revisão:** Após decisões pendentes
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
# Guia de Deploy - Arcádia Suite em VPS
|
||||
|
||||
Este guia explica como fazer o deploy do Arcádia Suite em um servidor VPS (Ubuntu 22.04).
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Mínimos do Servidor
|
||||
|
||||
| Recurso | Mínimo | Recomendado |
|
||||
|---------|--------|-------------|
|
||||
| RAM | 4 GB | 8 GB |
|
||||
| CPU | 2 cores | 4 cores |
|
||||
| Armazenamento | 40 GB SSD | 80 GB SSD |
|
||||
| Sistema | Ubuntu 22.04 LTS | Ubuntu 22.04 LTS |
|
||||
|
||||
---
|
||||
|
||||
## 1. Preparação do Servidor
|
||||
|
||||
### 1.1 Atualizar o sistema
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
|
||||
### 1.2 Instalar dependências básicas
|
||||
```bash
|
||||
sudo apt install -y curl wget git build-essential software-properties-common
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalar Node.js 20
|
||||
|
||||
```bash
|
||||
# Adicionar repositório NodeSource
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
|
||||
# Instalar Node.js
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# Verificar instalação
|
||||
node --version # Deve mostrar v20.x.x
|
||||
npm --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Instalar Python 3.11
|
||||
|
||||
```bash
|
||||
# Adicionar repositório
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa -y
|
||||
sudo apt update
|
||||
|
||||
# Instalar Python 3.11
|
||||
sudo apt install -y python3.11 python3.11-venv python3.11-dev python3-pip
|
||||
|
||||
# Definir como padrão (opcional)
|
||||
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
|
||||
|
||||
# Verificar instalação
|
||||
python3.11 --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Instalar e Configurar PostgreSQL
|
||||
|
||||
### 4.1 Instalar PostgreSQL
|
||||
```bash
|
||||
sudo apt install -y postgresql postgresql-contrib
|
||||
```
|
||||
|
||||
### 4.2 Criar banco de dados e usuário
|
||||
```bash
|
||||
# Acessar PostgreSQL
|
||||
sudo -u postgres psql
|
||||
|
||||
# Executar os comandos SQL:
|
||||
CREATE USER arcadia WITH PASSWORD 'sua_senha_segura';
|
||||
CREATE DATABASE arcadia_suite OWNER arcadia;
|
||||
GRANT ALL PRIVILEGES ON DATABASE arcadia_suite TO arcadia;
|
||||
\q
|
||||
```
|
||||
|
||||
### 4.3 Configurar acesso (se necessário)
|
||||
```bash
|
||||
# Editar pg_hba.conf para permitir conexões locais
|
||||
sudo nano /etc/postgresql/14/main/pg_hba.conf
|
||||
|
||||
# Adicionar linha:
|
||||
# local arcadia_suite arcadia md5
|
||||
|
||||
# Reiniciar PostgreSQL
|
||||
sudo systemctl restart postgresql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Instalar Nginx
|
||||
|
||||
```bash
|
||||
sudo apt install -y nginx
|
||||
|
||||
# Habilitar na inicialização
|
||||
sudo systemctl enable nginx
|
||||
sudo systemctl start nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Instalar PM2 (Gerenciador de Processos)
|
||||
|
||||
```bash
|
||||
sudo npm install -g pm2
|
||||
|
||||
# Configurar para iniciar automaticamente
|
||||
pm2 startup systemd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Clonar e Configurar o Projeto
|
||||
|
||||
### 7.1 Criar diretório e clonar
|
||||
```bash
|
||||
# Criar diretório da aplicação
|
||||
sudo mkdir -p /var/www/arcadia
|
||||
sudo chown $USER:$USER /var/www/arcadia
|
||||
|
||||
# Clonar o repositório (substitua pela URL do seu repo)
|
||||
cd /var/www/arcadia
|
||||
git clone https://github.com/seu-usuario/arcadia-suite.git .
|
||||
```
|
||||
|
||||
### 7.2 Instalar dependências Node.js
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 7.3 Configurar ambiente Python
|
||||
```bash
|
||||
# Criar ambiente virtual
|
||||
python3.11 -m venv venv
|
||||
|
||||
# Ativar ambiente
|
||||
source venv/bin/activate
|
||||
|
||||
# Instalar dependências Python
|
||||
pip install fastapi uvicorn nfelib lxml signxml python-dotenv psycopg2-binary
|
||||
|
||||
# Desativar ambiente
|
||||
deactivate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Configurar Variáveis de Ambiente
|
||||
|
||||
### 8.1 Criar arquivo .env
|
||||
```bash
|
||||
nano /var/www/arcadia/.env
|
||||
```
|
||||
|
||||
### 8.2 Adicionar variáveis
|
||||
```env
|
||||
# Banco de Dados
|
||||
DATABASE_URL=postgresql://arcadia:sua_senha_segura@localhost:5432/arcadia_suite
|
||||
PGHOST=localhost
|
||||
PGPORT=5432
|
||||
PGUSER=arcadia
|
||||
PGPASSWORD=sua_senha_segura
|
||||
PGDATABASE=arcadia_suite
|
||||
|
||||
# Ambiente
|
||||
NODE_ENV=production
|
||||
|
||||
# ERPNext (se usar)
|
||||
ERPNEXT_URL=https://seu-erpnext.com
|
||||
ERPNEXT_API_KEY=sua_api_key
|
||||
ERPNEXT_API_SECRET=seu_api_secret
|
||||
|
||||
# OpenAI (se usar IA)
|
||||
OPENAI_API_KEY=sua_chave_openai
|
||||
|
||||
# Sessão
|
||||
SESSION_SECRET=gere_uma_chave_aleatoria_longa_aqui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Build da Aplicação
|
||||
|
||||
```bash
|
||||
cd /var/www/arcadia
|
||||
|
||||
# Build do frontend
|
||||
npm run build
|
||||
|
||||
# Executar migrações do banco
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Configurar PM2
|
||||
|
||||
### 10.1 Criar arquivo de configuração
|
||||
```bash
|
||||
nano /var/www/arcadia/ecosystem.config.js
|
||||
```
|
||||
|
||||
### 10.2 Conteúdo do arquivo
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'arcadia-main',
|
||||
script: 'npm',
|
||||
args: 'run start',
|
||||
cwd: '/var/www/arcadia',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 5000
|
||||
},
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G'
|
||||
},
|
||||
{
|
||||
name: 'arcadia-fisco',
|
||||
script: '/var/www/arcadia/venv/bin/python',
|
||||
args: '-m uvicorn server.fisco.main:app --host 0.0.0.0 --port 8002',
|
||||
cwd: '/var/www/arcadia',
|
||||
autorestart: true,
|
||||
watch: false
|
||||
},
|
||||
{
|
||||
name: 'arcadia-contabil',
|
||||
script: '/var/www/arcadia/venv/bin/python',
|
||||
args: '-m uvicorn server.contabil.main:app --host 0.0.0.0 --port 8003',
|
||||
cwd: '/var/www/arcadia',
|
||||
autorestart: true,
|
||||
watch: false
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 10.3 Iniciar aplicações
|
||||
```bash
|
||||
cd /var/www/arcadia
|
||||
pm2 start ecosystem.config.js
|
||||
pm2 save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Configurar Nginx
|
||||
|
||||
### 11.1 Criar configuração do site
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/arcadia
|
||||
```
|
||||
|
||||
### 11.2 Conteúdo da configuração
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name seu-dominio.com.br www.seu-dominio.com.br;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# WebSocket para Socket.IO
|
||||
location /socket.io/ {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# APIs FastAPI
|
||||
location /api/fisco/ {
|
||||
proxy_pass http://localhost:8002/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /api/contabil/ {
|
||||
proxy_pass http://localhost:8003/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 Ativar site
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/arcadia /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Instalar Certificado SSL (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# Instalar Certbot
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
|
||||
# Obter certificado
|
||||
sudo certbot --nginx -d seu-dominio.com.br -d www.seu-dominio.com.br
|
||||
|
||||
# Renovação automática (já configurado por padrão)
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Firewall (UFW)
|
||||
|
||||
```bash
|
||||
# Habilitar UFW
|
||||
sudo ufw enable
|
||||
|
||||
# Permitir SSH
|
||||
sudo ufw allow OpenSSH
|
||||
|
||||
# Permitir HTTP e HTTPS
|
||||
sudo ufw allow 'Nginx Full'
|
||||
|
||||
# Verificar status
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Comandos Úteis
|
||||
|
||||
### Gerenciamento PM2
|
||||
```bash
|
||||
# Ver status das aplicações
|
||||
pm2 status
|
||||
|
||||
# Ver logs
|
||||
pm2 logs arcadia-main
|
||||
pm2 logs --lines 100
|
||||
|
||||
# Reiniciar aplicação
|
||||
pm2 restart arcadia-main
|
||||
|
||||
# Reiniciar todas
|
||||
pm2 restart all
|
||||
|
||||
# Parar aplicação
|
||||
pm2 stop arcadia-main
|
||||
```
|
||||
|
||||
### Atualizar Aplicação
|
||||
```bash
|
||||
cd /var/www/arcadia
|
||||
|
||||
# Puxar atualizações
|
||||
git pull
|
||||
|
||||
# Instalar novas dependências
|
||||
npm install
|
||||
|
||||
# Rebuild
|
||||
npm run build
|
||||
|
||||
# Reiniciar
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
### Backup do Banco de Dados
|
||||
```bash
|
||||
# Backup
|
||||
pg_dump -U arcadia arcadia_suite > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Restaurar
|
||||
psql -U arcadia arcadia_suite < backup_arquivo.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Checklist Final
|
||||
|
||||
- [ ] PostgreSQL rodando e acessível
|
||||
- [ ] Variáveis de ambiente configuradas (.env)
|
||||
- [ ] Build do frontend concluído
|
||||
- [ ] Migrações do banco executadas
|
||||
- [ ] PM2 iniciado com todas as aplicações
|
||||
- [ ] Nginx configurado e rodando
|
||||
- [ ] Certificado SSL instalado
|
||||
- [ ] Firewall configurado
|
||||
- [ ] Domínio apontando para o servidor
|
||||
|
||||
---
|
||||
|
||||
## Suporte
|
||||
|
||||
Em caso de problemas:
|
||||
|
||||
1. Verifique os logs: `pm2 logs`
|
||||
2. Verifique o Nginx: `sudo tail -f /var/log/nginx/error.log`
|
||||
3. Verifique o PostgreSQL: `sudo journalctl -u postgresql`
|
||||
|
||||
---
|
||||
|
||||
*Guia criado para Arcádia Suite - Business Operating System*
|
||||
|
|
@ -0,0 +1,567 @@
|
|||
# Manual de Instalação - Arcádia Suite
|
||||
|
||||
## Requisitos do Servidor
|
||||
|
||||
### Hardware Mínimo
|
||||
- CPU: 2 cores
|
||||
- RAM: 4GB
|
||||
- Disco: 40GB SSD
|
||||
- Rede: 100 Mbps
|
||||
|
||||
### Hardware Recomendado
|
||||
- CPU: 4+ cores
|
||||
- RAM: 8GB+
|
||||
- Disco: 100GB SSD
|
||||
- Rede: 1 Gbps
|
||||
|
||||
### Sistema Operacional
|
||||
- Ubuntu 22.04 LTS (recomendado)
|
||||
- Debian 12
|
||||
- Rocky Linux 9
|
||||
|
||||
---
|
||||
|
||||
## 1. Preparação do Servidor
|
||||
|
||||
### 1.1 Atualizar Sistema
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
|
||||
### 1.2 Instalar Dependências Básicas
|
||||
```bash
|
||||
sudo apt install -y curl wget git unzip build-essential
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalar Node.js 20
|
||||
|
||||
```bash
|
||||
# Adicionar repositório NodeSource
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
|
||||
# Instalar Node.js
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# Verificar instalação
|
||||
node --version # v20.x.x
|
||||
npm --version # 10.x.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Instalar Python 3.11
|
||||
|
||||
```bash
|
||||
# Instalar Python e pip
|
||||
sudo apt install -y python3.11 python3.11-venv python3-pip
|
||||
|
||||
# Criar link simbólico (se necessário)
|
||||
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
|
||||
|
||||
# Verificar instalação
|
||||
python3 --version # Python 3.11.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Instalar PostgreSQL 15
|
||||
|
||||
```bash
|
||||
# Adicionar repositório oficial
|
||||
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
|
||||
# Instalar PostgreSQL
|
||||
sudo apt install -y postgresql-15
|
||||
|
||||
# Iniciar e habilitar serviço
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
|
||||
# Criar usuário e banco de dados
|
||||
sudo -u postgres psql << EOF
|
||||
CREATE USER arcadia WITH PASSWORD 'SuaSenhaSegura123!';
|
||||
CREATE DATABASE arcadia_suite OWNER arcadia;
|
||||
GRANT ALL PRIVILEGES ON DATABASE arcadia_suite TO arcadia;
|
||||
\q
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Instalar Redis (Opcional - Cache/Sessions)
|
||||
|
||||
```bash
|
||||
sudo apt install -y redis-server
|
||||
sudo systemctl start redis-server
|
||||
sudo systemctl enable redis-server
|
||||
|
||||
# Verificar
|
||||
redis-cli ping # PONG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Clonar Projeto
|
||||
|
||||
```bash
|
||||
# Criar diretório de aplicação
|
||||
sudo mkdir -p /opt/arcadia
|
||||
sudo chown $USER:$USER /opt/arcadia
|
||||
cd /opt/arcadia
|
||||
|
||||
# Clonar repositório
|
||||
git clone https://github.com/seu-usuario/arcadia-suite.git .
|
||||
|
||||
# Ou extrair arquivo ZIP
|
||||
# unzip arcadia-suite.zip -d /opt/arcadia
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Configurar Variáveis de Ambiente
|
||||
|
||||
### 7.1 Criar arquivo .env
|
||||
```bash
|
||||
cd /opt/arcadia
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 7.2 Conteúdo do .env
|
||||
```env
|
||||
# Banco de Dados
|
||||
DATABASE_URL=postgresql://arcadia:SuaSenhaSegura123!@localhost:5432/arcadia_suite
|
||||
PGHOST=localhost
|
||||
PGPORT=5432
|
||||
PGUSER=arcadia
|
||||
PGPASSWORD=SuaSenhaSegura123!
|
||||
PGDATABASE=arcadia_suite
|
||||
|
||||
# Aplicação
|
||||
NODE_ENV=production
|
||||
PORT=5000
|
||||
SESSION_SECRET=sua-chave-secreta-muito-longa-e-aleatoria-aqui
|
||||
|
||||
# OpenAI (para recursos de IA)
|
||||
OPENAI_API_KEY=sk-sua-chave-openai
|
||||
|
||||
# ERPNext (opcional)
|
||||
ERPNEXT_URL=https://seu-erpnext.com
|
||||
ERPNEXT_API_KEY=sua-api-key
|
||||
ERPNEXT_API_SECRET=sua-api-secret
|
||||
|
||||
# WhatsApp (opcional)
|
||||
# Configurado via interface
|
||||
|
||||
# Protocolos MCP/A2A
|
||||
PROTOCOL_API_KEYS=chave1,chave2,chave3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Instalar Dependências do Projeto
|
||||
|
||||
### 8.1 Node.js (Frontend + Backend)
|
||||
```bash
|
||||
cd /opt/arcadia
|
||||
npm install
|
||||
```
|
||||
|
||||
### 8.2 Python (Serviços de IA)
|
||||
```bash
|
||||
cd /opt/arcadia
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Build da Aplicação
|
||||
|
||||
```bash
|
||||
cd /opt/arcadia
|
||||
|
||||
# Build do frontend (Vite)
|
||||
npm run build
|
||||
|
||||
# Verificar build
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Executar Migrações do Banco
|
||||
|
||||
```bash
|
||||
cd /opt/arcadia
|
||||
|
||||
# Gerar migrações (se necessário)
|
||||
npm run db:generate
|
||||
|
||||
# Aplicar migrações
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Configurar Serviço Systemd
|
||||
|
||||
### 11.1 Criar serviço principal
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/arcadia-suite.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Arcadia Suite - Office Estratégico
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/opt/arcadia
|
||||
Environment=NODE_ENV=production
|
||||
EnvironmentFile=/opt/arcadia/.env
|
||||
ExecStart=/usr/bin/node dist/index.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 11.2 Criar serviço Python (IA/Fiscal)
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/arcadia-python.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Arcadia Suite - Python Services
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/opt/arcadia/python
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
EnvironmentFile=/opt/arcadia/.env
|
||||
ExecStart=/opt/arcadia/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8001
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 11.3 Ativar serviços
|
||||
```bash
|
||||
# Ajustar permissões
|
||||
sudo chown -R www-data:www-data /opt/arcadia
|
||||
|
||||
# Recarregar systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Habilitar e iniciar serviços
|
||||
sudo systemctl enable arcadia-suite
|
||||
sudo systemctl enable arcadia-python
|
||||
sudo systemctl start arcadia-suite
|
||||
sudo systemctl start arcadia-python
|
||||
|
||||
# Verificar status
|
||||
sudo systemctl status arcadia-suite
|
||||
sudo systemctl status arcadia-python
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Configurar Nginx (Proxy Reverso)
|
||||
|
||||
### 12.1 Instalar Nginx
|
||||
```bash
|
||||
sudo apt install -y nginx
|
||||
```
|
||||
|
||||
### 12.2 Configurar Virtual Host
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/arcadia
|
||||
```
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name seu-dominio.com.br;
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name seu-dominio.com.br;
|
||||
|
||||
# SSL (usar certbot para gerar)
|
||||
ssl_certificate /etc/letsencrypt/live/seu-dominio.com.br/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/seu-dominio.com.br/privkey.pem;
|
||||
|
||||
# SSL Settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# Logs
|
||||
access_log /var/log/nginx/arcadia.access.log;
|
||||
error_log /var/log/nginx/arcadia.error.log;
|
||||
|
||||
# Tamanho máximo de upload (para migrações)
|
||||
client_max_body_size 500M;
|
||||
|
||||
# Proxy para aplicação Node.js
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# WebSocket para Socket.IO
|
||||
location /socket.io/ {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# API Python (IA/Fiscal)
|
||||
location /api/python/ {
|
||||
proxy_pass http://127.0.0.1:8001/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 12.3 Ativar configuração
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/arcadia /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Certificado SSL (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# Instalar Certbot
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
|
||||
# Gerar certificado
|
||||
sudo certbot --nginx -d seu-dominio.com.br
|
||||
|
||||
# Renovação automática (já configurada)
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Configurar Firewall
|
||||
|
||||
```bash
|
||||
# Instalar UFW
|
||||
sudo apt install -y ufw
|
||||
|
||||
# Configurar regras
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
sudo ufw allow ssh
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
|
||||
# Ativar firewall
|
||||
sudo ufw enable
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Backup Automático
|
||||
|
||||
### 15.1 Script de backup
|
||||
```bash
|
||||
sudo nano /opt/arcadia/scripts/backup.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/var/backups/arcadia"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup do banco
|
||||
pg_dump -h localhost -U arcadia arcadia_suite | gzip > $BACKUP_DIR/db_$DATE.sql.gz
|
||||
|
||||
# Backup dos arquivos de upload
|
||||
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz /opt/arcadia/uploads
|
||||
|
||||
# Manter últimos 7 dias
|
||||
find $BACKUP_DIR -type f -mtime +7 -delete
|
||||
|
||||
echo "Backup concluído: $DATE"
|
||||
```
|
||||
|
||||
### 15.2 Agendar no cron
|
||||
```bash
|
||||
chmod +x /opt/arcadia/scripts/backup.sh
|
||||
sudo crontab -e
|
||||
|
||||
# Adicionar linha (backup às 3h da manhã)
|
||||
0 3 * * * /opt/arcadia/scripts/backup.sh >> /var/log/arcadia-backup.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Monitoramento
|
||||
|
||||
### 16.1 Logs em tempo real
|
||||
```bash
|
||||
# Aplicação Node.js
|
||||
sudo journalctl -u arcadia-suite -f
|
||||
|
||||
# Serviço Python
|
||||
sudo journalctl -u arcadia-python -f
|
||||
|
||||
# Nginx
|
||||
sudo tail -f /var/log/nginx/arcadia.access.log
|
||||
sudo tail -f /var/log/nginx/arcadia.error.log
|
||||
```
|
||||
|
||||
### 16.2 Verificar saúde
|
||||
```bash
|
||||
# Status dos serviços
|
||||
sudo systemctl status arcadia-suite
|
||||
sudo systemctl status arcadia-python
|
||||
sudo systemctl status postgresql
|
||||
sudo systemctl status nginx
|
||||
|
||||
# Uso de recursos
|
||||
htop
|
||||
df -h
|
||||
free -m
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. Primeiro Acesso
|
||||
|
||||
1. Acesse: `https://seu-dominio.com.br`
|
||||
2. Faça login com credenciais iniciais:
|
||||
- Email: `admin@arcadia.com.br`
|
||||
- Senha: `admin123` (altere imediatamente!)
|
||||
3. Configure seu tenant/empresa
|
||||
4. Configure integrações (ERPNext, WhatsApp, OpenAI)
|
||||
|
||||
---
|
||||
|
||||
## 18. Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Reiniciar aplicação
|
||||
sudo systemctl restart arcadia-suite
|
||||
|
||||
# Ver logs
|
||||
sudo journalctl -u arcadia-suite -n 100
|
||||
|
||||
# Atualizar código
|
||||
cd /opt/arcadia
|
||||
git pull origin main
|
||||
npm install
|
||||
npm run build
|
||||
sudo systemctl restart arcadia-suite
|
||||
|
||||
# Executar migrações
|
||||
npm run db:push
|
||||
|
||||
# Limpar cache
|
||||
redis-cli FLUSHALL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Solução de Problemas
|
||||
|
||||
### Aplicação não inicia
|
||||
```bash
|
||||
# Verificar logs
|
||||
sudo journalctl -u arcadia-suite -n 50
|
||||
|
||||
# Verificar permissões
|
||||
ls -la /opt/arcadia
|
||||
|
||||
# Testar manualmente
|
||||
cd /opt/arcadia
|
||||
node dist/index.js
|
||||
```
|
||||
|
||||
### Erro de conexão com banco
|
||||
```bash
|
||||
# Verificar PostgreSQL
|
||||
sudo systemctl status postgresql
|
||||
psql -h localhost -U arcadia -d arcadia_suite
|
||||
|
||||
# Verificar .env
|
||||
cat /opt/arcadia/.env | grep DATABASE
|
||||
```
|
||||
|
||||
### WebSocket não conecta
|
||||
```bash
|
||||
# Verificar configuração Nginx
|
||||
sudo nginx -t
|
||||
|
||||
# Verificar porta
|
||||
netstat -tlpn | grep 5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Estrutura de Módulos Instalados
|
||||
|
||||
| Categoria | Módulos |
|
||||
|-----------|---------|
|
||||
| Inteligência | Agent, Scientist, Knowledge, Automações, MCP, A2A |
|
||||
| Comunicação | XOS Inbox, Chat, Comunidades, XOS Central, NPS |
|
||||
| Negócios | CRM, Compass, Produção, Suporte, Valuation, Fisco, Contábil, People, ERP, Financeiro, Retail, Plus, Marketplace |
|
||||
| Analytics | Insights, Central APIs |
|
||||
| Desenvolvimento | IDE, Canvas, API Hub, API Tester, DocType/Page Builder, Engineering |
|
||||
| Educação | LMS |
|
||||
| Operações | Field Ops, Quality ISO, Assistência Técnica, Suppliers |
|
||||
| Administração | Admin, Home, Migration XOS, Super Admin |
|
||||
|
||||
**Total: 40+ módulos**
|
||||
|
||||
---
|
||||
|
||||
## Suporte
|
||||
|
||||
- Documentação: `/docs/`
|
||||
- Email: suporte@arcadia.com.br
|
||||
- API Docs: `https://seu-dominio.com.br/api-hub`
|
||||
|
||||
---
|
||||
|
||||
*Arcádia Suite - O Office Estratégico da Empresa Moderna*
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
# Plano de Implementação - Módulo Retail (Loja de Celular e Assistência Técnica)
|
||||
|
||||
**Versão:** 1.1
|
||||
**Data:** Janeiro 2026
|
||||
**Status:** Em Desenvolvimento
|
||||
|
||||
---
|
||||
|
||||
## 0. Requisitos Especiais Identificados
|
||||
|
||||
### 0.1 Fluxo Trade-In → O.S. Interna → Estoque
|
||||
|
||||
Quando um aparelho Trade-In é aprovado para aquisição, deve:
|
||||
1. Criar automaticamente uma **Ordem de Serviço Interna** para revisão/manutenção
|
||||
2. Ao finalizar a O.S., o aparelho entra no **estoque como produto normal**
|
||||
3. O estoque deve **diferenciar a origem**: Trade-In vs Fornecedor
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌───────────────────┐ ┌─────────────────┐
|
||||
│ TRADE-IN │ │ O.S. INTERNA │ │ ESTOQUE │
|
||||
│ (Avaliação) │ ──→ │ (Revisão) │ ──→ │ (Disponível) │
|
||||
└─────────────────┘ └───────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
• Checklist OK • Limpeza • Origem: Trade-In
|
||||
• Valor acordado • Teste funcional • Custo: valor + OS
|
||||
• Assinatura • Troca peças? • Preço venda
|
||||
• Documento legal • Laudo técnico • IMEI rastreado
|
||||
```
|
||||
|
||||
### 0.2 Cadastro Unificado de Pessoas
|
||||
|
||||
Uma única tabela de **Pessoas** com múltiplos papéis:
|
||||
- Cliente
|
||||
- Fornecedor
|
||||
- Colaborador/Funcionário
|
||||
- Técnico
|
||||
- Representante
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PESSOA (person) │
|
||||
│ • id, nome, cpf_cnpj, email, telefone, endereço │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ CLIENTE │ │FORNECEDOR│ │COLABORADOR│
|
||||
│ (role) │ │ (role) │ │ (role) │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Análise do Estado Atual
|
||||
|
||||
### 1.1 O Que Já Temos Implementado
|
||||
|
||||
| Módulo | Funcionalidade | Status | Observações |
|
||||
|--------|----------------|--------|-------------|
|
||||
| **Estrutura Base** | Schema de tabelas (mobile_devices, device_evaluations, service_orders, pos_sales, stock_transfers) | ✅ Completo | Alinhado com requisitos |
|
||||
| **Lojas/Filiais** | CRUD de lojas (retail_stores) e armazéns (retail_warehouses) | ✅ Completo | Suporte multi-loja |
|
||||
| **Dispositivos/IMEI** | Cadastro, busca por IMEI, listagem | ✅ Básico | Falta histórico (Kardex) |
|
||||
| **Trade-In** | Avaliação com checklist customizável, termo de transferência, assinatura digital | ✅ Completo | Documento imprimível |
|
||||
| **Ordens de Serviço** | CRUD básico, itens da OS | ✅ Parcial | Falta fluxo completo de status |
|
||||
| **PDV** | Sessões, vendas básicas, itens | ✅ Parcial | Falta carrinho funcional no frontend |
|
||||
| **Transferências** | Estrutura para transferência entre lojas | ✅ Parcial | Falta workflow completo |
|
||||
| **Dashboard** | Estatísticas básicas | ✅ Básico | Pode ser expandido |
|
||||
|
||||
### 1.2 O Que Está Faltando (Gap Analysis)
|
||||
|
||||
| Requisito (Documento) | Status | Prioridade | Esforço |
|
||||
|-----------------------|--------|------------|---------|
|
||||
| Histórico completo de IMEI (Kardex) | ❌ Não implementado | Alta | Médio |
|
||||
| Geração de etiquetas com código de barras | ❌ Não implementado | Alta | Baixo |
|
||||
| Fluxo completo de status da O.S. | ⚠️ Parcial | Alta | Médio |
|
||||
| Orçamentação automática | ❌ Não implementado | Alta | Médio |
|
||||
| Notificação ao cliente (WhatsApp/SMS) | ⚠️ Integração existe | Média | Baixo |
|
||||
| Portal público de consulta O.S. | ❌ Não implementado | Média | Baixo |
|
||||
| PDV com carrinho funcional | ⚠️ Parcial | Alta | Alto |
|
||||
| Validação IMEI na venda | ❌ Não implementado | Alta | Baixo |
|
||||
| Baixa automática de peças | ❌ Não implementado | Alta | Médio |
|
||||
| Separação estoque venda/reparo | ⚠️ Via warehouses | Média | Baixo |
|
||||
| Impressão de recibos/comprovantes | ❌ Não implementado | Média | Médio |
|
||||
| Relatórios específicos | ⚠️ Parcial (BI existe) | Média | Médio |
|
||||
|
||||
---
|
||||
|
||||
## 2. Arquitetura de Integração
|
||||
|
||||
### 2.1 Reutilização de Módulos Existentes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA SUITE (Base) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ✅ Clientes (customers) → Usar cadastro existente │
|
||||
│ ✅ Produtos (products) → Usar cadastro existente │
|
||||
│ ✅ Fornecedores (suppliers) → Usar cadastro existente │
|
||||
│ ✅ Usuários/Funcionários (users) → Usar sistema de auth │
|
||||
│ ✅ WhatsApp (Baileys) → Integrar notificações │
|
||||
│ ✅ ERPNext → Sincronizar dados quando conectado │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MÓDULO RETAIL (Novo) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 📱 Dispositivos (mobile_devices) → Controle por IMEI │
|
||||
│ 🔧 Ordens de Serviço (service_orders) → Assistência Técnica │
|
||||
│ 💰 PDV (pos_sales) → Ponto de Venda │
|
||||
│ 📦 Estoque Retail (via warehouses) → Peças e Aparelhos │
|
||||
│ 🔄 Trade-In (device_evaluations) → Avaliação de usados │
|
||||
│ 🏪 Multi-Loja (retail_stores) → Franquias │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Fluxo de Status da O.S. (Implementar)
|
||||
|
||||
```
|
||||
┌──────────┐ ┌────────────┐ ┌───────────┐ ┌────────────────────┐
|
||||
│ ABERTA │ → │ DIAGNÓSTICO│ → │ ORÇAMENTO │ → │ AGUARDANDO_APROVAÇÃO│
|
||||
└──────────┘ └────────────┘ └───────────┘ └────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────────────┴────┐
|
||||
▼ ▼
|
||||
┌────────────┐ ┌────────────┐
|
||||
│ RECUSADA │ │ APROVADA │
|
||||
└────────────┘ └────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────┐ ┌───────────┐ ┌──────────┐
|
||||
│ AGUARDANDO_PECA │ ← │ EM_REPARO │ → │ QUALIDADE │
|
||||
└─────────────────┘ └───────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ AGUARDANDO_RETIRADA │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ FINALIZADA │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Plano de Implementação por Fases
|
||||
|
||||
### Fase 0: Estrutura Base - Pessoas e Trade-In Flow (Semana 0-1) ⭐ PRIORIDADE
|
||||
|
||||
#### 0.1 Cadastro Unificado de Pessoas
|
||||
- [ ] Criar tabela `persons` com campos base (nome, cpf_cnpj, email, telefone, endereço)
|
||||
- [ ] Criar tabela `person_roles` (person_id, role_type, dados específicos do papel)
|
||||
- [ ] Tipos de papel: `customer`, `supplier`, `employee`, `technician`, `partner`
|
||||
- [ ] API CRUD `/persons` com filtro por papel
|
||||
- [ ] Migrar referências de customers/suppliers para usar persons
|
||||
- [ ] UI unificada de cadastro de pessoas com seleção de papéis
|
||||
|
||||
#### 0.2 Fluxo Trade-In → O.S. Interna → Estoque
|
||||
- [ ] Adicionar campo `origin_type` em mobile_devices (trade_in, supplier, internal)
|
||||
- [ ] Adicionar campo `is_internal` em service_orders para diferenciar OS cliente vs interna
|
||||
- [ ] Criar trigger automático: ao aprovar Trade-In → criar O.S. Interna
|
||||
- [ ] O.S. Interna com tipo: revisão, limpeza, manutenção preventiva
|
||||
- [ ] Ao finalizar O.S. Interna → criar dispositivo no estoque
|
||||
- [ ] Calcular custo total: valor_trade_in + custo_os
|
||||
- [ ] Sugerir preço de venda baseado em margem configurável
|
||||
- [ ] UI de acompanhamento do fluxo Trade-In completo
|
||||
|
||||
#### 0.3 Diferenciação de Estoque por Origem
|
||||
- [ ] Campo `acquisition_type` em mobile_devices: trade_in, purchase, consignment
|
||||
- [ ] Campo `acquisition_cost` (custo de aquisição)
|
||||
- [ ] Campo `related_evaluation_id` (link com avaliação Trade-In)
|
||||
- [ ] Campo `related_service_order_id` (link com O.S. de revisão)
|
||||
- [ ] Filtros no estoque por origem
|
||||
- [ ] Relatório de margem por origem
|
||||
|
||||
### Fase 1: Completar o Núcleo (Semana 2-3)
|
||||
|
||||
#### 1.1 Histórico de IMEI (Kardex)
|
||||
- [ ] Criar tabela `imei_history` no schema
|
||||
- [ ] API para registrar movimentações automáticas
|
||||
- [ ] Endpoint GET `/devices/:imei/history`
|
||||
- [ ] UI de visualização do histórico do dispositivo
|
||||
|
||||
#### 1.2 Fluxo Completo de O.S.
|
||||
- [ ] Atualizar enum de status conforme fluxograma
|
||||
- [ ] API PATCH `/service-orders/:id/status` com validação de transições
|
||||
- [ ] Registrar automaticamente em `imei_history`
|
||||
- [ ] UI com workflow visual de status
|
||||
- [ ] Diferenciar O.S. Cliente vs O.S. Interna na listagem
|
||||
|
||||
#### 1.3 Orçamentação
|
||||
- [ ] API POST `/service-orders/:id/generate-quote`
|
||||
- [ ] Cálculo automático baseado em itens adicionados
|
||||
- [ ] UI para adicionar peças/serviços e visualizar orçamento
|
||||
- [ ] Impressão de orçamento
|
||||
|
||||
#### 1.4 Geração de Etiquetas
|
||||
- [ ] API GET `/devices/:imei/label` retornando PDF
|
||||
- [ ] Incluir código de barras (IMEI + número OS)
|
||||
- [ ] Botão de impressão na tela de dispositivo/OS
|
||||
|
||||
### Fase 2: PDV Funcional (Semana 3-4)
|
||||
|
||||
#### 2.1 Carrinho de Vendas
|
||||
- [ ] Componente de carrinho no frontend
|
||||
- [ ] Busca de produtos por código/nome
|
||||
- [ ] Leitura de IMEI para aparelhos
|
||||
- [ ] Validação de IMEI disponível para venda
|
||||
|
||||
#### 2.2 Integração Venda + O.S.
|
||||
- [ ] Adicionar O.S. finalizada como item da venda
|
||||
- [ ] Baixa automática de peças no fechamento da OS
|
||||
|
||||
#### 2.3 Formas de Pagamento
|
||||
- [ ] UI para múltiplas formas de pagamento
|
||||
- [ ] Cálculo de troco
|
||||
- [ ] Integração Trade-In como forma de pagamento
|
||||
|
||||
#### 2.4 Recibos e Comprovantes
|
||||
- [ ] Geração de recibo de venda (impressão)
|
||||
- [ ] Comprovante de entrega de OS
|
||||
|
||||
### Fase 3: Automação e Comunicação (Semana 5-6)
|
||||
|
||||
#### 3.1 Notificações ao Cliente
|
||||
- [ ] Integrar com WhatsApp existente (Baileys)
|
||||
- [ ] Templates de mensagem: orçamento pronto, OS pronta, lembrete
|
||||
- [ ] API POST `/service-orders/:id/notify-customer`
|
||||
- [ ] Histórico de notificações enviadas
|
||||
|
||||
#### 3.2 Portal do Cliente (Público)
|
||||
- [ ] Rota pública GET `/public/os/:orderNumber`
|
||||
- [ ] Página simples de consulta de status
|
||||
- [ ] QR Code na etiqueta/recibo
|
||||
|
||||
#### 3.3 Relatórios Específicos
|
||||
- [ ] OS por período/técnico/status
|
||||
- [ ] Vendas por vendedor/produto/período
|
||||
- [ ] Tempo médio de reparo
|
||||
- [ ] Peças mais utilizadas
|
||||
|
||||
### Fase 4: Estoque Avançado (Semana 7-8)
|
||||
|
||||
#### 4.1 Separação Estoque Venda/Reparo
|
||||
- [ ] Categorização de warehouses (tipo: venda/reparo)
|
||||
- [ ] Regras de transferência entre tipos
|
||||
- [ ] Alertas de estoque mínimo por categoria
|
||||
|
||||
#### 4.2 Baixa Automática de Peças
|
||||
- [ ] Trigger ao finalizar reparo
|
||||
- [ ] Reserva de peças ao aprovar OS
|
||||
- [ ] Liberação se OS cancelada
|
||||
|
||||
#### 4.3 Inventário por IMEI
|
||||
- [ ] Listagem de aparelhos em estoque por IMEI
|
||||
- [ ] Localização física (prateleira/loja)
|
||||
- [ ] Tempo em estoque
|
||||
|
||||
---
|
||||
|
||||
## 4. APIs a Implementar
|
||||
|
||||
### 4.0 Módulo Pessoas (Prioridade Máxima) ⭐
|
||||
|
||||
| Método | Endpoint | Descrição | Status |
|
||||
|--------|----------|-----------|--------|
|
||||
| GET | `/persons` | Listar pessoas (filtro por papel) | ❌ Fazer |
|
||||
| POST | `/persons` | Criar pessoa | ❌ Fazer |
|
||||
| GET | `/persons/:id` | Detalhes da pessoa | ❌ Fazer |
|
||||
| PUT | `/persons/:id` | Atualizar pessoa | ❌ Fazer |
|
||||
| POST | `/persons/:id/roles` | Adicionar papel à pessoa | ❌ Fazer |
|
||||
| DELETE | `/persons/:id/roles/:role` | Remover papel | ❌ Fazer |
|
||||
| GET | `/persons/search?q=` | Buscar por nome/CPF | ❌ Fazer |
|
||||
|
||||
### 4.1 Módulo Trade-In Flow (Prioridade Máxima) ⭐
|
||||
|
||||
| Método | Endpoint | Descrição | Status |
|
||||
|--------|----------|-----------|--------|
|
||||
| POST | `/evaluations/:id/approve-and-process` | Aprovar e criar O.S. Interna | ❌ Fazer |
|
||||
| GET | `/evaluations/:id/full-flow` | Status completo do fluxo | ❌ Fazer |
|
||||
| POST | `/service-orders/:id/finalize-internal` | Finalizar OS e criar no estoque | ❌ Fazer |
|
||||
|
||||
### 4.2 Módulo IMEI (Prioridade Alta)
|
||||
|
||||
| Método | Endpoint | Descrição | Status |
|
||||
|--------|----------|-----------|--------|
|
||||
| GET | `/devices/:imei/history` | Histórico completo (Kardex) | ❌ Fazer |
|
||||
| GET | `/devices/:imei/label` | Gerar etiqueta PDF | ❌ Fazer |
|
||||
| PATCH | `/devices/:id/status` | Atualizar status c/ registro | ❌ Fazer |
|
||||
| GET | `/devices?origin=trade_in` | Filtrar por origem | ❌ Fazer |
|
||||
|
||||
### 4.3 Módulo O.S. (Prioridade Alta)
|
||||
|
||||
| Método | Endpoint | Descrição | Status |
|
||||
|--------|----------|-----------|--------|
|
||||
| POST | `/service-orders/:id/generate-quote` | Gerar orçamento | ❌ Fazer |
|
||||
| PATCH | `/service-orders/:id/status` | Workflow de status | ⚠️ Melhorar |
|
||||
| POST | `/service-orders/:id/notify-customer` | Enviar notificação | ❌ Fazer |
|
||||
| GET | `/public/os/:orderNumber` | Consulta pública | ❌ Fazer |
|
||||
| GET | `/service-orders?is_internal=true` | Filtrar OS internas | ❌ Fazer |
|
||||
|
||||
### 4.4 Módulo Vendas (Prioridade Alta)
|
||||
|
||||
| Método | Endpoint | Descrição | Status |
|
||||
|--------|----------|-----------|--------|
|
||||
| POST | `/sales/validate-imei` | Validar IMEI para venda | ❌ Fazer |
|
||||
| GET | `/sales/:id/receipt` | Gerar recibo PDF | ❌ Fazer |
|
||||
|
||||
### 4.5 Módulo Estoque (Prioridade Média)
|
||||
|
||||
| Método | Endpoint | Descrição | Status |
|
||||
|--------|----------|-----------|--------|
|
||||
| POST | `/inventory/:id/decrease` | Baixar do estoque | ❌ Fazer |
|
||||
| GET | `/inventory/low-stock` | Itens abaixo do mínimo | ❌ Fazer |
|
||||
| GET | `/inventory/by-origin` | Relatório por origem | ❌ Fazer |
|
||||
|
||||
---
|
||||
|
||||
## 5. Componentes de UI a Criar/Melhorar
|
||||
|
||||
### 5.0 Componentes Novos - Fase 0 ⭐
|
||||
|
||||
| Componente | Descrição | Prioridade |
|
||||
|------------|-----------|------------|
|
||||
| `PersonForm` | Cadastro unificado de pessoas com seleção de papéis | Máxima |
|
||||
| `PersonSearch` | Busca de pessoas por nome/CPF com indicador de papéis | Máxima |
|
||||
| `TradeInFlowTracker` | Acompanhamento visual: Avaliação → O.S. → Estoque | Máxima |
|
||||
| `InternalServiceOrder` | Formulário de O.S. interna (revisão/manutenção) | Máxima |
|
||||
| `DeviceOriginBadge` | Badge indicando origem (Trade-In / Fornecedor) | Alta |
|
||||
|
||||
### 5.1 Novos Componentes - Outras Fases
|
||||
|
||||
| Componente | Descrição | Prioridade |
|
||||
|------------|-----------|------------|
|
||||
| `IMEIHistory` | Timeline de movimentações do IMEI | Alta |
|
||||
| `ServiceOrderWorkflow` | Painel visual de status da OS | Alta |
|
||||
| `QuoteGenerator` | Formulário de orçamentação | Alta |
|
||||
| `POSCart` | Carrinho do PDV funcional | Alta |
|
||||
| `PaymentMethods` | Seletor de formas de pagamento | Alta |
|
||||
| `ReceiptPreview` | Visualização/impressão de recibos | Média |
|
||||
| `LabelPrint` | Impressão de etiquetas | Média |
|
||||
| `CustomerPortal` | Página pública de consulta | Média |
|
||||
| `NotificationHistory` | Histórico de mensagens enviadas | Baixa |
|
||||
|
||||
### 5.2 Melhorias em Componentes Existentes
|
||||
|
||||
| Componente | Melhoria | Prioridade |
|
||||
|------------|----------|------------|
|
||||
| `ArcadiaRetail.tsx` | Adicionar carrinho no PDV | Alta |
|
||||
| Aba Estoque | Filtrar por origem (Trade-In/Fornecedor) | Alta |
|
||||
| Aba Estoque | Separar por tipo (venda/reparo) | Média |
|
||||
| Aba Serviços | Workflow visual de status | Alta |
|
||||
| Aba Serviços | Diferenciar O.S. Cliente vs O.S. Interna | Alta |
|
||||
| Aba Trade-In | Botão "Aprovar e Processar" com fluxo completo | Alta |
|
||||
| Dashboard | KPIs específicos do negócio | Média |
|
||||
|
||||
---
|
||||
|
||||
## 6. Estimativa de Esforço
|
||||
|
||||
| Fase | Duração | Complexidade | Dependências |
|
||||
|------|---------|--------------|--------------|
|
||||
| **Fase 0 - Pessoas + Trade-In Flow** | 1-2 semanas | Alta | Nenhuma |
|
||||
| Fase 1 - Núcleo (IMEI, OS, Orçamento) | 2 semanas | Média | Fase 0 |
|
||||
| Fase 2 - PDV | 2 semanas | Alta | Fase 1 |
|
||||
| Fase 3 - Automação | 2 semanas | Média | Fase 1, WhatsApp |
|
||||
| Fase 4 - Estoque Avançado | 2 semanas | Média | Fases 1-2 |
|
||||
|
||||
**Total Estimado:** 8-10 semanas para MVP completo
|
||||
|
||||
---
|
||||
|
||||
## 7. Schema de Dados - Novas Tabelas
|
||||
|
||||
### 7.1 Tabela `persons` (Pessoas Unificadas)
|
||||
|
||||
```sql
|
||||
CREATE TABLE persons (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
cpf_cnpj VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(20),
|
||||
phone2 VARCHAR(20),
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(2),
|
||||
zip_code VARCHAR(10),
|
||||
birth_date DATE,
|
||||
notes TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 7.2 Tabela `person_roles` (Papéis da Pessoa)
|
||||
|
||||
```sql
|
||||
CREATE TABLE person_roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
person_id INTEGER REFERENCES persons(id) NOT NULL,
|
||||
role_type VARCHAR(20) NOT NULL, -- customer, supplier, employee, technician, partner
|
||||
-- Campos específicos por papel
|
||||
credit_limit NUMERIC(12,2), -- para customer
|
||||
payment_terms VARCHAR(50), -- para supplier
|
||||
employee_code VARCHAR(20), -- para employee
|
||||
commission_rate NUMERIC(5,2), -- para employee/technician
|
||||
hire_date DATE, -- para employee
|
||||
specialization VARCHAR(100), -- para technician
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 7.3 Campos Adicionais em `mobile_devices`
|
||||
|
||||
```sql
|
||||
ALTER TABLE mobile_devices ADD COLUMN acquisition_type VARCHAR(20) DEFAULT 'purchase';
|
||||
-- Valores: trade_in, purchase, consignment, internal_transfer
|
||||
ALTER TABLE mobile_devices ADD COLUMN acquisition_cost NUMERIC(12,2);
|
||||
ALTER TABLE mobile_devices ADD COLUMN related_evaluation_id INTEGER REFERENCES device_evaluations(id);
|
||||
ALTER TABLE mobile_devices ADD COLUMN related_service_order_id INTEGER REFERENCES service_orders(id);
|
||||
ALTER TABLE mobile_devices ADD COLUMN person_id INTEGER REFERENCES persons(id); -- substituir supplier_id
|
||||
```
|
||||
|
||||
### 7.4 Campos Adicionais em `service_orders`
|
||||
|
||||
```sql
|
||||
ALTER TABLE service_orders ADD COLUMN is_internal BOOLEAN DEFAULT false;
|
||||
ALTER TABLE service_orders ADD COLUMN internal_type VARCHAR(30); -- revision, cleaning, maintenance
|
||||
ALTER TABLE service_orders ADD COLUMN source_evaluation_id INTEGER REFERENCES device_evaluations(id);
|
||||
```
|
||||
|
||||
### 7.5 Tabela `imei_history` (Kardex do IMEI)
|
||||
|
||||
```sql
|
||||
CREATE TABLE imei_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id INTEGER REFERENCES mobile_devices(id) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
-- entrada, venda, devolucao, transferencia, reparo_iniciado, reparo_finalizado, trade_in_aprovado
|
||||
previous_status VARCHAR(50),
|
||||
new_status VARCHAR(50),
|
||||
related_order_id INTEGER, -- pode ser OS, venda, transferência
|
||||
related_order_type VARCHAR(20), -- service_order, sale, transfer, evaluation
|
||||
notes TEXT,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Próximos Passos Imediatos
|
||||
|
||||
1. **Criar tabela `imei_history`** para rastreamento completo
|
||||
2. **Implementar API de histórico** GET `/devices/:imei/history`
|
||||
3. **Melhorar fluxo de status** da O.S. com validações
|
||||
4. **Criar componente de carrinho** funcional no PDV
|
||||
5. **Implementar geração de orçamento** automático
|
||||
|
||||
---
|
||||
|
||||
## 8. Integrações Existentes a Utilizar
|
||||
|
||||
| Sistema | Uso | Status |
|
||||
|---------|-----|--------|
|
||||
| ERPNext | Sincronizar clientes, produtos, fornecedores | ✅ Conectado |
|
||||
| WhatsApp (Baileys) | Notificações ao cliente | ✅ Disponível |
|
||||
| OpenAI | Sugestões inteligentes | ✅ Disponível |
|
||||
| PostgreSQL | Banco de dados | ✅ Em uso |
|
||||
|
||||
---
|
||||
|
||||
*Este documento deve ser atualizado conforme o progresso da implementação.*
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
# Arcádia XOS - Planejamento Consolidado
|
||||
|
||||
## Visão Geral
|
||||
|
||||
**XOS (Experience Operating System)** é o sistema operacional de experiência do cliente do Arcádia Suite, unificando marketing, vendas, atendimento e operações em uma única plataforma nativa.
|
||||
|
||||
### Mantra
|
||||
> "O cliente no centro. A experiência como diferencial. A tecnologia como facilitadora."
|
||||
|
||||
### Inspiração
|
||||
Baseado nos conceitos do erxes XOS, implementado nativamente na stack do Arcádia Suite (React + Express + PostgreSQL).
|
||||
|
||||
---
|
||||
|
||||
## Arquitetura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA XOS │
|
||||
│ Experience Operating System │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
|
||||
│ │ SITE BUILDER │ │ MARKETING │ │ CRM AVANÇADO │ │ ATENDIMENTO │ │
|
||||
│ │ │ │ HUB │ │ │ │ │ │
|
||||
│ │ • Editor │ │ • Campanhas │ │ • Pipeline │ │ • Inbox │ │
|
||||
│ │ • Templates │ │ • Automações │ │ • Contatos │ │ • Tickets │ │
|
||||
│ │ • Importador │ │ • Landing │ │ • Lead Score │ │ • Chatbot │ │
|
||||
│ │ • Domínios │ │ • Email/SMS │ │ • Segmentos │ │ • SLA │ │
|
||||
│ └───────────────┘ └───────────────┘ └───────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ VISÃO 360° DO CLIENTE │ │
|
||||
│ │ Histórico completo: conversas, compras, tickets, interações │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ CAMADA DE INTELIGÊNCIA │ │
|
||||
│ │ Manus AI | Knowledge Graph | Dev Agent | Scientist │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Módulos do XOS
|
||||
|
||||
### 1. XOS Central (Hub)
|
||||
**Rota:** `/xos`
|
||||
|
||||
| Componente | Descrição |
|
||||
|------------|-----------|
|
||||
| Dashboard | Visão geral de métricas (leads, tickets, conversões) |
|
||||
| Visão 360° | Perfil completo do cliente em uma tela |
|
||||
| Timeline | Histórico de todas as interações |
|
||||
| Quick Actions | Ações rápidas (enviar mensagem, criar ticket, etc) |
|
||||
|
||||
### 2. CRM Avançado
|
||||
**Rota:** `/xos/crm`
|
||||
|
||||
| Funcionalidade | Descrição |
|
||||
|----------------|-----------|
|
||||
| Pipeline Visual | Kanban de oportunidades com drag & drop |
|
||||
| Contatos | Gestão unificada de leads e clientes |
|
||||
| Empresas | Gestão de contas B2B |
|
||||
| Segmentação | Criar grupos por comportamento/atributos |
|
||||
| Lead Scoring | Pontuação automática de leads |
|
||||
| Atividades | Tarefas, ligações, reuniões agendadas |
|
||||
| Relatórios | Funil, conversão, performance de vendedores |
|
||||
|
||||
### 3. Marketing Hub
|
||||
**Rota:** `/xos/marketing`
|
||||
|
||||
| Funcionalidade | Descrição |
|
||||
|----------------|-----------|
|
||||
| Campanhas | Criar e gerenciar campanhas multicanal |
|
||||
| Email Marketing | Templates, envio, rastreamento |
|
||||
| SMS Marketing | Integração Twilio/Zenvia |
|
||||
| WhatsApp Marketing | Campanhas via WhatsApp Business |
|
||||
| Automações | Fluxos de nutrição e follow-up |
|
||||
| Landing Pages | Páginas de captura integradas |
|
||||
| Formulários | Captura de leads com campos customizados |
|
||||
| Pop-ups | Widgets de captura para sites |
|
||||
| Analytics | ROI de campanhas, conversões |
|
||||
|
||||
### 4. Atendimento (Inbox Omnichannel)
|
||||
**Rota:** `/xos/inbox`
|
||||
|
||||
| Funcionalidade | Descrição |
|
||||
|----------------|-----------|
|
||||
| Inbox Unificado | WhatsApp + Chat + Email em uma tela |
|
||||
| Tickets | Sistema de suporte com prioridades |
|
||||
| SLA | Tempo de resposta e resolução |
|
||||
| Atribuição | Distribuição automática para atendentes |
|
||||
| Tags | Categorização de conversas |
|
||||
| Macros | Respostas rápidas padronizadas |
|
||||
| Knowledge Base | Base de conhecimento para clientes |
|
||||
| Chatbot | Atendimento automático inicial |
|
||||
| Satisfação | Pesquisa NPS/CSAT após atendimento |
|
||||
|
||||
### 5. Site Builder
|
||||
**Rota:** `/xos/sites`
|
||||
|
||||
| Funcionalidade | Descrição |
|
||||
|----------------|-----------|
|
||||
| Editor Visual | Drag & drop de blocos |
|
||||
| Templates | Modelos prontos por segmento |
|
||||
| Importador | Importar sites existentes (HTML/CSS/URL) |
|
||||
| Páginas | Criar páginas ilimitadas |
|
||||
| Blog | Sistema de publicação de conteúdo |
|
||||
| SEO | Meta tags, sitemap, Open Graph |
|
||||
| Domínios | Conectar domínio próprio |
|
||||
| Analytics | Visitas, conversões, origem |
|
||||
| Responsivo | Preview mobile/tablet/desktop |
|
||||
|
||||
### 6. Automações
|
||||
**Rota:** `/xos/automations`
|
||||
|
||||
| Funcionalidade | Descrição |
|
||||
|----------------|-----------|
|
||||
| Workflow Builder | Editor visual de fluxos |
|
||||
| Triggers | Eventos que disparam automações |
|
||||
| Ações | Email, WhatsApp, criar tarefa, webhook |
|
||||
| Condições | Filtros e ramificações |
|
||||
| Agendamento | Executar em horário específico |
|
||||
| Logs | Histórico de execuções |
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de Dados (PostgreSQL)
|
||||
|
||||
### Tabelas Principais
|
||||
|
||||
```sql
|
||||
-- Contatos unificados
|
||||
xos_contacts (
|
||||
id, company_id, type, -- lead, customer, partner
|
||||
name, email, phone, whatsapp,
|
||||
avatar_url, tags[], custom_fields,
|
||||
lead_score, lead_status, -- new, qualified, customer
|
||||
source, -- website, whatsapp, manual, import
|
||||
assigned_to, -- user_id
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
-- Empresas/Contas
|
||||
xos_companies (
|
||||
id, company_id, name, domain,
|
||||
industry, size, revenue,
|
||||
address, city, state,
|
||||
contacts[], -- array de contact_ids
|
||||
custom_fields,
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
-- Pipeline de Vendas
|
||||
xos_pipelines (
|
||||
id, company_id, name,
|
||||
stages[], -- array de estágios
|
||||
is_default, created_at
|
||||
)
|
||||
|
||||
xos_deals (
|
||||
id, company_id, pipeline_id, stage_id,
|
||||
contact_id, company_id,
|
||||
title, value, currency,
|
||||
expected_close_date,
|
||||
assigned_to, status, -- open, won, lost
|
||||
lost_reason, won_reason,
|
||||
created_at, updated_at, closed_at
|
||||
)
|
||||
|
||||
-- Inbox / Conversas
|
||||
xos_conversations (
|
||||
id, company_id, contact_id,
|
||||
channel, -- whatsapp, chat, email
|
||||
channel_id, -- session_id do canal
|
||||
status, -- open, pending, resolved
|
||||
assigned_to, tags[],
|
||||
first_response_at, resolved_at,
|
||||
satisfaction_score,
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
xos_messages (
|
||||
id, conversation_id,
|
||||
direction, -- inbound, outbound
|
||||
sender_type, sender_id, -- contact ou user
|
||||
content, content_type, -- text, image, file
|
||||
metadata, -- attachments, etc
|
||||
read_at, created_at
|
||||
)
|
||||
|
||||
-- Tickets
|
||||
xos_tickets (
|
||||
id, company_id, contact_id, conversation_id,
|
||||
subject, description,
|
||||
priority, -- low, medium, high, urgent
|
||||
status, -- open, pending, in_progress, resolved, closed
|
||||
category, tags[],
|
||||
assigned_to,
|
||||
sla_due_at, first_response_at, resolved_at,
|
||||
satisfaction_score,
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
-- Campanhas
|
||||
xos_campaigns (
|
||||
id, company_id, name,
|
||||
type, -- email, sms, whatsapp
|
||||
status, -- draft, scheduled, running, completed
|
||||
segment_id, -- filtro de contatos
|
||||
content, template_id,
|
||||
scheduled_at, started_at, completed_at,
|
||||
stats, -- sent, delivered, opened, clicked
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
-- Automações
|
||||
xos_automations (
|
||||
id, company_id, name,
|
||||
trigger_type, trigger_config,
|
||||
actions[], -- array de ações
|
||||
conditions[], -- filtros
|
||||
is_active,
|
||||
stats, -- executions, success, failures
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
-- Sites
|
||||
xos_sites (
|
||||
id, company_id, name,
|
||||
domain, subdomain,
|
||||
template_id,
|
||||
settings, -- colors, fonts, etc
|
||||
is_published,
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
xos_pages (
|
||||
id, site_id,
|
||||
title, slug, path,
|
||||
blocks[], -- array de blocos do editor
|
||||
meta_title, meta_description,
|
||||
is_published,
|
||||
created_at, updated_at
|
||||
)
|
||||
|
||||
-- Blocos reutilizáveis
|
||||
xos_blocks (
|
||||
id, company_id, name,
|
||||
type, -- header, hero, features, pricing, etc
|
||||
content, -- JSON do bloco
|
||||
is_global, -- disponível para todos
|
||||
created_at
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integrações Nativas
|
||||
|
||||
| Sistema | Integração |
|
||||
|---------|------------|
|
||||
| **WhatsApp** | Baileys (já existe) → alimenta Inbox |
|
||||
| **Chat Interno** | Já existe → alimenta Inbox |
|
||||
| **ERPNext** | Clientes, produtos, pedidos |
|
||||
| **Arcádia Fisco** | NF-e vinculada a deals |
|
||||
| **Arcádia Retail** | Vendas PDV → histórico cliente |
|
||||
| **Knowledge Graph** | Indexa conversas e interações |
|
||||
| **Manus AI** | Atendimento automático, sugestões |
|
||||
| **Dev Agent** | Customiza todos os módulos |
|
||||
| **Scientist** | Análises e relatórios avançados |
|
||||
|
||||
---
|
||||
|
||||
## Casos de Uso
|
||||
|
||||
### 1. Venda Interna do Arcádia Suite
|
||||
```
|
||||
Landing Page → Formulário → Lead no CRM →
|
||||
Automação de nutrição → Manus qualifica →
|
||||
Demo agendada → Proposta → Fechamento →
|
||||
Onboarding automático
|
||||
```
|
||||
|
||||
### 2. E-commerce (Cliente usando)
|
||||
```
|
||||
Visitante → Pop-up com cupom → Lead capturado →
|
||||
Automação de abandono de carrinho →
|
||||
WhatsApp com lembrete → Compra →
|
||||
Pós-venda via Inbox → Pesquisa NPS
|
||||
```
|
||||
|
||||
### 3. Serviços (Cliente usando)
|
||||
```
|
||||
Site com formulário → Lead → Pipeline de propostas →
|
||||
Proposta enviada → Follow-up automático →
|
||||
Contrato fechado → Projeto criado →
|
||||
Suporte via Tickets → Renovação automática
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fases de Implementação
|
||||
|
||||
### Fase 1: Fundação (2 semanas)
|
||||
- [ ] Estrutura de dados (tabelas PostgreSQL)
|
||||
- [ ] API base do XOS
|
||||
- [ ] XOS Central (dashboard e navegação)
|
||||
- [ ] Migração de contatos existentes
|
||||
|
||||
### Fase 2: CRM (2 semanas)
|
||||
- [ ] Pipeline visual (Kanban)
|
||||
- [ ] Gestão de contatos
|
||||
- [ ] Lead scoring básico
|
||||
- [ ] Atividades e tarefas
|
||||
|
||||
### Fase 3: Inbox Omnichannel (2 semanas)
|
||||
- [ ] Inbox unificado
|
||||
- [ ] Integração WhatsApp existente
|
||||
- [ ] Integração Chat existente
|
||||
- [ ] Sistema de tickets
|
||||
|
||||
### Fase 4: Marketing (2 semanas)
|
||||
- [ ] Campanhas de email
|
||||
- [ ] Campanhas WhatsApp
|
||||
- [ ] Automações básicas
|
||||
- [ ] Landing pages
|
||||
|
||||
### Fase 5: Site Builder (2 semanas)
|
||||
- [ ] Editor visual
|
||||
- [ ] Biblioteca de blocos
|
||||
- [ ] Importador de sites
|
||||
- [ ] Publicação e domínios
|
||||
|
||||
### Fase 6: Inteligência (2 semanas)
|
||||
- [ ] Visão 360° completa
|
||||
- [ ] Integração Manus AI
|
||||
- [ ] Analytics e relatórios
|
||||
- [ ] Chatbot avançado
|
||||
|
||||
---
|
||||
|
||||
## Monetização (Marketplace)
|
||||
|
||||
| Plano | Preço/mês | Inclui |
|
||||
|-------|-----------|--------|
|
||||
| **XOS Starter** | R$ 149 | CRM + Inbox (até 1.000 contatos) |
|
||||
| **XOS Pro** | R$ 299 | + Marketing + Automações (até 10.000 contatos) |
|
||||
| **XOS Business** | R$ 499 | + Site Builder + API (até 50.000 contatos) |
|
||||
| **XOS Enterprise** | Sob consulta | Ilimitado + Suporte dedicado |
|
||||
|
||||
---
|
||||
|
||||
## Métricas de Sucesso
|
||||
|
||||
| Métrica | Meta |
|
||||
|---------|------|
|
||||
| Tempo de implementação | 12 semanas |
|
||||
| Conversão de leads | +30% |
|
||||
| Tempo de resposta | -50% |
|
||||
| Satisfação do cliente | NPS > 50 |
|
||||
| Adoção do Marketplace | 100 assinaturas em 6 meses |
|
||||
|
||||
---
|
||||
|
||||
## Tecnologias
|
||||
|
||||
| Camada | Tecnologia |
|
||||
|--------|------------|
|
||||
| Frontend | React + TypeScript + Tailwind + shadcn/ui |
|
||||
| Backend | Express + Drizzle ORM |
|
||||
| Banco | PostgreSQL |
|
||||
| Real-time | Socket.IO (já existe) |
|
||||
| Email | Integração SMTP / Amazon SES |
|
||||
| SMS | Twilio / Zenvia |
|
||||
| WhatsApp | Baileys (já existe) |
|
||||
| IA | OpenAI API (já existe) |
|
||||
|
||||
---
|
||||
|
||||
## Próximos Passos
|
||||
|
||||
1. **Aprovar planejamento** ✓
|
||||
2. **Criar estrutura de dados** (tabelas)
|
||||
3. **Implementar XOS Central** (hub)
|
||||
4. **Desenvolver módulos** em fases
|
||||
5. **Testes e ajustes**
|
||||
6. **Lançamento no Marketplace**
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
-- Migration 0003: Arcádia Agentic Suite — Skills POO
|
||||
-- Criado em: 2026-03-24 | Autor: João
|
||||
-- Adiciona tabelas arcadia_skills e skill_executions
|
||||
-- NÃO altera nem remove xos_skill_registry (modelo XOS legado preservado)
|
||||
|
||||
-- ─── Tabela principal de Skills (modelo orientado a objetos) ─────────────────
|
||||
CREATE TABLE IF NOT EXISTS "arcadia_skills" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Identidade
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"slug" VARCHAR(255) NOT NULL,
|
||||
"description" TEXT,
|
||||
"version" VARCHAR(50) NOT NULL DEFAULT '1.0.0',
|
||||
"icon" VARCHAR(100),
|
||||
"tags" TEXT[],
|
||||
|
||||
-- Namespace multi-tenant (system > tenant > company > user)
|
||||
"namespace" VARCHAR(20) NOT NULL DEFAULT 'tenant',
|
||||
"tenant_id" INTEGER REFERENCES "tenants"("id") ON DELETE CASCADE,
|
||||
"company_id" INTEGER,
|
||||
"user_id" VARCHAR REFERENCES "users"("id") ON DELETE SET NULL,
|
||||
|
||||
-- Herança POO: lista de slugs referenciando outras skills
|
||||
"extends" TEXT[],
|
||||
-- Interfaces / contratos implementados
|
||||
"implements" TEXT[],
|
||||
|
||||
-- Encapsulamento
|
||||
"visibility_execute" VARCHAR(20) DEFAULT 'public',
|
||||
"visibility_params" VARCHAR(20) DEFAULT 'public',
|
||||
|
||||
-- Composição: dependências como referências /tipo/caminho
|
||||
"dependencies" TEXT[],
|
||||
|
||||
-- Trigger automático
|
||||
"trigger_type" VARCHAR(30),
|
||||
"trigger_config" JSONB,
|
||||
|
||||
-- Corpo da skill (Markdown com blocos /skill/, /kg/, /tool/, etc.)
|
||||
"body" TEXT,
|
||||
|
||||
-- Schemas de entrada e saída
|
||||
"parameters_schema" JSONB,
|
||||
"return_schema" JSONB,
|
||||
|
||||
-- Estado
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'draft',
|
||||
"is_system" BOOLEAN DEFAULT false,
|
||||
|
||||
-- Autoria
|
||||
"author" VARCHAR(255),
|
||||
"created_by" VARCHAR REFERENCES "users"("id") ON DELETE SET NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Slug único por namespace + tenant
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_arcadia_skills_slug_tenant
|
||||
ON arcadia_skills(slug, tenant_id, namespace);
|
||||
|
||||
-- Índices de consulta frequente
|
||||
CREATE INDEX IF NOT EXISTS idx_arcadia_skills_tenant ON arcadia_skills(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_arcadia_skills_namespace ON arcadia_skills(namespace);
|
||||
CREATE INDEX IF NOT EXISTS idx_arcadia_skills_status ON arcadia_skills(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_arcadia_skills_created_by ON arcadia_skills(created_by);
|
||||
|
||||
-- ─── Tabela de execuções de skills ──────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS "skill_executions" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"skill_id" UUID NOT NULL REFERENCES "arcadia_skills"("id") ON DELETE CASCADE,
|
||||
|
||||
-- Contexto de execução
|
||||
"tenant_id" INTEGER REFERENCES "tenants"("id"),
|
||||
"company_id" INTEGER,
|
||||
"user_id" VARCHAR REFERENCES "users"("id") ON DELETE SET NULL,
|
||||
|
||||
-- Origem
|
||||
"triggered_by" VARCHAR(30),
|
||||
"automation_id" INTEGER,
|
||||
"parent_execution_id" UUID,
|
||||
|
||||
-- Dados
|
||||
"input_params" JSONB,
|
||||
"output_result" JSONB,
|
||||
"resolved_dependencies" JSONB,
|
||||
|
||||
-- Estado
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
"error_message" TEXT,
|
||||
"duration_ms" INTEGER,
|
||||
|
||||
-- Imutabilidade / auditoria
|
||||
"audit_hash" VARCHAR(64),
|
||||
|
||||
"started_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP
|
||||
);
|
||||
|
||||
-- Índices de consulta frequente
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_executions_skill_id ON skill_executions(skill_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_executions_tenant ON skill_executions(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_executions_status ON skill_executions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_executions_started ON skill_executions(started_at DESC);
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
-- Migration 0004: OpenClaw — Detecção de Padrões e Sugestões de Skills Emergentes
|
||||
-- Fase 4 do planejamento estratégico Arcádia Agentic Suite
|
||||
|
||||
-- Tabela de padrões detectados
|
||||
CREATE TABLE IF NOT EXISTS "detected_patterns" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"tenant_id" INTEGER REFERENCES "tenants"("id"),
|
||||
"user_id" VARCHAR REFERENCES "users"("id"),
|
||||
|
||||
-- Padrão detectado
|
||||
"action_type" VARCHAR(100) NOT NULL,
|
||||
"description" TEXT,
|
||||
"frequency" INTEGER NOT NULL DEFAULT 0,
|
||||
"confidence" NUMERIC(4, 3) NOT NULL DEFAULT '0',
|
||||
|
||||
-- Janela de análise
|
||||
"first_seen_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_seen_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Metadados do padrão (contexto, módulos envolvidos, etc.)
|
||||
"metadata" JSONB DEFAULT '{}',
|
||||
|
||||
-- Ciclo de vida: active | archived | converted
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "detected_patterns_tenant_user_idx"
|
||||
ON "detected_patterns"("tenant_id", "user_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "detected_patterns_action_type_idx"
|
||||
ON "detected_patterns"("action_type");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "detected_patterns_status_idx"
|
||||
ON "detected_patterns"("status");
|
||||
|
||||
-- Tabela de sugestões de skills emergentes
|
||||
CREATE TABLE IF NOT EXISTS "skill_suggestions" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"pattern_id" UUID REFERENCES "detected_patterns"("id") ON DELETE CASCADE,
|
||||
"tenant_id" INTEGER REFERENCES "tenants"("id"),
|
||||
"user_id" VARCHAR REFERENCES "users"("id"),
|
||||
|
||||
-- Skill sugerida
|
||||
"suggested_skill_name" VARCHAR(200) NOT NULL,
|
||||
"suggested_description" TEXT,
|
||||
"estimated_automation" TEXT,
|
||||
"confidence" NUMERIC(4, 3) NOT NULL DEFAULT '0',
|
||||
|
||||
-- Skill gerada (após confirmação)
|
||||
"generated_skill_id" UUID REFERENCES "skills"("id"),
|
||||
|
||||
-- Estado da sugestão: pending | accepted | rejected | expired
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
|
||||
-- Rastreabilidade
|
||||
"source" VARCHAR(50) NOT NULL DEFAULT 'openclaw',
|
||||
"reviewed_by" VARCHAR REFERENCES "users"("id"),
|
||||
"reviewed_at" TIMESTAMP,
|
||||
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_suggestions_pattern_id_idx"
|
||||
ON "skill_suggestions"("pattern_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_suggestions_tenant_user_idx"
|
||||
ON "skill_suggestions"("tenant_id", "user_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_suggestions_status_idx"
|
||||
ON "skill_suggestions"("status");
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
-- Migration: Add skill_versions table for Git-like versioning
|
||||
-- Phase 2.4: Versionamento Git-like de skills
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
skill_id UUID NOT NULL REFERENCES arcadia_skills(id) ON DELETE CASCADE,
|
||||
sha VARCHAR(64) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
author VARCHAR(255) NOT NULL,
|
||||
content JSONB NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Índices
|
||||
UNIQUE(skill_id, sha),
|
||||
INDEX skill_versions_skill_id (skill_id),
|
||||
INDEX skill_versions_created_at (created_at)
|
||||
);
|
||||
|
||||
-- Trigger para criar versão inicial ao criar skill (opcional)
|
||||
-- CREATE OR REPLACE FUNCTION create_initial_skill_version()
|
||||
-- RETURNS TRIGGER AS $$
|
||||
-- BEGIN
|
||||
-- INSERT INTO skill_versions (skill_id, sha, message, author, content)
|
||||
-- VALUES (
|
||||
-- NEW.id,
|
||||
-- encode(digest(NEW.id || NEW.content::text, 'sha256'), 'hex'),
|
||||
-- 'Initial version',
|
||||
-- 'system',
|
||||
-- NEW.content
|
||||
-- );
|
||||
-- RETURN NEW;
|
||||
-- END;
|
||||
-- $$ LANGUAGE plpgsql;
|
||||
--
|
||||
-- CREATE TRIGGER skill_initial_version AFTER INSERT ON arcadia_skills
|
||||
-- FOR EACH ROW EXECUTE FUNCTION create_initial_skill_version();
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
-- Phase 4: OpenClaw — Tabelas de detecção de padrões e sugestões de skills emergentes
|
||||
-- Executar se as tabelas ainda não existirem (schema já definido em shared/schema.ts)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS detected_patterns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
user_id VARCHAR REFERENCES users(id),
|
||||
action_type VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
frequency INTEGER NOT NULL DEFAULT 0,
|
||||
confidence NUMERIC(4,3) NOT NULL DEFAULT 0,
|
||||
first_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_suggestions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pattern_id UUID REFERENCES detected_patterns(id) ON DELETE CASCADE,
|
||||
tenant_id INTEGER REFERENCES tenants(id),
|
||||
user_id VARCHAR REFERENCES users(id),
|
||||
suggested_skill_name VARCHAR(200) NOT NULL,
|
||||
suggested_description TEXT,
|
||||
estimated_automation TEXT,
|
||||
confidence NUMERIC(4,3) NOT NULL DEFAULT 0,
|
||||
generated_skill_id UUID REFERENCES arcadia_skills(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
source VARCHAR(50) NOT NULL DEFAULT 'openclaw',
|
||||
reviewed_by VARCHAR REFERENCES users(id),
|
||||
reviewed_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_detected_patterns_status ON detected_patterns(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_detected_patterns_user ON detected_patterns(user_id, action_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_suggestions_status ON skill_suggestions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_suggestions_pattern ON skill_suggestions(pattern_id);
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,7 +9,10 @@
|
|||
"build": "tsx script/build.ts",
|
||||
"start": "NODE_ENV=production node dist/index.cjs",
|
||||
"check": "tsc",
|
||||
"db:push": "drizzle-kit push"
|
||||
"db:push": "drizzle-kit push",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:openclaw": "vitest run tests/openclaw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
APP_NAME="MyApp"
|
||||
APP_FONE="55000000000"
|
||||
APP_ENV=local
|
||||
# Atenção: APP_KEY é obrigatória no .env.
|
||||
# Ela pode estar vazia (APP_KEY=) antes do build,
|
||||
# pois será gerada automaticamente pelo "php artisan key:generate".
|
||||
# Apenas não deve ser removida do arquivo .env.
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://app.exemplo.com/
|
||||
PATH_STORE="store.exemplo.com"
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=myapp_db
|
||||
DB_USERNAME=myapp_user
|
||||
DB_PASSWORD=myapp_pass
|
||||
MYSQL_ROOT_PASSWORD=rootpass123
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.exemplo.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME="contato@exemplo.com"
|
||||
MAIL_PASSWORD="minhasenha123"
|
||||
MAIL_ENCRYPTION=ssl
|
||||
MAIL_FROM_ADDRESS="no-reply@exemplo.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
SIGNUP=1
|
||||
MAILMASTER="master@exemplo.com"
|
||||
SITE_SUPORTE="https://suporte.exemplo.com"
|
||||
MARKETPLACE=1
|
||||
ECOMMERCE=1
|
||||
MERCADOLIVRE=1
|
||||
NUVEMSHOP=1
|
||||
WOOCOMMERCE=1
|
||||
CONTADOR=1
|
||||
PAGINACAO=30
|
||||
RESERVAS=1
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# ========================================
|
||||
# LARAVEL / PHP
|
||||
# ========================================
|
||||
|
||||
/vendor/
|
||||
/node_modules/
|
||||
|
||||
# Composer PHAR
|
||||
composer.phar
|
||||
|
||||
|
||||
|
||||
# Laravel storage & cache
|
||||
/storage/*.key
|
||||
/storage/app/*
|
||||
/storage/framework/cache/data/*
|
||||
/storage/framework/sessions/*
|
||||
/storage/framework/testing/*
|
||||
/storage/framework/views/*
|
||||
/storage/logs/*
|
||||
.npm/
|
||||
.composer/
|
||||
psysh_history
|
||||
# Bootstrap cache: versionar só a pasta, não os arquivos gerados
|
||||
/bootstrap/cache/*
|
||||
!/bootstrap/cache/.gitignore
|
||||
|
||||
# Laravel .env files
|
||||
.env
|
||||
.env.backup
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.testing
|
||||
.bash*
|
||||
|
||||
# Laravel Homestead
|
||||
Homestead.yaml
|
||||
Homestead.json
|
||||
|
||||
# SQLite databases
|
||||
database/*.sqlite
|
||||
|
||||
# Cache
|
||||
.phpunit.result.cache
|
||||
.phpunit.cache
|
||||
.phpstan/resultCache.php
|
||||
phpstan.neon.tmp
|
||||
|
||||
# Ignorar debugbar (se usar)
|
||||
public/debugbar/*
|
||||
|
||||
# Ignorar Telescope (se usar)
|
||||
storage/telescope/*
|
||||
storage/logs/telescope.log
|
||||
|
||||
# ========================================
|
||||
# IDEs & Sistemas Operacionais
|
||||
# ========================================
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
.history/
|
||||
*.code-workspace
|
||||
|
||||
# PHPStorm / JetBrains
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# ========================================
|
||||
# NODE / FRONTEND
|
||||
# ========================================
|
||||
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# Vite / Mix
|
||||
/public/hot
|
||||
/public/build
|
||||
/public/mix-manifest.json
|
||||
/public/vite.svg
|
||||
resources/js/*.hot-update.js
|
||||
resources/css/*.hot-update.js
|
||||
|
||||
# ========================================
|
||||
# DOCKER
|
||||
# ========================================
|
||||
|
||||
docker-compose.override.yml
|
||||
docker-compose.override.yaml
|
||||
docker/*.env
|
||||
docker/.env
|
||||
.docker/
|
||||
.dockerignore
|
||||
docker-data/
|
||||
*.pid
|
||||
|
||||
# ========================================
|
||||
# TESTS
|
||||
# ========================================
|
||||
|
||||
/coverage/*
|
||||
/reports/*
|
||||
/tests/tmp/*
|
||||
|
||||
# ========================================
|
||||
# zip do projeto
|
||||
# ========================================
|
||||
|
||||
control_plus.zip
|
||||
*.zip
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
RewriteEngine on
|
||||
RewriteCond %{REQUEST_URI} !^public
|
||||
RewriteRule ^(.*)$ public/$1 [L]
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
FROM php:8.2-fpm
|
||||
|
||||
ARG UID=1000
|
||||
ARG GID=33
|
||||
|
||||
# Sistema + extensões PHP + Node 18
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
unzip \
|
||||
curl \
|
||||
libzip-dev \
|
||||
libpng-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libfreetype6-dev \
|
||||
libicu-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
zip \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
bcmath \
|
||||
exif \
|
||||
gd \
|
||||
intl \
|
||||
pcntl \
|
||||
pdo_mysql \
|
||||
sockets \
|
||||
zip \
|
||||
soap \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Ajusta UID/GID do www-data para bater com o host (rob: dockershared)
|
||||
RUN groupmod -g ${GID} www-data \
|
||||
&& usermod -u ${UID} -g ${GID} www-data
|
||||
|
||||
# Composer da imagem oficial
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Copia o script de entrypoint (como root mesmo)
|
||||
COPY docker/check-env.sh /usr/local/bin/check-env.sh
|
||||
RUN chmod +x /usr/local/bin/check-env.sh
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# A partir daqui o usuário padrão é www-data (mesmo UID do rob)
|
||||
USER www-data
|
||||
|
||||
# Define o home do www-data
|
||||
ENV HOME=/var/www/html
|
||||
|
||||
# Configs úteis do Composer
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
EXPOSE 9000 5173
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/check-env.sh"]
|
||||
CMD ["php-fpm"]
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# ---------------------------------------
|
||||
# Controle do ambiente Docker - Laravel + Vite + MySQL
|
||||
# ---------------------------------------
|
||||
|
||||
COMPOSE = docker compose
|
||||
APP_SERVICE = app
|
||||
DB_SERVICE = db
|
||||
|
||||
DB_WAIT_RETRIES ?= 30
|
||||
DB_WAIT_SECONDS ?= 2
|
||||
|
||||
REQUIRED_ENV = \
|
||||
APP_ENV \
|
||||
APP_URL \
|
||||
APP_KEY \
|
||||
DB_CONNECTION \
|
||||
DB_HOST \
|
||||
DB_PORT \
|
||||
DB_DATABASE \
|
||||
DB_USERNAME \
|
||||
DB_PASSWORD
|
||||
|
||||
.PHONY: setup up dev \
|
||||
check-env wait-db \
|
||||
perms docker-up docker-up-build docker-down docker-down-volumes \
|
||||
composer-install npm-install assets-build \
|
||||
key migrate-seed
|
||||
|
||||
# ---------------------------
|
||||
# Setup completo (instalação inicial)
|
||||
# ---------------------------
|
||||
setup: check-env docker-up-build perms composer-install npm-install wait-db key migrate-seed assets-build docker-down
|
||||
@echo "✅ Setup completo (containers parados)."
|
||||
|
||||
# ---------------------------
|
||||
# Subir ambiente normal (sem Vite)
|
||||
# ---------------------------
|
||||
up: check-env docker-up
|
||||
@echo "✅ Containers ativos:"
|
||||
@echo " Backend: http://localhost:8000"
|
||||
|
||||
# ---------------------------
|
||||
# Subir ambiente dev (com Vite)
|
||||
# ---------------------------
|
||||
dev: check-env docker-up
|
||||
$(COMPOSE) exec -d $(APP_SERVICE) npm run dev -- --host 0.0.0.0 --port 5173
|
||||
@echo "✅ Ambiente DEV iniciado:"
|
||||
@echo " Backend: http://localhost:8000"
|
||||
@echo " Vite: http://localhost:5173"
|
||||
|
||||
# ---------------------------
|
||||
# Remove tudo relacionado ao projeto e reconstrói do zero
|
||||
# ---------------------------
|
||||
rebuild-force:
|
||||
@echo "🧨 Removendo containers, volumes, redes e imagens relacionadas..."
|
||||
$(COMPOSE) down -v --rmi all --remove-orphans
|
||||
@echo "🧨 Removendo imagens órfãs..."
|
||||
docker image prune -f
|
||||
docker volume prune -f
|
||||
@echo "🔨 Reconstruindo tudo do zero..."
|
||||
$(MAKE) setup
|
||||
# ---------------------------
|
||||
# Helpers internos
|
||||
# ---------------------------
|
||||
check-env:
|
||||
@echo "Verificando .env..."
|
||||
@if [ ! -f .env ]; then \
|
||||
echo "ERRO: .env não encontrado."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
missing=""; \
|
||||
for var in $(REQUIRED_ENV); do \
|
||||
if ! grep -q "^$$var=" .env; then \
|
||||
if [ "$$var" = "APP_KEY" ]; then \
|
||||
echo "ERRO: APP_KEY deve existir no .env (pode estar vazia: APP_KEY=, ou conter uma chave válida)."; \
|
||||
fi; \
|
||||
missing="$$missing $$var"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [ -n "$$missing" ]; then \
|
||||
echo "ERRO: variáveis faltando no .env:"; \
|
||||
echo " $$missing"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo ".env OK."
|
||||
|
||||
|
||||
# Espera real pelo MySQL
|
||||
wait-db:
|
||||
@echo "Aguardando MySQL..."
|
||||
@$(COMPOSE) exec -T $(DB_SERVICE) sh -c '\
|
||||
c=0; \
|
||||
while ! mysqladmin ping -h127.0.0.1 --silent 2>/dev/null; do \
|
||||
c=$$((c+1)); \
|
||||
if [ $$c -ge $(DB_WAIT_RETRIES) ]; then \
|
||||
echo "ERRO: MySQL não respondeu."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo " aguardando... (tentativa $$c)"; \
|
||||
sleep $(DB_WAIT_SECONDS); \
|
||||
done; \
|
||||
echo "✅ MySQL está pronto."; \
|
||||
'
|
||||
|
||||
perms:
|
||||
$(COMPOSE) exec -T $(APP_SERVICE) sh -c "\
|
||||
mkdir -p storage/logs bootstrap/cache && \
|
||||
chown -R www-data:www-data storage bootstrap/cache && \
|
||||
chmod -R 775 storage bootstrap/cache \
|
||||
"
|
||||
|
||||
docker-up-build:
|
||||
$(COMPOSE) up -d --build
|
||||
|
||||
docker-up:
|
||||
$(COMPOSE) up -d
|
||||
|
||||
docker-down:
|
||||
$(COMPOSE) down
|
||||
|
||||
docker-down-volumes:
|
||||
$(COMPOSE) down -v
|
||||
|
||||
composer-install:
|
||||
$(COMPOSE) exec -T $(APP_SERVICE) composer install --no-interaction --prefer-dist
|
||||
|
||||
npm-install:
|
||||
$(COMPOSE) exec -T $(APP_SERVICE) npm install
|
||||
|
||||
assets-build:
|
||||
$(COMPOSE) exec -T $(APP_SERVICE) npm run build
|
||||
|
||||
key:
|
||||
$(COMPOSE) exec -T $(APP_SERVICE) php artisan key:generate || true
|
||||
|
||||
migrate-seed:
|
||||
$(COMPOSE) exec -T $(APP_SERVICE) php artisan migrate --seed
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
# Ambiente Laravel com Docker + Makefile
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## UID/GID (permissões)
|
||||
|
||||
Ajuste o usuário local no `docker-compose.yml`:
|
||||
|
||||
```
|
||||
id -u # UID
|
||||
id -g # GID
|
||||
```
|
||||
|
||||
Atualize:
|
||||
|
||||
```yaml
|
||||
args:
|
||||
UID: <seu-uid>
|
||||
GID: <seu-gid>
|
||||
```
|
||||
|
||||
## Configuração do .env
|
||||
|
||||
Crie e valide:
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
make check-env
|
||||
```
|
||||
|
||||
## Comandos principais
|
||||
|
||||
### Containers
|
||||
|
||||
```
|
||||
make up
|
||||
make down
|
||||
make restart
|
||||
make logs
|
||||
make build
|
||||
```
|
||||
|
||||
### Setup inicial
|
||||
|
||||
```
|
||||
make first-build
|
||||
```
|
||||
|
||||
### Reset completo
|
||||
|
||||
```
|
||||
make fresh-install
|
||||
```
|
||||
|
||||
### Utilidades
|
||||
|
||||
```
|
||||
make composer COMPOSER_CMD="update"
|
||||
make npm NPM_CMD="run dev"
|
||||
make artisan ARTISAN_CMD="config:cache"
|
||||
make migrate
|
||||
make rollback
|
||||
make seed
|
||||
make fresh
|
||||
make bash
|
||||
make dbbash
|
||||
make perms
|
||||
```
|
||||
|
||||
## Acesso
|
||||
|
||||
- http://localhost:8000
|
||||
- Ou URL definida em `APP_URL`
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Models\ContaPagar;
|
||||
use App\Models\Notificacao;
|
||||
use App\Models\ItemNfe;
|
||||
use App\Models\Produto;
|
||||
use App\Models\Estoque;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\Agendamento;
|
||||
use App\Models\MensagemAgendamentoLog;
|
||||
use App\Models\ConfiguracaoAgendamento;
|
||||
use App\Utils\WhatsAppUtil;
|
||||
|
||||
class AlertaCron extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'alerta:cron';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Cria alertas para empresas';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
|
||||
protected $whatsAppUtil;
|
||||
|
||||
public function __construct(WhatsAppUtil $whatsAppUtil){
|
||||
parent::__construct();
|
||||
$this->whatsAppUtil = $whatsAppUtil;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$empresas = Empresa::where('status', 1)->get();
|
||||
|
||||
foreach($empresas as $empresa){
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', $empresa->id)->first();
|
||||
$alertasAtivos = null;
|
||||
if($config != null){
|
||||
$alertasAtivos = json_decode($config->notificacoes);
|
||||
}
|
||||
// dd($alertasAtivos);
|
||||
if($alertasAtivos == null || in_array('Contas a receber', $alertasAtivos)){
|
||||
$contasReceber = ContaReceber::where('empresa_id', $empresa->id)
|
||||
->where('status', 0)
|
||||
->whereDate('data_vencimento', date('Y-m-d'))
|
||||
->get();
|
||||
|
||||
foreach($contasReceber as $conta){
|
||||
$descricaoCurta = $conta->cliente->razao_social . " R$" . __moeda($conta->valor_integral);
|
||||
$this->criaNotificacao('conta_recebers', $conta->id, $empresa, 'Conta a receber', $descricaoCurta, $conta);
|
||||
}
|
||||
}
|
||||
|
||||
if($alertasAtivos == null || in_array('Contas a pagar', $alertasAtivos)){
|
||||
$contasPagar = ContaPagar::where('empresa_id', $empresa->id)
|
||||
->where('status', 0)
|
||||
->whereDate('data_vencimento', date('Y-m-d'))
|
||||
->get();
|
||||
|
||||
foreach($contasPagar as $conta){
|
||||
$descricaoCurta = $conta->fornecedor->razao_social . " R$" . __moeda($conta->valor_integral);
|
||||
$this->criaNotificacao('conta_pagars', $conta->id, $empresa, 'Conta a pagar', $descricaoCurta, $conta);
|
||||
}
|
||||
}
|
||||
|
||||
if($alertasAtivos == null || in_array('Alerta de validade', $alertasAtivos)){
|
||||
$produtosComAlertaValidade = Produto::where('empresa_id', $empresa->id)
|
||||
->where('alerta_validade', '>', 0)->get();
|
||||
foreach($produtosComAlertaValidade as $produto){
|
||||
$date = date('Y-m-d', strtotime(date('Y-m-d'). "+$produto->alerta_validade days"));
|
||||
$itens = ItemNfe::where('produto_id', $produto->id)
|
||||
->whereDate('data_vencimento', $date)
|
||||
->get();
|
||||
|
||||
foreach($itens as $i){
|
||||
$descricaoCurta = $i->produto->nome;
|
||||
$this->criaNotificacao('compras', $i->id, $empresa, 'Alerta de vencimento', $descricaoCurta, $i, 'media');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($alertasAtivos == null || in_array('Alerta de estoque', $alertasAtivos)){
|
||||
$produtosComEstoqueMinimo = Produto::where('empresa_id', $empresa->id)
|
||||
->where('estoque_minimo', '>', 0)->get();
|
||||
foreach($produtosComEstoqueMinimo as $produto){
|
||||
$estoque = Estoque::where('produto_id', $produto->id)->first();
|
||||
|
||||
if($estoque != null && $estoque->quantidade <= $produto->estoque_minimo){
|
||||
$descricaoCurta = $produto->nome;
|
||||
$this->criaNotificacao('estoques', $estoque->id, $empresa, 'Alerta de estoque', $descricaoCurta, $estoque, 'media');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$configuracaoAgendamento = ConfiguracaoAgendamento::where('empresa_id', $empresa->id)
|
||||
->first();
|
||||
|
||||
if($configuracaoAgendamento != null && $configuracaoAgendamento->token_whatsapp){
|
||||
$this->criaAlertaAgendamento($configuracaoAgendamento);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function criaAlertaAgendamento($config){
|
||||
$agendamentos = $this->getAgendamentosHoje($config->empresa_id);
|
||||
|
||||
if($config->msg_wpp_manha && $config->msg_wpp_manha_horario){
|
||||
$dataAtual = date('Y-m-d H:i');
|
||||
$dataEnvio = date('Y-m-d ') . $config->msg_wpp_manha_horario;
|
||||
if(strtotime($dataAtual) >= strtotime($dataEnvio)){
|
||||
foreach($agendamentos as $a){
|
||||
|
||||
if($a->cliente->telefone && $a->msg_wpp_manha_horario == 0){
|
||||
$msg = $this->criaMensagemAgendamento($a, $config->mensagem_manha);
|
||||
if($msg != ""){
|
||||
$telefone = "55".preg_replace('/[^0-9]/', '', $a->cliente->telefone);
|
||||
// dd($telefone);
|
||||
$retorno = $this->whatsAppUtil->sendMessageWithToken($telefone, $msg, $config->empresa_id, $config->token_whatsapp);
|
||||
$retorno = json_decode($retorno);
|
||||
if($retorno->success){
|
||||
$a->msg_wpp_manha_horario = 1;
|
||||
$a->save();
|
||||
MensagemAgendamentoLog::create([
|
||||
'empresa_id' => $config->empresa_id,
|
||||
'mensagem' => $msg,
|
||||
'cliente_id' => $a->cliente_id
|
||||
]);
|
||||
}else{
|
||||
// dd($retorno);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if($config->msg_wpp_alerta){
|
||||
$dataAtual = date('Y-m-d H:i');
|
||||
foreach($agendamentos as $a){
|
||||
|
||||
$dataEnvio = date('Y-m-d H:i', strtotime($a->data . " " . $a->inicio . "- $config->msg_wpp_alerta_minutos_antecedencia minutes"));
|
||||
|
||||
if(strtotime($dataAtual) >= strtotime($dataEnvio)){
|
||||
// foreach($agendamentos as $a){
|
||||
if($a->cliente->telefone && $a->msg_wpp_alerta_horario == 0){
|
||||
$msg = $this->criaMensagemAgendamento($a, $config->mensagem_alerta);
|
||||
|
||||
if($msg != ""){
|
||||
$telefone = "55".preg_replace('/[^0-9]/', '', $a->cliente->telefone);
|
||||
$retorno = $this->whatsAppUtil->sendMessageWithToken($telefone, $msg, $config->empresa_id, $config->token_whatsapp);
|
||||
$retorno = json_decode($retorno);
|
||||
if($retorno->success){
|
||||
$a->msg_wpp_alerta_horario = 1;
|
||||
$a->save();
|
||||
|
||||
MensagemAgendamentoLog::create([
|
||||
'empresa_id' => $config->empresa_id,
|
||||
'mensagem' => $msg,
|
||||
'cliente_id' => $a->cliente_id
|
||||
]);
|
||||
}else{
|
||||
// dd($retorno);
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function criaMensagemAgendamento($agendamento, $msg){
|
||||
if(strlen(trim($msg)) == 0) return "";
|
||||
$msg = str_replace("%nome%", $agendamento->cliente->razao_social, $msg);
|
||||
$msg = str_replace("%data%", __data_pt($agendamento->data, 0), $msg);
|
||||
$msg = str_replace("%hora%", substr($agendamento->inicio, 0, 5), $msg);
|
||||
|
||||
$textoServico = "SERVIÇOS\n";
|
||||
foreach($agendamento->itens as $i){
|
||||
if($i->servico){
|
||||
$textoServico .= number_format($i->quantidade, 0) . "X ";
|
||||
|
||||
$textoServico .= " " . $i->servico->nome;
|
||||
$textoServico .= " R$ " . __moeda($i->valor);
|
||||
$textoServico .= " = R$ " . __moeda($i->valor);
|
||||
$textoServico .= "\n";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$msg = str_replace("%serviços%", $textoServico, $msg);
|
||||
|
||||
return $msg;
|
||||
}
|
||||
|
||||
private function getAgendamentosHoje($empresa_id){
|
||||
return Agendamento::where('empresa_id', $empresa_id)
|
||||
->whereDate('data', date('Y-m-d'))->get();
|
||||
}
|
||||
|
||||
private function criaNotificacao($tabela, $referencia, $empresa, $titulo, $descricaoCurta, $objeto, $prioridade = 'baixa'){
|
||||
$item = Notificacao::where('empresa_id', $empresa->id)
|
||||
->where('tabela', $tabela)
|
||||
->where('referencia', $referencia)->first();
|
||||
|
||||
if($item == null){
|
||||
$descricao = $this->getDescricao($tabela, $objeto);
|
||||
Notificacao::create([
|
||||
'empresa_id' => $empresa->id,
|
||||
'tabela' => $tabela,
|
||||
'descricao' => $descricao,
|
||||
'descricao_curta' => $descricaoCurta,
|
||||
'referencia' => $referencia,
|
||||
'status' => 1,
|
||||
'por_sistema' => 1,
|
||||
'prioridade' => $prioridade,
|
||||
'visualizada' => 0,
|
||||
'titulo' => $titulo
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDescricao($tabela, $item){
|
||||
if($tabela == 'conta_recebers'){
|
||||
return view('notificacao.partials.conta_receber', compact('item'));
|
||||
}
|
||||
if($tabela == 'conta_pagars'){
|
||||
return view('notificacao.partials.conta_pagar', compact('item'));
|
||||
}
|
||||
if($tabela == 'compras'){
|
||||
return view('notificacao.partials.compras', compact('item'));
|
||||
}
|
||||
if($tabela == 'estoques'){
|
||||
return view('notificacao.partials.estoques', compact('item'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Utils\WhatsAppUtil;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\MensagemPadraoCrm;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\EmailConfig;
|
||||
use App\Utils\EmailUtil;
|
||||
use App\Models\FilaEnvioCron;
|
||||
|
||||
use Mail;
|
||||
|
||||
class AniversarioCron extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'aniversario:cron';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Envio de mensagens aniversario clientes';
|
||||
|
||||
protected $whatsAppUtil;
|
||||
protected $emailUtil;
|
||||
|
||||
public function __construct(WhatsAppUtil $whatsAppUtil, EmailUtil $emailUtil){
|
||||
parent::__construct();
|
||||
$this->whatsAppUtil = $whatsAppUtil;
|
||||
$this->emailUtil = $emailUtil;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$data = MensagemPadraoCrm::where('status', 1)
|
||||
->where('tipo', 'aniversario')->get();
|
||||
|
||||
$hoje = \Carbon\Carbon::now();
|
||||
foreach($data as $item){
|
||||
$clientes = Cliente::where('empresa_id', $item->empresa_id)
|
||||
->whereDay('data_nascimento', $hoje->day)
|
||||
->whereMonth('data_nascimento', $hoje->month)
|
||||
->get();
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', $item->empresa_id)->first();
|
||||
|
||||
foreach($clientes as $cliente){
|
||||
$texto = $this->__replaceText($item, $cliente);
|
||||
|
||||
try{
|
||||
if($item->enviar_email){
|
||||
$this->__enviarEmail($cliente, $texto);
|
||||
}
|
||||
|
||||
if($item->enviar_whatsapp){
|
||||
$this->__enviarWhatsApp($cliente, $texto, $config);
|
||||
}
|
||||
|
||||
FilaEnvioCron::create([
|
||||
'empresa_id' => $cliente->empresa_id,
|
||||
'mensagem' => $texto,
|
||||
'erro' => '',
|
||||
'status' => 'enviado',
|
||||
'enviar_whatsapp' => $item->enviar_whatsapp,
|
||||
'enviar_email' => $item->enviar_email,
|
||||
'whatsapp' => $cliente->telefone,
|
||||
'cliente_id' => $cliente->id,
|
||||
'tipo' => $item->tipo,
|
||||
'email' => $cliente->email,
|
||||
'agendar_para' => date('Y-m-d')
|
||||
]);
|
||||
}catch(\Exception $e){
|
||||
FilaEnvioCron::create([
|
||||
'empresa_id' => $cliente->empresa_id,
|
||||
'mensagem' => $texto,
|
||||
'erro' => $e->getMessage(),
|
||||
'status' => 'erro',
|
||||
'enviar_whatsapp' => $item->enviar_whatsapp,
|
||||
'enviar_email' => $item->enviar_email,
|
||||
'whatsapp' => $cliente->telefone,
|
||||
'cliente_id' => $cliente->id,
|
||||
'tipo' => $item->tipo,
|
||||
'email' => $cliente->email,
|
||||
'agendar_para' => date('Y-m-d')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function __replaceText($item, $cliente){
|
||||
$texto = str_replace("[nome_cliente]", $cliente->razao_social, $item->mensagem);
|
||||
return $texto;
|
||||
}
|
||||
|
||||
private function __enviarEmail($cliente, $texto){
|
||||
|
||||
if(!$cliente->email){
|
||||
return null;
|
||||
}
|
||||
|
||||
$emailConfig = EmailConfig::where('empresa_id', $cliente->empresa_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if($emailConfig != null){
|
||||
|
||||
$result = $this->emailUtil->enviaEmailPHPMailer($cliente->email, 'Feliz aniversário', $texto, $emailConfig);
|
||||
}else{
|
||||
Mail::send('mail.envio_cron', ['texto' => $texto], function($m) use ($cliente){
|
||||
|
||||
$nomeEmail = env('MAIL_FROM_NAME');
|
||||
$m->from(env('MAIL_USERNAME'), $nomeEmail);
|
||||
$m->subject('Feliz aniversário');
|
||||
$m->to($cliente->email);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function __enviarWhatsApp($cliente, $texto, $config){
|
||||
|
||||
if(!$cliente->telefone){
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = '';
|
||||
if($config && strlen($config->token_whatsapp) > 10){
|
||||
$token = $config->token_whatsapp;
|
||||
}
|
||||
|
||||
if($token != ''){
|
||||
$numero = preg_replace('/[^0-9]/', '', $cliente->telefone);
|
||||
$retorno = $this->whatsAppUtil->sendMessageWithToken('55'.$numero, $texto, $cliente->empresa_id, $token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\ConfiguracaoSuper;
|
||||
use App\Models\FinanceiroBoleto;
|
||||
use App\Models\LogBoleto;
|
||||
|
||||
class BoletoCron extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:boleto-cron';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Gerar boletos de planos com asaas';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$config = ConfiguracaoSuper::first();
|
||||
$empresas = Empresa::orderBy('nome')
|
||||
->where('receber_com_boleto', 1)
|
||||
->where('status', 1)
|
||||
->where('dia_vencimento_boleto', '!=', null)
|
||||
->get();
|
||||
|
||||
foreach($empresas as $empresa){
|
||||
if($empresa->plano){
|
||||
if($empresa->asaas_id == null){
|
||||
$dataCliente = [
|
||||
'name' => $empresa->nome,
|
||||
'cpfCnpj' => $empresa->cpf_cnpj,
|
||||
'email' => $empresa->email,
|
||||
'mobilePhone' => preg_replace('/[^0-9]/', '', $empresa->telefone),
|
||||
'address' => $empresa->rua,
|
||||
'addressNumber' => $empresa->numero,
|
||||
'province' => $empresa->bairro,
|
||||
'postalCode' => preg_replace('/[^0-9]/', '', $empresa->cep),
|
||||
];
|
||||
|
||||
$endPoint = 'https://api-sandbox.asaas.com/v3/customers';
|
||||
if($config->sandbox_boleto == 0){
|
||||
$endPoint = 'https://api.asaas.com/v3/customers';
|
||||
}
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
$response = $client->request('POST', $endPoint, [
|
||||
'body' => json_encode($dataCliente),
|
||||
'headers' => [
|
||||
'accept' => 'application/json',
|
||||
'access_token' => $config->asaas_token_boleto,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody(),true);
|
||||
|
||||
$empresa->asaas_id = $data['id'];
|
||||
$empresa->save();
|
||||
}
|
||||
$valor = $empresa->plano->valor;
|
||||
$vencimento = date('Y-m') . "-" . $empresa->dia_vencimento_boleto;
|
||||
|
||||
$financeiroBoleto = FinanceiroBoleto::where('empresa_id', $empresa->id)
|
||||
->whereMonth('vencimento', date('m'))->first();
|
||||
if($financeiroBoleto == null){
|
||||
$dataBoleto = [
|
||||
'customer' => $empresa->asaas_id,
|
||||
'billingType' => 'BOLETO',
|
||||
'value' => $valor,
|
||||
'dueDate' => $vencimento
|
||||
];
|
||||
|
||||
if($config->percentual_juros_padrao_boleto){
|
||||
$dataBoleto['interest'] = [
|
||||
'value' => $config->percentual_juros_padrao_boleto
|
||||
];
|
||||
}
|
||||
|
||||
if($config->percentual_multa_padrao_boleto){
|
||||
$dataBoleto['fine'] = [
|
||||
'value' => $config->percentual_multa_padrao_boleto
|
||||
];
|
||||
}
|
||||
|
||||
$endPoint = 'https://api-sandbox.asaas.com/v3/payments';
|
||||
if($config->sandbox_boleto == 0){
|
||||
$endPoint = 'https://api.asaas.com/v3/payments';
|
||||
}
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$response = $client->request('POST', $endPoint, [
|
||||
'body' => json_encode($dataBoleto),
|
||||
'headers' => [
|
||||
'accept' => 'application/json',
|
||||
'access_token' => $config->asaas_token_boleto,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody(),true);
|
||||
|
||||
if($data){
|
||||
$boleto = [
|
||||
'empresa_id' => $empresa->id,
|
||||
'valor' => $valor,
|
||||
'vencimento' => $vencimento,
|
||||
'pdf_boleto' => $data['bankSlipUrl'],
|
||||
'valor_recebido' => 0,
|
||||
'juros' => $config->percentual_juros_padrao_boleto,
|
||||
'multa' => $config->percentual_multa_padrao_boleto,
|
||||
'status' => 0,
|
||||
'plano_id' => $empresa->plano->plano_id,
|
||||
'_id' => $data['id']
|
||||
];
|
||||
FinanceiroBoleto::create($boleto);
|
||||
|
||||
LogBoleto::create([
|
||||
'tipo' => 'geracao',
|
||||
'empresa_id' => $empresa->id,
|
||||
'status' => 1,
|
||||
'descricao' => 'Cron mensal'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\CashBackCliente;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\CashBackConfig;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Utils\WhatsAppUtil;
|
||||
|
||||
class CashBackCron extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'cash-back:cron';
|
||||
protected $util;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Eventos do cashback';
|
||||
|
||||
public function __construct(WhatsAppUtil $util)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->util = $util;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// $current = "John Smith";
|
||||
// file_put_contents('aaaa.txt', $current);
|
||||
|
||||
$this->enviaMensagensDia();
|
||||
$this->enviaMensagens5Dias();
|
||||
}
|
||||
|
||||
private function enviaMensagensDia(){
|
||||
|
||||
$empresas = Empresa::all();
|
||||
$date = date('Y-m-d', strtotime('+1 days'));
|
||||
foreach($empresas as $e){
|
||||
|
||||
$data = CashBackCliente::whereDate('data_expiracao', $date)
|
||||
->where('empresa_id', $e->id)
|
||||
->get();
|
||||
$config = CashBackConfig::where('empresa_id', $e->id)->first();
|
||||
|
||||
$configGeral = ConfigGeral::where('empresa_id', $e->id)->first();
|
||||
$token = '';
|
||||
if($configGeral && strlen($configGeral->token_whatsapp) > 10){
|
||||
$token = $configGeral->token_whatsapp;
|
||||
}
|
||||
if($config){
|
||||
foreach($data as $cashback){
|
||||
if($cashback->status_mensagem_1_dia == 0 && $cashback->status == 1){
|
||||
$numero = preg_replace('/[^0-9]/', '', $cashback->cliente->telefone);
|
||||
$texto = $config->mensagem_automatica_1_dia;
|
||||
|
||||
$nomeCliente = $cashback->cliente->razao_social;
|
||||
if($cashback->cliente->nome_fantasia != ''){
|
||||
$nomeCliente = $cashback->cliente->nome_fantasia;
|
||||
}
|
||||
// echo $cashback->cliente;
|
||||
|
||||
$texto = str_replace("{credito}", moeda($cashback->valor_credito), $texto);
|
||||
$texto = str_replace("{expiracao}", __date($cashback->data_expiracao, 0), $texto);
|
||||
$texto = str_replace("{nome}", $nomeCliente, $texto);
|
||||
if($numero != ''){
|
||||
if($token == ''){
|
||||
$retorno = $this->util->sendMessage('55'.$numero, $texto, $e->id);
|
||||
}else{
|
||||
$retorno = $this->util->sendMessageWithToken('55'.$numero, $texto, $e->id, $token);
|
||||
}
|
||||
$retorno = json_decode($retorno);
|
||||
if($retorno->success == true){
|
||||
if($retorno){
|
||||
$cashback->status_mensagem_1_dia = 1;
|
||||
$cashback->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function enviaMensagens5Dias(){
|
||||
|
||||
$empresas = Empresa::all();
|
||||
$date = date('Y-m-d', strtotime('+5 days'));
|
||||
foreach($empresas as $e){
|
||||
|
||||
$data = CashBackCliente::whereDate('data_expiracao', $date)
|
||||
->where('empresa_id', $e->id)
|
||||
->get();
|
||||
$config = CashBackConfig::where('empresa_id', $e->id)->first();
|
||||
|
||||
$configGeral = ConfigGeral::where('empresa_id', $e->id)->first();
|
||||
$token = '';
|
||||
if($configGeral && strlen($configGeral->token_whatsapp) > 10){
|
||||
$token = $configGeral->token_whatsapp;
|
||||
}
|
||||
|
||||
if($config){
|
||||
foreach($data as $cashback){
|
||||
if($cashback->status_mensagem_5_dias == 0 && $cashback->status == 1){
|
||||
$numero = preg_replace('/[^0-9]/', '', $cashback->cliente->telefone);
|
||||
$texto = $config->mensagem_automatica_5_dias;
|
||||
|
||||
$nomeCliente = $cashback->cliente->razao_social;
|
||||
if($cashback->cliente->nome_fantasia != ''){
|
||||
$nomeCliente = $cashback->cliente->nome_fantasia;
|
||||
}
|
||||
// echo $cashback->cliente;
|
||||
|
||||
$texto = str_replace("{credito}", moeda($cashback->valor_credito), $texto);
|
||||
$texto = str_replace("{expiracao}", __date($cashback->data_expiracao, 0), $texto);
|
||||
$texto = str_replace("{nome}", $nomeCliente, $texto);
|
||||
if($numero != ''){
|
||||
// $retorno = $this->util->sendMessage('55'.$numero, $texto, $e->id);
|
||||
|
||||
if($token == ''){
|
||||
$retorno = $this->util->sendMessage('55'.$numero, $texto, $e->id);
|
||||
}else{
|
||||
$retorno = $this->util->sendMessageWithToken('55'.$numero, $texto, $e->id, $token);
|
||||
}
|
||||
|
||||
$retorno = json_decode($retorno);
|
||||
if($retorno->success == true){
|
||||
if($retorno){
|
||||
$cashback->status_mensagem_5_dias = 1;
|
||||
$cashback->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\FilaEnvioCron as FilaEnvio;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\EmailConfig;
|
||||
use App\Utils\WhatsAppUtil;
|
||||
use App\Utils\EmailUtil;
|
||||
use Mail;
|
||||
|
||||
class FilaEnvioCron extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fila-envio:cron';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fila de envio';
|
||||
|
||||
protected $whatsAppUtil;
|
||||
protected $emailUtil;
|
||||
|
||||
public function __construct(WhatsAppUtil $whatsAppUtil, EmailUtil $emailUtil){
|
||||
parent::__construct();
|
||||
$this->whatsAppUtil = $whatsAppUtil;
|
||||
$this->emailUtil = $emailUtil;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$data = FilaEnvio::where(function($q) {
|
||||
$q->where('status', 'pendente')->orWhere('status', 'erro');
|
||||
})
|
||||
->whereDate('agendar_para', date('Y-m-d'))
|
||||
->get();
|
||||
|
||||
foreach($data as $item){
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', $item->empresa_id)->first();
|
||||
|
||||
if($item->cliente){
|
||||
try{
|
||||
if($item->enviar_whatsapp){
|
||||
$this->__enviarWhatsApp($item->cliente, $item->texto, $config);
|
||||
}
|
||||
|
||||
if($item->enviar_email){
|
||||
$this->__enviarEmail($item->cliente, $item->texto);
|
||||
}
|
||||
$item->enviado_em = date('Y-m-d H:i:s');
|
||||
$item->status = 'enviado';
|
||||
$item->save();
|
||||
}catch(\Exception $e){
|
||||
$item->erro = $e->getMessage();
|
||||
$item->status = 'erro';
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function __enviarEmail($cliente, $texto){
|
||||
|
||||
if(!$cliente->email){
|
||||
return null;
|
||||
}
|
||||
|
||||
$emailConfig = EmailConfig::where('empresa_id', $cliente->empresa_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if($emailConfig != null){
|
||||
|
||||
$result = $this->emailUtil->enviaEmailPHPMailer($cliente->email, 'Como foi sua experiência', $texto, $emailConfig);
|
||||
}else{
|
||||
Mail::send('mail.envio_cron', ['texto' => $texto], function($m) use ($cliente){
|
||||
|
||||
$nomeEmail = env('MAIL_FROM_NAME');
|
||||
$m->from(env('MAIL_USERNAME'), $nomeEmail);
|
||||
$m->subject('Como foi sua experiência');
|
||||
$m->to($cliente->email);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function __enviarWhatsApp($cliente, $texto, $config){
|
||||
|
||||
if(!$cliente->telefone){
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = '';
|
||||
if($config && strlen($config->token_whatsapp) > 10){
|
||||
$token = $config->token_whatsapp;
|
||||
}
|
||||
|
||||
if($token != ''){
|
||||
$numero = preg_replace('/[^0-9]/', '', $cliente->telefone);
|
||||
$retorno = $this->whatsAppUtil->sendMessageWithToken('55'.$numero, $texto, $cliente->empresa_id, $token);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\EmailConfig;
|
||||
use App\Models\FilaEnvioCron;
|
||||
use App\Utils\WhatsAppUtil;
|
||||
use App\Utils\EmailUtil;
|
||||
use Mail;
|
||||
use App\Models\MensagemPadraoCrm;
|
||||
use App\Models\Nfe;
|
||||
use App\Models\Nfce;
|
||||
use App\Models\Cliente;
|
||||
|
||||
class ReativarCron extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'reativar:cron';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clientes que algum tempo não compram';
|
||||
|
||||
protected $whatsAppUtil;
|
||||
protected $emailUtil;
|
||||
|
||||
public function __construct(WhatsAppUtil $whatsAppUtil, EmailUtil $emailUtil){
|
||||
parent::__construct();
|
||||
$this->whatsAppUtil = $whatsAppUtil;
|
||||
$this->emailUtil = $emailUtil;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$data = MensagemPadraoCrm::where('status', 1)
|
||||
->where('tipo', 'reativacao')->get();
|
||||
|
||||
foreach($data as $item){
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', $item->empresa_id)->first();
|
||||
|
||||
$nves = Nfe::where('empresa_id', $item->empresa_id)
|
||||
->where('created_at', '<=', \Carbon\Carbon::now()->subDays($item->dias_ultima_venda))
|
||||
->pluck('cliente_id');
|
||||
|
||||
$nfces = Nfce::where('empresa_id', $item->empresa_id)
|
||||
->where('created_at', '<=', \Carbon\Carbon::now()->subDays($item->dias_ultima_venda))
|
||||
->pluck('cliente_id');
|
||||
|
||||
$clientesIds = $nves->merge($nfces)->filter()->unique()->values();
|
||||
|
||||
foreach($clientesIds as $cid){
|
||||
$cliente = Cliente::find($cid);
|
||||
if($cliente){
|
||||
$texto = $this->__replaceText($item, $cliente);
|
||||
|
||||
try{
|
||||
if($item->enviar_email){
|
||||
$this->__enviarEmail($cliente, $texto);
|
||||
}
|
||||
|
||||
if($item->enviar_whatsapp){
|
||||
$this->__enviarWhatsApp($cliente, $texto, $config);
|
||||
}
|
||||
|
||||
FilaEnvioCron::create([
|
||||
'empresa_id' => $cliente->empresa_id,
|
||||
'mensagem' => $texto,
|
||||
'erro' => '',
|
||||
'status' => 'enviado',
|
||||
'enviar_whatsapp' => $item->enviar_whatsapp,
|
||||
'enviar_email' => $item->enviar_email,
|
||||
'whatsapp' => $cliente->telefone,
|
||||
'cliente_id' => $cliente->id,
|
||||
'tipo' => $item->tipo,
|
||||
'email' => $cliente->email,
|
||||
'agendar_para' => date('Y-m-d')
|
||||
]);
|
||||
}catch(\Exception $e){
|
||||
FilaEnvioCron::create([
|
||||
'empresa_id' => $cliente->empresa_id,
|
||||
'mensagem' => $texto,
|
||||
'erro' => $e->getMessage(),
|
||||
'status' => 'erro',
|
||||
'enviar_whatsapp' => $item->enviar_whatsapp,
|
||||
'enviar_email' => $item->enviar_email,
|
||||
'whatsapp' => $cliente->telefone,
|
||||
'cliente_id' => $cliente->id,
|
||||
'tipo' => $item->tipo,
|
||||
'email' => $cliente->email,
|
||||
'agendar_para' => date('Y-m-d')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function __replaceText($item, $cliente){
|
||||
$texto = str_replace("[nome_cliente]", $cliente->razao_social, $item->mensagem);
|
||||
$texto = str_replace("[dias_inativo]", $item->dias_ultima_venda, $texto);
|
||||
return $texto;
|
||||
}
|
||||
|
||||
private function __enviarEmail($cliente, $texto){
|
||||
|
||||
if(!$cliente->email){
|
||||
return null;
|
||||
}
|
||||
|
||||
$emailConfig = EmailConfig::where('empresa_id', $cliente->empresa_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if($emailConfig != null){
|
||||
$result = $this->emailUtil->enviaEmailPHPMailer($cliente->email, 'Feliz aniversário', $texto, $emailConfig);
|
||||
}else{
|
||||
Mail::send('mail.envio_cron', ['texto' => $texto], function($m) use ($cliente){
|
||||
|
||||
$nomeEmail = env('MAIL_FROM_NAME');
|
||||
$m->from(env('MAIL_USERNAME'), $nomeEmail);
|
||||
$m->subject('Feliz aniversário');
|
||||
$m->to($cliente->email);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function __enviarWhatsApp($cliente, $texto, $config){
|
||||
|
||||
if(!$cliente->telefone){
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = '';
|
||||
if($config && strlen($config->token_whatsapp) > 10){
|
||||
$token = $config->token_whatsapp;
|
||||
}
|
||||
|
||||
if($token != ''){
|
||||
$numero = preg_replace('/[^0-9]/', '', $cliente->telefone);
|
||||
$retorno = $this->whatsAppUtil->sendMessageWithToken('55'.$numero, $texto, $cliente->empresa_id, $token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// $schedule->command('alerta:cron')->dailyAt(1,13);
|
||||
$schedule->command('alerta:cron')->everyMinute();
|
||||
$schedule->command('cash-back:cron')->dailyAt('08:00');
|
||||
$schedule->command('app:boleto-cron')->monthly();
|
||||
$schedule->command('aniversario:cron')->hourly();
|
||||
|
||||
$schedule->command('fila-envio:cron')->dailyAt('08:00');
|
||||
$schedule->command('reativar:cron')->dailyAt('09:00');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* The list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
|
||||
class ContaPagarExport implements FromView
|
||||
{
|
||||
protected $data;
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
public function view(): View
|
||||
{
|
||||
return view('exports.contas_pagar', [
|
||||
'data' => $this->data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
|
||||
class ContaReceberExport implements FromView
|
||||
{
|
||||
protected $data;
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
public function view(): View
|
||||
{
|
||||
return view('exports.contas_receber', [
|
||||
'data' => $this->data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
|
||||
class ProdutoExport implements FromView
|
||||
{
|
||||
protected $data;
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
public function view(): View
|
||||
{
|
||||
return view('exports.produtos', [
|
||||
'data' => $this->data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
|
||||
class RelatorioCrmExport implements FromView
|
||||
{
|
||||
protected $data;
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
public function view(): View
|
||||
{
|
||||
return view('exports.relatorio_crm', [
|
||||
'data' => $this->data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
|
||||
class RelatorioEstoqueExport implements FromView
|
||||
{
|
||||
protected $data;
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
public function view(): View
|
||||
{
|
||||
return view('exports.relatorio_estoque', [
|
||||
'data' => $this->data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,972 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Caixa;
|
||||
use App\Models\ContadorEmpresa;
|
||||
use App\Models\Localizacao;
|
||||
use App\Models\UsuarioLocalizacao;
|
||||
use App\Models\AcaoLog;
|
||||
use App\Models\ApiLog;
|
||||
use App\Models\ApiConfig;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\ConfiguracaoSuper;
|
||||
use App\Models\FinanceiroBoleto;
|
||||
use App\Models\ProdutoTributacaoLocal;
|
||||
use App\Models\MarketPlaceConfig;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
function __convert_value_bd($valor)
|
||||
{
|
||||
if (strlen($valor) >= 8) {
|
||||
$valor = str_replace(".", "", $valor);
|
||||
}
|
||||
$valor = str_replace(",", ".", $valor);
|
||||
|
||||
return (float)$valor;
|
||||
}
|
||||
|
||||
function __removerAcentos(string $texto): string
|
||||
{
|
||||
$texto = trim($texto);
|
||||
|
||||
$map = [
|
||||
'Á'=>'A','À'=>'A','Â'=>'A','Ã'=>'A','Ä'=>'A',
|
||||
'á'=>'a','à'=>'a','â'=>'a','ã'=>'a','ä'=>'a',
|
||||
'É'=>'E','È'=>'E','Ê'=>'E','Ë'=>'E',
|
||||
'é'=>'e','è'=>'e','ê'=>'e','ë'=>'e',
|
||||
'Í'=>'I','Ì'=>'I','Î'=>'I','Ï'=>'I',
|
||||
'í'=>'i','ì'=>'i','î'=>'i','ï'=>'i',
|
||||
'Ó'=>'O','Ò'=>'O','Ô'=>'O','Õ'=>'O','Ö'=>'O',
|
||||
'ó'=>'o','ò'=>'o','ô'=>'o','õ'=>'o','ö'=>'o',
|
||||
'Ú'=>'U','Ù'=>'U','Û'=>'U','Ü'=>'U',
|
||||
'ú'=>'u','ù'=>'u','û'=>'u','ü'=>'u',
|
||||
'Ç'=>'C','ç'=>'c'
|
||||
];
|
||||
|
||||
$texto = strtr($texto, $map);
|
||||
|
||||
return preg_replace('/[^A-Za-z0-9\s\-\.\,\/]/', '', $texto);
|
||||
}
|
||||
|
||||
function __usaICMSST($empresa, $cst, $cfop){
|
||||
if (!$empresa->substituto_tributario) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cstPermitidos = ['10','30','70','90'];
|
||||
|
||||
if (!in_array($cst, $cstPermitidos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cfopsST = ['5401','5402','5403','6401','6402','6403'];
|
||||
|
||||
if (!in_array($cfop, $cfopsST)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function __itensPagina(){
|
||||
|
||||
if(!Auth::user()->empresa){
|
||||
return env('PAGINACAO') ?? 30;
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null){
|
||||
return env('PAGINACAO') ?? 30;
|
||||
}
|
||||
return $config->itens_por_pagina ?? 30;
|
||||
}
|
||||
|
||||
function __validaObjetoEmpresa($objeto)
|
||||
{
|
||||
if(!Auth::user()->empresa){
|
||||
return true;
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
|
||||
if(isset($objeto->empresa_id)){
|
||||
if($objeto->empresa_id != $empresa_id){
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function valor_por_extenso($valor = 0, $maiusculas = false) {
|
||||
|
||||
$singular = array("centavo", "real", "mil", "milhão", "bilhão", "trilhão", "quatrilhão");
|
||||
$plural = array("centavos", "reais", "mil", "milhões", "bilhões", "trilhões",
|
||||
"quatrilhões");
|
||||
|
||||
$c = array("", "cem", "duzentos", "trezentos", "quatrocentos",
|
||||
"quinhentos", "seiscentos", "setecentos", "oitocentos", "novecentos");
|
||||
$d = array("", "dez", "vinte", "trinta", "quarenta", "cinquenta",
|
||||
"sessenta", "setenta", "oitenta", "noventa");
|
||||
$d10 = array("dez", "onze", "doze", "treze", "quatorze", "quinze",
|
||||
"dezesseis", "dezesete", "dezoito", "dezenove");
|
||||
$u = array("", "um", "dois", "três", "quatro", "cinco", "seis",
|
||||
"sete", "oito", "nove");
|
||||
|
||||
$z = 0;
|
||||
$rt = "";
|
||||
|
||||
$valor = number_format($valor, 2, ".", ".");
|
||||
$inteiro = explode(".", $valor);
|
||||
for($i=0;$i<count($inteiro);$i++)
|
||||
for($ii=strlen($inteiro[$i]);$ii<3;$ii++)
|
||||
$inteiro[$i] = "0".$inteiro[$i];
|
||||
|
||||
$fim = count($inteiro) - ($inteiro[count($inteiro)-1] > 0 ? 1 : 2);
|
||||
for ($i=0;$i<count($inteiro);$i++) {
|
||||
$valor = $inteiro[$i];
|
||||
$rc = (($valor > 100) && ($valor < 200)) ? "cento" : $c[$valor[0]];
|
||||
$rd = ($valor[1] < 2) ? "" : $d[$valor[1]];
|
||||
$ru = ($valor > 0) ? (($valor[1] == 1) ? $d10[$valor[2]] : $u[$valor[2]]) : "";
|
||||
|
||||
$r = $rc.(($rc && ($rd || $ru)) ? " e " : "").$rd.(($rd &&
|
||||
$ru) ? " e " : "").$ru;
|
||||
$t = count($inteiro)-1-$i;
|
||||
$r .= $r ? " ".($valor > 1 ? $plural[$t] : $singular[$t]) : "";
|
||||
if ($valor == "000")$z++; elseif ($z > 0) $z--;
|
||||
if (($t==1) && ($z>0) && ($inteiro[0] > 0)) $r .= (($z>1) ? " de " : "").$plural[$t];
|
||||
if ($r) $rt = $rt . ((($i > 0) && ($i <= $fim) &&
|
||||
($inteiro[0] > 0) && ($z < 1)) ? ( ($i < $fim) ? ", " : " e ") : " ") . $r;
|
||||
}
|
||||
|
||||
if(!$maiusculas){
|
||||
return($rt ? $rt : "zero");
|
||||
} else {
|
||||
|
||||
if ($rt) $rt=ereg_replace(" E "," e ",ucwords($rt));
|
||||
return (($rt) ? ($rt) : "Zero");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function __validaObjetoEmpresaContador($contador_empresa_id, $empresa_id)
|
||||
{
|
||||
|
||||
$item = ContadorEmpresa::where('empresa_id', $empresa_id)
|
||||
->where('contador_id', $contador_empresa_id)->first();
|
||||
if($item == null){
|
||||
abort(403, 'Você não possui acesso a esse registro!');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function __tipoMenu()
|
||||
{
|
||||
if(!Auth::user()->empresa){
|
||||
return env('MENU_PADRAO');
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null){
|
||||
return env('MENU_PADRAO');
|
||||
}
|
||||
return $config->tipo_menu;
|
||||
}
|
||||
|
||||
function __tipoSmallHeader()
|
||||
{
|
||||
if(!Auth::user()->empresa){
|
||||
return '../images/small/small-4.jpg';
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null){
|
||||
return '../images/small/small-4.jpg';
|
||||
}
|
||||
return '../images/small/'.$config->small_header_user;
|
||||
|
||||
}
|
||||
|
||||
function __infoTopoMenu(){
|
||||
$config = ConfiguracaoSuper::first();
|
||||
if($config == null || $config->info_topo_menu == 1) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __casas_decimais_quantidade()
|
||||
{
|
||||
try{
|
||||
if(!Auth::user()->empresa){
|
||||
return 2;
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null){
|
||||
return 2;
|
||||
}
|
||||
return $config->casas_decimais_quantidade;
|
||||
}catch(\Exception $e){
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
function __dataTopBar()
|
||||
{
|
||||
if(!Auth::user()->empresa){
|
||||
return 'light';
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null){
|
||||
return 'light';
|
||||
}
|
||||
return $config->cor_top_bar;
|
||||
}
|
||||
|
||||
function __usuarioEscolherPlano(){
|
||||
$config = ConfiguracaoSuper::first();
|
||||
if($config == null || $config->usuario_alterar_plano == 1) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __finalizacaoPdv(){
|
||||
if(Auth::user()->finalizacao_pdv == null) return 'todos';
|
||||
return Auth::user()->finalizacao_pdv;
|
||||
}
|
||||
|
||||
function __dataMenuBar()
|
||||
{
|
||||
if(!Auth::user()->empresa){
|
||||
return 'light';
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null){
|
||||
return 'light';
|
||||
}
|
||||
return $config->cor_menu;
|
||||
}
|
||||
|
||||
function __dataThemeDefault()
|
||||
{
|
||||
|
||||
if(Auth::user() && Auth::user()->tema_padrao){
|
||||
return Auth::user()->tema_padrao;
|
||||
}
|
||||
$config = ConfiguracaoSuper::first();
|
||||
if($config == null){
|
||||
return 'light';
|
||||
}
|
||||
return $config->tema_padrao;
|
||||
}
|
||||
|
||||
function __moeda($valor, $casas_decimais = 2)
|
||||
{
|
||||
return number_format($valor, $casas_decimais, ',', '.');
|
||||
}
|
||||
|
||||
function __qtd($valor)
|
||||
{
|
||||
return number_format($valor, __casas_decimais_quantidade(), ',', '.');
|
||||
}
|
||||
|
||||
function __calcPercentual($v1, $v2){
|
||||
if($v1 > $v2){
|
||||
return number_format(100+(($v2-$v1)/$v1*100), 1);
|
||||
}else{
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
function __moedaInput($valor, $casas_decimais = 2)
|
||||
{
|
||||
return number_format($valor, $casas_decimais, ',', '');
|
||||
}
|
||||
|
||||
function __data_pt($data, $hora = true)
|
||||
{
|
||||
if ($hora) {
|
||||
return \Carbon\Carbon::parse($data)->format('d/m/Y H:i');
|
||||
} else {
|
||||
return \Carbon\Carbon::parse($data)->format('d/m/Y');
|
||||
}
|
||||
}
|
||||
|
||||
function __hora_pt($data)
|
||||
{
|
||||
return \Carbon\Carbon::parse($data)->format('H:i');
|
||||
}
|
||||
|
||||
function __isMaster()
|
||||
{
|
||||
if (Auth::user()->email == env("MAILMASTER")) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isSuporte()
|
||||
{
|
||||
return Auth::user()->suporte;
|
||||
}
|
||||
|
||||
function __isEmpresaMaster($empresa)
|
||||
{
|
||||
foreach($empresa->usuarios as $u){
|
||||
if($u->usuario->email == env("MAILMASTER")){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isContador()
|
||||
{
|
||||
if (Auth::user()->tipo_contador == 1) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isContadorPlano()
|
||||
{
|
||||
if (Auth::user()->empresa->empresa->cadastrar_planos == 1) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __escolheLocalidade()
|
||||
{
|
||||
return Auth::user()->escolher_localidade_venda;
|
||||
}
|
||||
|
||||
function __empresasDoContador()
|
||||
{
|
||||
$contador_id = Auth::user()->empresa->empresa_id;
|
||||
return ContadorEmpresa::where('contador_id', $contador_id)->get();
|
||||
}
|
||||
|
||||
function __faturaBoleto()
|
||||
{
|
||||
if(!Auth::user()->empresa) return null;
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
if(Auth::user()->empresa->empresa->receber_com_boleto == 0) return null;
|
||||
|
||||
$fatura = FinanceiroBoleto::where('empresa_id', $empresa_id)
|
||||
->whereMonth('vencimento', date('m'))
|
||||
->where('status', 0)
|
||||
->first();
|
||||
if($fatura != null){
|
||||
$config = ConfiguracaoSuper::first();
|
||||
|
||||
$dif = strtotime($fatura->vencimento) - strtotime(date("Y-m-d"));
|
||||
$dif = (int)floor($dif / (60 * 60 * 24));
|
||||
|
||||
if($dif <= $config->dias_alerta_boleto){
|
||||
// dd($fatura);
|
||||
return $fatura;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function __isAdmin()
|
||||
{
|
||||
return Auth::user()->admin;
|
||||
}
|
||||
|
||||
function __getError($e)
|
||||
{
|
||||
return "Linha: " . $e->getLine() . ", mensagem: " . $e->getMessage() . ", arquivo: " . $e->getFile();
|
||||
}
|
||||
|
||||
function __isCaixaAberto()
|
||||
{
|
||||
$usuario_id = Auth::user()->id;
|
||||
return Caixa::where('usuario_id', $usuario_id)->where('status', 1)->first();
|
||||
}
|
||||
|
||||
function get_id_user()
|
||||
{
|
||||
$usr = Auth::user()->id;
|
||||
return $usr;
|
||||
}
|
||||
|
||||
function get_name_user()
|
||||
{
|
||||
$usr = Auth::user()->name;
|
||||
return $usr;
|
||||
}
|
||||
|
||||
function __mask($val, $mask){
|
||||
$maskared = '';
|
||||
$k = 0;
|
||||
for ($i = 0; $i <= strlen($mask) - 1; ++$i) {
|
||||
if ($mask[$i] == '#') {
|
||||
if (isset($val[$k])) {
|
||||
$maskared .= $val[$k++];
|
||||
}
|
||||
} else {
|
||||
if (isset($mask[$i])) {
|
||||
$maskared .= $mask[$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $maskared;
|
||||
}
|
||||
|
||||
function __setMask($doc){
|
||||
$doc = preg_replace('/[^0-9]/', '', $doc);
|
||||
$mask = '##.###.###/####-##';
|
||||
if (strlen($doc) == 11) {
|
||||
$mask = '###.###.###-##';
|
||||
}
|
||||
return __mask($doc, $mask);
|
||||
}
|
||||
|
||||
function __isPlanoFiscal(){
|
||||
$empresa = auth::user()->empresa;
|
||||
if(!$empresa) return false;
|
||||
|
||||
$plano = $empresa->empresa->plano;
|
||||
if($plano){
|
||||
if($plano->plano->fiscal) return 1;
|
||||
}
|
||||
|
||||
if(__isContador()){
|
||||
return 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function __periodoExpirar(){
|
||||
$empresa = auth::user()->empresa;
|
||||
if(!$empresa) return false;
|
||||
|
||||
$plano = $empresa->empresa->plano;
|
||||
if(!$plano) return false;
|
||||
|
||||
// $plano->data_expiracao = "2025-04-16";
|
||||
$dif = strtotime($plano->data_expiracao) - strtotime(date("Y-m-d"));
|
||||
$dif = floor($dif / (60 * 60 * 24));
|
||||
|
||||
if($dif <= 5) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isActivePlan($empresa, $menu){
|
||||
if(!$empresa) return false;
|
||||
$plano = $empresa->empresa->plano;
|
||||
if($plano){
|
||||
$modulos = json_decode($plano->plano->modulos) ?? [];
|
||||
if(in_array($menu, $modulos)) return true;
|
||||
else return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function __isInternacionalizar($empresa){
|
||||
if(!$empresa) return false;
|
||||
$config = $empresa->empresa->configuracaoCardapio;
|
||||
if(!$config) return false;
|
||||
if($config->intercionalizar == 1) return 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
function __isNotificacao($empresa){
|
||||
if(!$empresa) return false;
|
||||
$config = $empresa->empresa->configuracaoCardapio;
|
||||
if(!$config) return false;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function __isNotificacaoMarketPlace($empresa){
|
||||
if(!$empresa) return false;
|
||||
$config = $empresa->empresa->configuracaoMarketPlace;
|
||||
if(!$config) return false;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function __isNotificacaoEcommerce($empresa){
|
||||
if(!$empresa) return false;
|
||||
$config = $empresa->empresa->configuracaoEcommerce;
|
||||
if(!$config) return false;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function __countLocalAtivo(){
|
||||
try{
|
||||
if(!Auth::user()->empresa){
|
||||
return 0;
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
return Localizacao::where('empresa_id', $empresa_id)
|
||||
->where('status', 1)->count();
|
||||
}catch(\Exception $e){
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function __getLocaisAtivos(){
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
return Localizacao::where('empresa_id', $empresa_id)
|
||||
->where('status', 1)->get();
|
||||
}
|
||||
|
||||
function __getLocalAtivo(){
|
||||
if(!Auth::user()->empresa){
|
||||
return 0;
|
||||
}
|
||||
$empresa_id = Auth::user()->empresa->empresa_id;
|
||||
return Localizacao::where('empresa_id', $empresa_id)
|
||||
->where('status', 1)->first();
|
||||
}
|
||||
|
||||
function __getLocaisAtivoUsuario(){
|
||||
$usuario_id = Auth::user()->id;
|
||||
return Localizacao::where('usuario_localizacaos.usuario_id', $usuario_id)
|
||||
->select('localizacaos.*')
|
||||
->join('usuario_localizacaos', 'usuario_localizacaos.localizacao_id', '=', 'localizacaos.id')
|
||||
->where('localizacaos.status', 1)->get();
|
||||
}
|
||||
|
||||
function __objetoParaEmissao($empresa, $local_id){
|
||||
|
||||
$primeiraLocalizacao = Localizacao::where('empresa_id', $empresa->id)
|
||||
->where('status', 1)->first();
|
||||
|
||||
$count = Localizacao::where('empresa_id', $empresa->id)
|
||||
->where('status', 1)->count();
|
||||
if($count <= 1) return $empresa;
|
||||
|
||||
$localizacao = Localizacao::findOrFail($local_id);
|
||||
if($primeiraLocalizacao == $localizacao) return $empresa;
|
||||
return $localizacao;
|
||||
}
|
||||
|
||||
function __createLog($empresa_id, $local, $acao, $descricao){
|
||||
AcaoLog::create([
|
||||
'empresa_id' => $empresa_id,
|
||||
'local' => $local,
|
||||
'acao' => $acao,
|
||||
'descricao' => substr($descricao, 0, 255),
|
||||
]);
|
||||
}
|
||||
|
||||
function __createApiLog($empresa_id, $token, $status, $descricao, $tipo, $prefixo){
|
||||
ApiLog::create([
|
||||
'empresa_id' => $empresa_id,
|
||||
'token' => $token,
|
||||
'status' => $status,
|
||||
'descricao' => substr($descricao, 0, 255),
|
||||
'tipo' => $tipo,
|
||||
'prefixo' => $prefixo
|
||||
]);
|
||||
}
|
||||
|
||||
function __validaPermissaoToken($token, $permissao){
|
||||
$item = ApiConfig::where('token', $token)->first();
|
||||
if($item){
|
||||
$permissoes_acesso = $item->permissoes_acesso != 'null' ? json_decode($item->permissoes_acesso) : [];
|
||||
|
||||
if(in_array($permissao, $permissoes_acesso)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isSegmentoPlanoOtica(){
|
||||
$empresa = auth::user()->empresa;
|
||||
if(!$empresa) return false;
|
||||
|
||||
$plano = $empresa->empresa->plano;
|
||||
if($plano){
|
||||
if($plano->plano->segmento && $plano->plano->segmento->nome == 'Ótica') return 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function __isSegmentoServico($empresa_id){
|
||||
$config = MarketPlaceConfig::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null) return 0;
|
||||
$segmento = json_decode($config->segmento);
|
||||
if(in_array('servicos', $segmento)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isSegmentoProduto($empresa_id){
|
||||
$config = MarketPlaceConfig::where('empresa_id', $empresa_id)->first();
|
||||
if($config == null) return 0;
|
||||
$segmento = json_decode($config->segmento);
|
||||
if(in_array('produtos', $segmento)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __isProdutoServicoDelivery($empresa_id){
|
||||
if(__isSegmentoProduto($empresa_id) && __isSegmentoServico($empresa_id)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function __tributacaoProdutoLocal($item, $campo, $local_id){
|
||||
$itemLocal = ProdutoTributacaoLocal::where('produto_id', $item->id)
|
||||
->where('local_id', $local_id)->first();
|
||||
|
||||
if($itemLocal != null){
|
||||
return $itemLocal[$campo];
|
||||
}
|
||||
return $item[$campo];
|
||||
}
|
||||
|
||||
function __tributacaoProdutoLocalNcm($item, $local_id){
|
||||
$itemLocal = ProdutoTributacaoLocal::where('produto_id', $item->id)
|
||||
->where('local_id', $local_id)->first();
|
||||
if($itemLocal != null){
|
||||
return $itemLocal->_ncm ? [$itemLocal->ncm => $itemLocal->_ncm->descricao] : [];
|
||||
}
|
||||
return $item->_ncm ? [$item->ncm => $item->_ncm->descricao] : [];
|
||||
}
|
||||
|
||||
function __primeiroLocal($local_id, $empresa_id){
|
||||
$local = Localizacao::where('empresa_id', $empresa_id)
|
||||
->where('status', 1)->first();
|
||||
return $local_id == $local->id;
|
||||
}
|
||||
|
||||
function __tributacaoProdutoLocalVenda($produto, $local_id){
|
||||
|
||||
$itemLocal = ProdutoTributacaoLocal::where('produto_id', $produto->id)
|
||||
->where('local_id', $local_id)->first();
|
||||
|
||||
if($itemLocal == null || __primeiroLocal($local_id, $produto->empresa_id)){
|
||||
return $produto;
|
||||
}
|
||||
|
||||
$produto->ncm = $itemLocal->ncm;
|
||||
$produto->perc_icms = $itemLocal->perc_icms;
|
||||
$produto->perc_pis = $itemLocal->perc_pis;
|
||||
$produto->perc_cofins = $itemLocal->perc_cofins;
|
||||
$produto->perc_ipi = $itemLocal->perc_ipi;
|
||||
|
||||
$produto->cest = $itemLocal->cest;
|
||||
$produto->origem = $itemLocal->origem;
|
||||
$produto->cst_csosn = $itemLocal->cst_csosn;
|
||||
$produto->cst_pis = $itemLocal->cst_pis;
|
||||
$produto->cst_cofins = $itemLocal->cst_cofins;
|
||||
|
||||
$produto->cst_ipi = $itemLocal->cst_ipi;
|
||||
$produto->valor_unitario = $itemLocal->valor_unitario;
|
||||
$produto->cfop_estadual = $itemLocal->cfop_estadual;
|
||||
$produto->cfop_outro_estado = $itemLocal->cfop_outro_estado;
|
||||
|
||||
return $produto;
|
||||
}
|
||||
|
||||
function __valorProdutoLocal($produto, $local_id){
|
||||
$itemLocal = ProdutoTributacaoLocal::where('produto_id', $produto->id)
|
||||
->where('local_id', $local_id)->first();
|
||||
|
||||
if($itemLocal == null || __primeiroLocal($local_id, $produto->empresa_id)){
|
||||
return $produto->valor_unitario;
|
||||
}
|
||||
|
||||
return $itemLocal->valor_unitario;
|
||||
}
|
||||
|
||||
function __getPais($codigo){
|
||||
if(isset(__getPaises()[$codigo])){
|
||||
return __getPaises()[$codigo];
|
||||
}
|
||||
return 'Brasil';
|
||||
}
|
||||
|
||||
function __getPaises(){
|
||||
return [
|
||||
'0132' => "Afeganistão",
|
||||
'7560' => "África do Sul",
|
||||
'0175' => "Albânia, República da",
|
||||
'0230' => "Alemanha",
|
||||
'0370' => "Andorra Sim",
|
||||
'0400' => "Angola",
|
||||
'0418' => "Anguilla Sim",
|
||||
'0434' => "Antigua e Barbuda Sim",
|
||||
'0477' => "Antilhas Holandesas Sim",
|
||||
'0531' => "Arábia Saudita",
|
||||
'0590' => "Argélia",
|
||||
'0639' => "Argentina",
|
||||
'0647' => "Armênia, República da",
|
||||
'0655' => "Aruba",
|
||||
'0698' => "Austrália",
|
||||
'0728' => "Áustria",
|
||||
'0736' => "Azerbaijão, República do",
|
||||
'0779' => "Bahamas, Ilhas Sim",
|
||||
'0809' => "Bahrein, Ilhas Sim",
|
||||
'0817' => "Bangladesh",
|
||||
'0833' => "Barbados Sim",
|
||||
'0850' => "Belarus",
|
||||
'0876' => "Bélgica",
|
||||
'0884' => "Belize Sim",
|
||||
'2291' => "Benin",
|
||||
'0906' => "Bermudas Sim",
|
||||
'0973' => "Bolívia",
|
||||
'0981' => "Bósnia-Herzegovina",
|
||||
'1015' => "Botsuana",
|
||||
'1058' => "Brasil",
|
||||
'1082' => "Brunei",
|
||||
'1112' => "Bulgária, República da",
|
||||
'0310' => "Burkina Faso",
|
||||
'1155' => "Burundi",
|
||||
'1198' => "Butão",
|
||||
'1279' => "Cabo Verde, República de",
|
||||
'1457' => "Camarões",
|
||||
'1414' => "Camboja",
|
||||
'1490' => "Canadá",
|
||||
'1504' => "Canal, Ilhas do (Jersey e Guernsey) Sim",
|
||||
'1511' => "Canárias, Ilhas",
|
||||
'1546' => "Catar",
|
||||
'1376' => "Cayman, Ilhas Sim",
|
||||
'1538' => "Cazaquistão, República do",
|
||||
'7889' => "Chade",
|
||||
'1589' => "Chile",
|
||||
'1600' => "China, República Popular da",
|
||||
'1635' => "Chipre Sim",
|
||||
'5118' => "Christmas, Ilha (Navidad)",
|
||||
'7412' => "Cingapura",
|
||||
'1651' => "Cocos (Keeling), Ilhas",
|
||||
'1694' => "Colômbia",
|
||||
'1732' => "Comores, Ilhas",
|
||||
'8885' => "Congo, República Democrática do",
|
||||
'1775' => "Congo, República do",
|
||||
'1830' => "Cook, Ilhas Sim",
|
||||
'1872' => "Coréia, Rep. Pop. Democrática da",
|
||||
'1902' => "Coréia, República da",
|
||||
'1937' => "Costa do Marfim",
|
||||
'1961' => "Costa Rica Sim",
|
||||
'1988' => "Coveite",
|
||||
'1953' => "Croácia, República da",
|
||||
'1996' => "Cuba",
|
||||
'2321' => "Dinamarca",
|
||||
'7838' => "Djibuti Sim",
|
||||
'2356' => "Dominica, Ilha Sim",
|
||||
'2402' => "Egito",
|
||||
'6874' => "El Salvador",
|
||||
'2445' => "Emirados Árabes Unidos",
|
||||
'2399' => "Equador",
|
||||
'2437' => "Eritréia",
|
||||
'6289' => "Escócia",
|
||||
'2470' => "Eslovaca, República",
|
||||
'2461' => "Eslovênia, República da",
|
||||
'2453' => "Espanha",
|
||||
'2496' => "Estados Unidos",
|
||||
'2518' => "Estônia, República da",
|
||||
'2534' => "Etiópia",
|
||||
'2550' => "Falkland (Ilhas Malvinas)",
|
||||
'2593' => "Feroe, Ilhas",
|
||||
'8702' => "Fiji",
|
||||
'2674' => "Filipinas",
|
||||
'2712' => "Finlândia",
|
||||
'1619' => "Formosa (Taiwan)",
|
||||
'2755' => "França",
|
||||
'2810' => "Gabão",
|
||||
'6289' => "Gales, País de",
|
||||
'2852' => "Gâmbia",
|
||||
'2895' => "Gana",
|
||||
'2917' => "Geórgia, República da",
|
||||
'2933' => "Gibraltar Sim",
|
||||
'6289' => "Grã-Bretanha",
|
||||
'2976' => "Granada Sim",
|
||||
'3018' => "Grécia",
|
||||
'3050' => "Groenlândia",
|
||||
'3093' => "Guadalupe",
|
||||
'3131' => "Guam",
|
||||
'3174' => "Guatemala",
|
||||
'3379' => "Guiana",
|
||||
'3255' => "Guiana Francesa",
|
||||
'3298' => "Guiné",
|
||||
'3344' => "Guiné-Bissau",
|
||||
'3310' => "Guiné-Equatorial",
|
||||
'3417' => "Haiti",
|
||||
'5738' => "Holanda (Países Baixos)",
|
||||
'3450' => "Honduras",
|
||||
'3514' => "Hong Kong, Região Adm. Especial",
|
||||
'3557' => "Hungria, República da",
|
||||
'3573' => "Iêmen",
|
||||
'3611' => "Índia",
|
||||
'3654' => "Indonésia",
|
||||
'6289' => "Inglaterra",
|
||||
'3727' => "Irã, República Islâmica do",
|
||||
'3697' => "Iraque",
|
||||
'3751' => "Irlanda",
|
||||
'6289' => "Irlanda do Norte",
|
||||
'3794' => "Islândia",
|
||||
'3832' => "Israel",
|
||||
'3867' => "Itália",
|
||||
'3883' => "Iugoslávia, República Fed. da",
|
||||
'3913' => "Jamaica",
|
||||
'3999' => "Japão",
|
||||
'3964' => "Johnston, Ilhas",
|
||||
'4030' => "Jordânia",
|
||||
'4111' => "Kiribati",
|
||||
'4200' => "Laos, Rep. Pop. Democrática do",
|
||||
'4235' => "Lebuan Sim",
|
||||
'4260' => "Lesoto",
|
||||
'4278' => "Letônia, República da",
|
||||
'4316' => "Líbano",
|
||||
'4340' => "Libéria Sim",
|
||||
'4383' => "Líbia",
|
||||
'4405' => "Liechtenstein Sim",
|
||||
'4421' => "Lituânia, República da",
|
||||
'4456' => "Luxemburgo",
|
||||
'4472' => "Macau",
|
||||
'4499' => "Macedônia",
|
||||
'4502' => "Madagascar",
|
||||
'4525' => "Madeira, Ilha da Sim",
|
||||
'4553' => "Malásia",
|
||||
'4588' => "Malavi",
|
||||
'4618' => "Maldivas",
|
||||
'4642' => "Máli",
|
||||
'4677' => "Malta Sim",
|
||||
'3595' => "Man, Ilhas Sim",
|
||||
'4723' => "Marianas do Norte",
|
||||
'4740' => "Marrocos",
|
||||
'4766' => "Marshall, Ilhas Sim",
|
||||
'4774' => "Martinica",
|
||||
'4855' => "Maurício Sim",
|
||||
'4880' => "Mauritânia",
|
||||
'4936' => "México",
|
||||
'0930' => "Mianmar (Birmânia)",
|
||||
'4995' => "Micronésia",
|
||||
'4901' => "Midway, Ilhas",
|
||||
'5053' => "Moçambique",
|
||||
'4944' => "Moldávia, República da",
|
||||
'4952' => "Mônaco Sim",
|
||||
'4979' => "Mongólia",
|
||||
'5010' => "Montserrat, Ilhas Sim",
|
||||
'5070' => "Namíbia",
|
||||
'5088' => "Nauru Sim",
|
||||
'5177' => "Nepal",
|
||||
'5215' => "Nicarágua",
|
||||
'5258' => "Niger",
|
||||
'5282' => "Nigéria",
|
||||
'5312' => "Niue, Ilha Sim",
|
||||
'5355' => "Norfolk, Ilha",
|
||||
'5380' => "Noruega",
|
||||
'5428' => "Nova Caledônia",
|
||||
'5487' => "Nova Zelândia",
|
||||
'5568' => "Omã",
|
||||
'5738' => "Países Baixos (Holanda)",
|
||||
'5754' => "Palau",
|
||||
'5800' => "Panamá Sim",
|
||||
'5452' => "Papua Nova Guiné",
|
||||
'5762' => "Paquistão",
|
||||
'5860' => "Paraguai",
|
||||
'5894' => "Peru",
|
||||
'5932' => "Pitcairn, Ilha",
|
||||
'5991' => "Polinésia Francesa",
|
||||
'6033' => "Polônia, República da",
|
||||
'6114' => "Porto Rico",
|
||||
'6076' => "Portugal",
|
||||
'6238' => "Quênia",
|
||||
'6254' => "Quirguiz, República",
|
||||
'6289' => "Reino Unido",
|
||||
'6408' => "República Centro-Africana",
|
||||
'6475' => "República Dominicana",
|
||||
'6602' => "Reunião, Ilha",
|
||||
'6700' => "Romênia",
|
||||
'6750' => "Ruanda",
|
||||
'6769' => "Rússia",
|
||||
'6858' => "Saara Ocidental",
|
||||
'6777' => "Salomão, Ilhas",
|
||||
'6904' => "Samoa Sim",
|
||||
'6912' => "Samoa Americana",
|
||||
'6971' => "San Marino Sim",
|
||||
'7102' => "Santa Helena",
|
||||
'7153' => "Santa Lúcia Sim",
|
||||
'6955' => "São Cristóvão e Neves Sim",
|
||||
'7005' => "São Pedro e Miquelon",
|
||||
'7200' => "São Tomé e Príncipe, Ilhas",
|
||||
'7056' => "São Vicente e Granadinas Sim",
|
||||
'7285' => "Senegal",
|
||||
'7358' => "Serra Leoa",
|
||||
'7315' => "Seychelles Sim",
|
||||
'7447' => "Síria, República Árabe da",
|
||||
'7480' => "Somália",
|
||||
'7501' => "Sri Lanka",
|
||||
'7544' => "Suazilândia",
|
||||
'7595' => "Sudão",
|
||||
'7641' => "Suécia",
|
||||
'7676' => "Suíça",
|
||||
'7706' => "Suriname",
|
||||
'7722' => "Tadjiquistão",
|
||||
'7765' => "Tailândia",
|
||||
'7803' => "Tanzânia, República Unida da",
|
||||
'7919' => "Tcheca, República",
|
||||
'7820' => "Território Britânico Oc. Índico ",
|
||||
'7951' => "Timor Leste",
|
||||
'8001' => "Togo",
|
||||
'8109' => "Tonga Sim",
|
||||
'8052' => "Toquelau, Ilhas",
|
||||
'8150' => "Trinidad e Tobago",
|
||||
'8206' => "Tunísia",
|
||||
'8230' => "Turcas e Caicos, Ilhas Sim",
|
||||
'8249' => "Turcomenistão, República do",
|
||||
'8273' => "Turquia",
|
||||
'8281' => "Tuvalu",
|
||||
'8311' => "Ucrânia",
|
||||
'8338' => "Uganda",
|
||||
'8451' => "Uruguai",
|
||||
'8478' => "Uzbequistão, República do",
|
||||
'5517' => "Vanuatu Sim",
|
||||
'8486' => "Vaticano, Estado da Cidade do",
|
||||
'8508' => "Venezuela",
|
||||
'8583' => "Vietnã",
|
||||
'8630' => "Virgens, Ilhas (Britânicas) Sim",
|
||||
'8664' => "Virgens, Ilhas (E.U.A.) Sim",
|
||||
'8737' => "Wake, Ilha",
|
||||
'8753' => "Wallis e Futuna, Ilhas",
|
||||
'8907' => "Zâmbia",
|
||||
'6653' => "Zimbábue",
|
||||
'8958' => "Zona do Canal do Panamá"
|
||||
];
|
||||
}
|
||||
|
||||
function __getUltimoNumeroSequencial($empresa_id, $tabela){
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
$ultimo = DB::table($tabela)->where('empresa_id', $empresa_id)
|
||||
->orderBy('numero_sequencial', 'desc')
|
||||
->where('numero_sequencial', '>', 0)->first();
|
||||
|
||||
$numero = $ultimo != null ? $ultimo->numero_sequencial : 0;
|
||||
if($config == null){
|
||||
return $numero;
|
||||
}
|
||||
if($tabela == 'produtos'){
|
||||
return $config->ultimo_codigo_produto > $numero ? $config->ultimo_codigo_produto : $numero;
|
||||
}
|
||||
|
||||
if($tabela == 'clientes'){
|
||||
return $config->ultimo_codigo_cliente > $numero ? $config->ultimo_codigo_cliente : $numero;
|
||||
}
|
||||
|
||||
if($tabela == 'fornecedors'){
|
||||
return $config->ultimo_codigo_fornecedor > $numero ? $config->ultimo_codigo_fornecedor : $numero;
|
||||
}
|
||||
}
|
||||
|
||||
function __setUltimoNumeroSequencial($empresa_id, $tabela, $numero){
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
|
||||
if($config != null){
|
||||
if($tabela == 'produtos'){
|
||||
$config->ultimo_codigo_produto = (int)$numero;
|
||||
}
|
||||
|
||||
if($tabela == 'clientes'){
|
||||
$config->ultimo_codigo_cliente = (int)$numero;
|
||||
}
|
||||
|
||||
if($tabela == 'fornecedors'){
|
||||
$config->ultimo_codigo_fornecedor = (int)$numero;
|
||||
}
|
||||
|
||||
$config->save();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DiaSemana;
|
||||
use App\Models\Funcionamento;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\Interrupcoes;
|
||||
use App\Models\Agendamento;
|
||||
use App\Models\Servico;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AgendamentoController extends Controller
|
||||
{
|
||||
public function buscarHorarios(Request $request)
|
||||
{
|
||||
$servicos = json_decode($request->servicos);
|
||||
$data = $request->data;
|
||||
$start_time = $request->start_time;
|
||||
$empresa_id = $request->empresa_id;
|
||||
$funcionario_id = $request->funcionario_id;
|
||||
|
||||
$diaSemanaNumerico = date('w', strtotime($data));
|
||||
|
||||
$diaStr = DiaSemana::getDia($diaSemanaNumerico);
|
||||
$totalServico = 0;
|
||||
$tempoServico = 0;
|
||||
|
||||
foreach($servicos as $s){
|
||||
$item = Servico::findOrFail($s);
|
||||
$tempoServico += (float)$item->tempo_servico;
|
||||
$totalServico += (float)$item->valor;
|
||||
}
|
||||
|
||||
$funcionarios = Funcionario::where('funcionarios.empresa_id', $empresa_id)
|
||||
->select('funcionarios.*')
|
||||
->join('dia_semanas', 'dia_semanas.funcionario_id', '=', 'funcionarios.id')
|
||||
->where(function($q) use ($diaStr) {
|
||||
return $q->orWhere('dia', 'LIKE', '%'.$diaStr.'%');
|
||||
})
|
||||
->when(!empty($funcionario_id), function ($query) use ($funcionario_id) {
|
||||
return $query->where('funcionarios.id', $funcionario_id);
|
||||
})
|
||||
->get();
|
||||
|
||||
$horarios = [];
|
||||
$minutosEspaco = 5;
|
||||
foreach($funcionarios as $f){
|
||||
$funcionamento = Funcionamento
|
||||
::where('funcionario_id', $f->id)
|
||||
->where('dia_id', $diaStr)
|
||||
->first(); // funcionamento do dia
|
||||
|
||||
$inicio = $funcionamento->inicio;
|
||||
|
||||
if($start_time){
|
||||
$inicio = $start_time;
|
||||
}
|
||||
$fim = $funcionamento->fim;
|
||||
|
||||
if($data == date('Y-m-d')){
|
||||
$inicio = strtotime("$data $inicio");
|
||||
$agora = strtotime("$data " . date('H:i'));
|
||||
$horaAgora = date('H:i');
|
||||
|
||||
if($agora > $inicio){
|
||||
// $inicio = date('H:i', strtotime(date('Y-m-d H:i') . "+ $minutosEspaco minutes"));
|
||||
$inicio = $this->proximoMinuto($minutosEspaco);
|
||||
// return $inicio;
|
||||
}
|
||||
}
|
||||
|
||||
$dif = strtotime("$data $fim") - strtotime("$data $inicio");
|
||||
|
||||
$minutosDif = $dif/(60);
|
||||
if($minutosDif > 0 && $tempoServico > 0){
|
||||
|
||||
$contador = $minutosDif/$tempoServico;
|
||||
|
||||
$interrupcoes = Interrupcoes::where('funcionario_id', $f->id)
|
||||
->where('status', 1)
|
||||
->where('dia_id', $diaStr)->get();
|
||||
|
||||
$inicio = strtotime("$data $inicio");
|
||||
for($i=0; $i<$contador; $i++){
|
||||
$fim = strtotime("+".$tempoServico." minutes", $inicio);
|
||||
|
||||
$temp = [
|
||||
'funcionario_id' => $f->id,
|
||||
'funcionario_nome' => $f->nome,
|
||||
'inicio' => date('H:i', $inicio),
|
||||
'fim' => date('H:i', $fim),
|
||||
'data' => $data,
|
||||
'total' => $totalServico,
|
||||
'tempoServico' => $tempoServico
|
||||
];
|
||||
|
||||
$add = true;
|
||||
|
||||
$interrupcao = Interrupcoes::where('funcionario_id', $f->id)
|
||||
->where('dia_id', $diaStr)
|
||||
->whereTime('inicio', '<=', date('H:i', $inicio))
|
||||
->whereTime('fim', '>=', date('H:i', $inicio))
|
||||
->first();
|
||||
|
||||
if($interrupcao != null){
|
||||
|
||||
$add = false;
|
||||
}else{
|
||||
|
||||
$agendamento = Agendamento::where('funcionario_id', $f->id)
|
||||
->whereDate('data', $data)
|
||||
->whereTime('inicio', '<=', date('H:i', $inicio))
|
||||
->whereTime('termino', '>=', date('H:i', $inicio))
|
||||
->first();
|
||||
|
||||
if($agendamento != null){
|
||||
$add = false;
|
||||
}
|
||||
}
|
||||
|
||||
if($add == true){
|
||||
array_push($horarios, $temp);
|
||||
}
|
||||
|
||||
$inicio = $fim;
|
||||
}
|
||||
}
|
||||
}
|
||||
return view('agendamento.partials.agenda_row', compact('horarios'));
|
||||
|
||||
}
|
||||
|
||||
private function proximoMinuto($minutosEspaco){
|
||||
$inicio = date('H:i', strtotime(date('Y-m-d H:i') . "+ $minutosEspaco minutes"));
|
||||
$minuto = date('i', strtotime($inicio));
|
||||
$hora = date('H', strtotime($inicio));
|
||||
|
||||
$divisao = (int)($minuto/15);
|
||||
if($divisao == 0){
|
||||
$inicio = date('H').':15';
|
||||
}else if($divisao == 1){
|
||||
$inicio = date('H').':30';
|
||||
}
|
||||
else if($divisao == 2){
|
||||
$inicio = date('H').':45';
|
||||
}else{
|
||||
$inicio = date('H', strtotime(date('Y-m-d H:i') . "+ 1 hour")).':00';
|
||||
}
|
||||
return $inicio;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ContaPagar;
|
||||
use App\Models\CategoriaConta;
|
||||
use App\Models\Localizacao;
|
||||
|
||||
class ContaPagarController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
$data = ContaPagar::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'fornecedor_id', 'descricao', 'valor_integral', 'valor_pago', 'status', 'data_vencimento', 'data_pagamento',
|
||||
'observacao', 'tipo_pagamento', 'categoria_conta_id', 'created_at')
|
||||
->with(['fornecedor', 'categoria'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function categorias(Request $request){
|
||||
$data = CategoriaConta::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'nome')
|
||||
->orderBy('nome', 'asc')
|
||||
->where('status', 1)
|
||||
->where('tipo', 'pagar')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try {
|
||||
|
||||
$local = Localizacao::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)->first();
|
||||
|
||||
$request->merge([
|
||||
'valor_integral' => $request->valor,
|
||||
'valor_original' => $request->valor,
|
||||
'valor_pago' => $request->status ? $request->valor : 0,
|
||||
'data_pagamento' => $request->status ? $request->data_vencimento : null,
|
||||
'local_id' => $local->id
|
||||
]);
|
||||
|
||||
$conta = ContaPagar::create($request->all());
|
||||
return response()->json($conta, 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
__createLog($request->empresa_id, 'Conta a Pagar', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id){
|
||||
try {
|
||||
$item = ContaPagar::findOrFail($id);
|
||||
|
||||
$request->merge([
|
||||
'valor_integral' => $request->valor,
|
||||
'valor_original' => $request->valor,
|
||||
'valor_pago' => $request->status ? $request->valor : 0,
|
||||
'data_pagamento' => $request->status ? $request->data_vencimento : null,
|
||||
]);
|
||||
|
||||
$item->fill($request->all())->save();
|
||||
return response()->json($item, 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
__createLog($request->empresa_id, 'Conta a Pagar', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function pay(Request $request){
|
||||
try {
|
||||
$item = ContaPagar::findOrFail($request->conta_id);
|
||||
|
||||
$item->status = 1;
|
||||
$item->valor_pago = $request->valor;
|
||||
$item->data_pagamento = $request->data_recebimento;
|
||||
$item->tipo_pagamento = $request->tipo_pagamento;
|
||||
|
||||
$item->save();
|
||||
return response()->json($item, 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id){
|
||||
$item = ContaPagar::findOrFail($id);
|
||||
|
||||
try{
|
||||
$item->delete();
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
__createLog($item->empresa_id, 'Conta a Pagar', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Fornecedor;
|
||||
use App\Models\ContaPagar;
|
||||
|
||||
class FornecedorController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
$data = Fornecedor::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'razao_social', 'cpf_cnpj', 'ie', 'rua', 'numero', 'bairro', 'telefone', 'email', 'complemento', 'cep', 'cidade_id',
|
||||
'created_at')
|
||||
->with('cidade')
|
||||
->orderBy('razao_social', 'desc')
|
||||
->get();
|
||||
|
||||
foreach($data as $c){
|
||||
$c->soma_contas = ContaPagar::where('fornecedor_id', $c->id)
|
||||
->where('status', 0)->sum('valor_integral');
|
||||
}
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
Fornecedor::create($request->all());
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id){
|
||||
try{
|
||||
$item = Fornecedor::findOrFail($id);
|
||||
$item->fill($request->all())->save();
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id){
|
||||
try{
|
||||
$item = Fornecedor::findOrFail($id);
|
||||
|
||||
if(sizeof($item->compras) > 0){
|
||||
return response()->json("Não é possível remover um fornecedor com compras!", 401);
|
||||
}
|
||||
|
||||
$item->produtoFornecedor()->delete();
|
||||
$item->delete();
|
||||
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,732 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Nfe;
|
||||
use App\Models\ItemNfe;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\ComissaoVenda;
|
||||
use App\Models\Produto;
|
||||
use App\Models\MargemComissao;
|
||||
use App\Models\FaturaNfe;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Models\Caixa;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\NaturezaOperacao;
|
||||
use App\Models\Transportadora;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Utils\EstoqueUtil;
|
||||
use App\Utils\EmailUtil;
|
||||
use App\Utils\SiegUtil;
|
||||
use NFePHP\DA\NFe\Danfe;
|
||||
use App\Services\NFeService;
|
||||
use Dompdf\Dompdf;
|
||||
use NFePHP\DA\NFe\Daevento;
|
||||
|
||||
class NfeController extends Controller
|
||||
{
|
||||
|
||||
protected $estoqueUtil;
|
||||
protected $emailUtil;
|
||||
protected $siegUtil;
|
||||
|
||||
public function __construct(EmailUtil $emailUtil, SiegUtil $siegUtil, EstoqueUtil $estoqueUtil){
|
||||
$this->emailUtil = $emailUtil;
|
||||
$this->siegUtil = $siegUtil;
|
||||
$this->estoqueUtil = $estoqueUtil;
|
||||
}
|
||||
|
||||
public function find(Request $request){
|
||||
$item = Nfe::with(['itens', 'cliente', 'fatura'])->findOrFail($request->id);
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function all(Request $request){
|
||||
$estado = $request->estado;
|
||||
$cliente = $request->cliente;
|
||||
$start_date = $request->dataInicio;
|
||||
$end_date = $request->dataFim;
|
||||
$data = Nfe::where('empresa_id', $request->empresa_id)
|
||||
->with(['itens', 'cliente', 'fatura'])
|
||||
->orderBy('id', 'desc')
|
||||
->select('numero_sequencial', 'total', 'cliente_id', 'desconto', 'acrescimo', 'created_at', 'estado', 'id')
|
||||
->when($estado, function ($query) use ($estado) {
|
||||
return $query->where('estado', $estado);
|
||||
})
|
||||
->when($cliente, function ($query) use ($cliente) {
|
||||
return $query->where('cliente_id', $cliente);
|
||||
})
|
||||
->when(($start_date), function ($query) use ($start_date) {
|
||||
return $query->whereDate('created_at', '>=', \Carbon\Carbon::parse($start_date)->format('Y-m-d'));
|
||||
})
|
||||
->when(($end_date), function ($query) use ($end_date) {
|
||||
return $query->whereDate('created_at', '<=', \Carbon\Carbon::parse($end_date)->format('Y-m-d'));
|
||||
})
|
||||
// ->get();
|
||||
->paginate($request->get('per_page', env("PAGINACAO")));
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
$nfe = DB::transaction(function () use ($request) {
|
||||
|
||||
$empresa = Empresa::find($request->empresa_id);
|
||||
$cliente = Cliente::find($request->cliente);
|
||||
|
||||
$numero_nfe = $empresa->numero_ultima_nfe_producao+1;
|
||||
if ($empresa->ambiente == 2) {
|
||||
$numero_nfe = $empresa->numero_ultima_nfe_homologacao+1;
|
||||
}
|
||||
|
||||
$total = __convert_value_bd(str_replace("R$ ", "", $request->total));
|
||||
$desconto = __convert_value_bd(str_replace("R$ ", "", $request->desconto));
|
||||
$acrescimo = __convert_value_bd(str_replace("R$ ", "", $request->acrescimo));
|
||||
$valor_recebido = __convert_value_bd(str_replace("R$ ", "", $request->valor_recebido));
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$caixa = Caixa::where('usuario_id', $funcionario->usuario_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if($caixa == null){
|
||||
return response()->json("Caixa está fechado!", 401);
|
||||
}
|
||||
|
||||
$objetoVenda = [
|
||||
'cliente_id' => $request->cliente,
|
||||
'natureza_id' => $request->natureza_id,
|
||||
'ambiente' => $empresa->ambiente,
|
||||
'total' => $total,
|
||||
'desconto' => $desconto,
|
||||
'acrescimo' => $acrescimo,
|
||||
'estado' => 'novo',
|
||||
'numero' => $numero_nfe,
|
||||
'chave' => '',
|
||||
'observacao' => $request->observacao,
|
||||
'local_id' => $caixa->local_id,
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'numero_serie' => $empresa->numero_serie_nfce ?? 1,
|
||||
'cliente_cpf_cnpj' => $request->cpf_nota,
|
||||
'cliente_nome' => '',
|
||||
'troco' => $valor_recebido > 0 ? ($valor_recebido - $total) : 0,
|
||||
|
||||
'emissor_nome' => $empresa->nome,
|
||||
'emissor_cpf_cnpj' => $empresa->cpf_cnpj,
|
||||
|
||||
'caixa_id' => $caixa->id,
|
||||
'local_id' => $caixa->local_id,
|
||||
'user_id' => $funcionario->usuario_id,
|
||||
'numero_sequencial' => $this->getLastNumero($request->empresa_id)
|
||||
];
|
||||
|
||||
$nfe = Nfe::create($objetoVenda);
|
||||
|
||||
foreach($request->itens as $item){
|
||||
$product = Produto::findOrFail($item['produto_id']);
|
||||
|
||||
$variacao_id = isset($item['variacao_id']) ? $item['variacao_id'] : null;
|
||||
$cfop = $product->cfop_estadual;
|
||||
if($empresa->cidade->uf != $cliente->cidade->uf){
|
||||
$cfop = $product->cfop_outro_estado;
|
||||
}
|
||||
$itemNfe = ItemNfe::create([
|
||||
'nfe_id' => $nfe->id,
|
||||
'produto_id' => $product->id,
|
||||
'quantidade' => $item['quantidade'],
|
||||
'valor_unitario' => $product->valor_unitario,
|
||||
'sub_total' => $item['sub_total'],
|
||||
'perc_icms' => __convert_value_bd($product->perc_icms),
|
||||
'perc_pis' => __convert_value_bd($product->perc_pis),
|
||||
'perc_cofins' => __convert_value_bd($product->perc_cofins),
|
||||
'perc_ipi' => __convert_value_bd($product->perc_ipi),
|
||||
'cst_csosn' => $product->cst_csosn,
|
||||
'cst_pis' => $product->cst_pis,
|
||||
'cst_cofins' => $product->cst_cofins,
|
||||
'cst_ipi' => $product->cst_ipi,
|
||||
'cfop' => $cfop,
|
||||
'ncm' => $product->ncm,
|
||||
'variacao_id' => $variacao_id,
|
||||
]);
|
||||
|
||||
if ($product->gerenciar_estoque) {
|
||||
$this->estoqueUtil->reduzEstoque($product->id, $item['quantidade'], $variacao_id, $caixa->local_id);
|
||||
}
|
||||
|
||||
$tipo = 'reducao';
|
||||
$codigo_transacao = $nfe->id;
|
||||
$tipo_transacao = 'venda_nfe';
|
||||
$this->estoqueUtil->movimentacaoProduto($product->id, $item['quantidade'], $tipo, $codigo_transacao, $tipo_transacao, $funcionario->usuario_id, $variacao_id);
|
||||
}
|
||||
|
||||
foreach($request->fatura as $key => $fatura){
|
||||
FaturaNfe::create([
|
||||
'nfe_id' => $nfe->id,
|
||||
'tipo_pagamento' => $fatura['tipo_pagamento'],
|
||||
'data_vencimento' => $fatura['vencimento'],
|
||||
'valor' => $item['valor']
|
||||
]);
|
||||
|
||||
if(strtotime($fatura['vencimento']) > strtotime(date('Y-m-d'))){
|
||||
ContaReceber::create([
|
||||
'empresa_id' => $nfe->empresa_id,
|
||||
'nfe_id' => $nfe->id,
|
||||
'cliente_id' => $request->cliente,
|
||||
'valor_integral' => $item['valor'],
|
||||
'tipo_pagamento' => $fatura['tipo_pagamento'],
|
||||
'data_vencimento' => $fatura['vencimento'],
|
||||
'local_id' => $caixa->local_id,
|
||||
'descricao' => "Parcela " . $key+1 . " de " . sizeof($request->fatura)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$comissao = $funcionario->comissao;
|
||||
$valorRetorno = $this->calcularComissaoVenda($nfe, $comissao, $nfe->empresa_id);
|
||||
|
||||
if($valorRetorno > 0){
|
||||
ComissaoVenda::create([
|
||||
'funcionario_id' => $funcionario->id,
|
||||
'nfce_id' => null,
|
||||
'nfe_id' => $nfe->id,
|
||||
'tabela' => 'nfe',
|
||||
'valor' => $valorRetorno,
|
||||
'valor_venda' => $nfe->total,
|
||||
'status' => 0,
|
||||
'empresa_id' => $nfe->empresa_id
|
||||
]);
|
||||
}
|
||||
|
||||
return $nfe;
|
||||
});
|
||||
return response()->json($nfe, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
public function update(Request $request){
|
||||
try{
|
||||
$nfe = DB::transaction(function () use ($request) {
|
||||
|
||||
$nfe = Nfe::findOrFail($request->id);
|
||||
|
||||
$empresa = $nfe->empresa;
|
||||
$cliente = $nfe->cliente;
|
||||
|
||||
$total = __convert_value_bd(str_replace("R$ ", "", $request->total));
|
||||
$desconto = __convert_value_bd(str_replace("R$ ", "", $request->desconto));
|
||||
$acrescimo = __convert_value_bd(str_replace("R$ ", "", $request->acrescimo));
|
||||
$valor_recebido = __convert_value_bd(str_replace("R$ ", "", $request->valor_recebido));
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$caixa = Caixa::where('usuario_id', $funcionario->usuario_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if($caixa == null){
|
||||
return response()->json("Caixa está fechado!", 401);
|
||||
}
|
||||
|
||||
$objetoVenda = [
|
||||
'cliente_id' => $request->cliente,
|
||||
'natureza_id' => $request->natureza_id,
|
||||
'ambiente' => $empresa->ambiente,
|
||||
'total' => $total,
|
||||
'desconto' => $desconto,
|
||||
'acrescimo' => $acrescimo,
|
||||
'observacao' => $request->observacao,
|
||||
'cliente_cpf_cnpj' => $request->cpf_nota,
|
||||
'cliente_nome' => '',
|
||||
'troco' => $valor_recebido > 0 ? ($valor_recebido - $total) : 0
|
||||
];
|
||||
|
||||
$nfe->update($objetoVenda)->save();
|
||||
|
||||
foreach($nfe->itens as $it){
|
||||
$it->itensDimensao()->delete();
|
||||
$it->delete();
|
||||
}
|
||||
$item->fatura()->delete();
|
||||
ContaReceber::where('nfe_id', $item->id)->delete();
|
||||
|
||||
foreach($request->itens as $item){
|
||||
$product = Produto::findOrFail($item['produto_id']);
|
||||
|
||||
$variacao_id = isset($item['variacao_id']) ? $item['variacao_id'] : null;
|
||||
$cfop = $product->cfop_estadual;
|
||||
if($empresa->cidade->uf != $cliente->cidade->uf){
|
||||
$cfop = $product->cfop_outro_estado;
|
||||
}
|
||||
$itemNfe = ItemNfe::create([
|
||||
'nfe_id' => $nfe->id,
|
||||
'produto_id' => $product->id,
|
||||
'quantidade' => $item['quantidade'],
|
||||
'valor_unitario' => $product->valor_unitario,
|
||||
'sub_total' => $item['sub_total'],
|
||||
'perc_icms' => __convert_value_bd($product->perc_icms),
|
||||
'perc_pis' => __convert_value_bd($product->perc_pis),
|
||||
'perc_cofins' => __convert_value_bd($product->perc_cofins),
|
||||
'perc_ipi' => __convert_value_bd($product->perc_ipi),
|
||||
'cst_csosn' => $product->cst_csosn,
|
||||
'cst_pis' => $product->cst_pis,
|
||||
'cst_cofins' => $product->cst_cofins,
|
||||
'cst_ipi' => $product->cst_ipi,
|
||||
'cfop' => $cfop,
|
||||
'ncm' => $product->ncm,
|
||||
'variacao_id' => $variacao_id,
|
||||
]);
|
||||
|
||||
if ($product->gerenciar_estoque) {
|
||||
$this->estoqueUtil->reduzEstoque($product->id, $item['quantidade'], $variacao_id, $caixa->local_id);
|
||||
}
|
||||
|
||||
$tipo = 'reducao';
|
||||
$codigo_transacao = $nfe->id;
|
||||
$tipo_transacao = 'venda_nfe';
|
||||
$this->estoqueUtil->movimentacaoProduto($product->id, $item['quantidade'], $tipo, $codigo_transacao, $tipo_transacao, $funcionario->usuario_id, $variacao_id);
|
||||
}
|
||||
|
||||
foreach($request->fatura as $key => $fatura){
|
||||
FaturaNfe::create([
|
||||
'nfe_id' => $nfe->id,
|
||||
'tipo_pagamento' => $fatura['tipo_pagamento'],
|
||||
'data_vencimento' => $fatura['vencimento'],
|
||||
'valor' => $item['valor']
|
||||
]);
|
||||
|
||||
if(strtotime($fatura['vencimento']) > strtotime(date('Y-m-d'))){
|
||||
ContaReceber::create([
|
||||
'empresa_id' => $nfe->empresa_id,
|
||||
'nfe_id' => $nfe->id,
|
||||
'cliente_id' => $request->cliente,
|
||||
'valor_integral' => $item['valor'],
|
||||
'tipo_pagamento' => $fatura['tipo_pagamento'],
|
||||
'data_vencimento' => $fatura['vencimento'],
|
||||
'local_id' => $caixa->local_id,
|
||||
'descricao' => "Parcela " . $key+1 . " de " . sizeof($request->fatura)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$comissao = ComissaoVenda::where('empresa_id', $nfe->empresa_id)
|
||||
->where('nfe_id', $nfe->id)->first();
|
||||
|
||||
if($comissao){
|
||||
$comissao->delete();
|
||||
}
|
||||
|
||||
$comissao = $funcionario->comissao;
|
||||
$valorRetorno = $this->calcularComissaoVenda($nfe, $comissao, $nfe->empresa_id);
|
||||
|
||||
if($valorRetorno > 0){
|
||||
ComissaoVenda::create([
|
||||
'funcionario_id' => $funcionario->id,
|
||||
'nfce_id' => null,
|
||||
'nfe_id' => $nfe->id,
|
||||
'tabela' => 'nfe',
|
||||
'valor' => $valorRetorno,
|
||||
'valor_venda' => $nfe->total,
|
||||
'status' => 0,
|
||||
'empresa_id' => $nfe->empresa_id
|
||||
]);
|
||||
}
|
||||
|
||||
return $nfe;
|
||||
});
|
||||
return response()->json($nfe, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
private function calcularComissaoVenda($nfce, $comissao, $empresa_id)
|
||||
{
|
||||
$valorRetorno = 0;
|
||||
$config = ConfigGeral::where('empresa_id', $empresa_id)->first();
|
||||
|
||||
$tipoComissao = 'percentual_vendedor';
|
||||
if($config != null && $config->tipo_comissao == 'percentual_margem'){
|
||||
$tipoComissao = 'percentual_margem';
|
||||
}
|
||||
if($tipoComissao == 'percentual_vendedor'){
|
||||
$valorRetorno = ((float)$nfce->total * (float)$comissao) / 100;
|
||||
}else{
|
||||
foreach ($nfce->itens as $i) {
|
||||
|
||||
$percentualLucro = ((($i->produto->valor_compra-$i->valor_unitario)/$i->produto->valor_compra)*100)*-1;
|
||||
$margens = MargemComissao::where('empresa_id', $empresa_id)->get();
|
||||
$margemComissao = null;
|
||||
$dif = 0;
|
||||
$difAnterior = 100;
|
||||
foreach($margens as $m){
|
||||
$margem = $m->margem;
|
||||
if($percentualLucro >= $margem){
|
||||
$dif = $percentualLucro - $margem;
|
||||
if($dif < $difAnterior){
|
||||
$margemComissao = $m;
|
||||
$difAnterior = $dif;
|
||||
}
|
||||
}
|
||||
}
|
||||
if($margemComissao){
|
||||
$valorRetorno += ($i->sub_total * $margemComissao->percentual) / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $valorRetorno;
|
||||
}
|
||||
|
||||
private function getLastNumero($empresa_id){
|
||||
$last = Nfe::where('empresa_id', $empresa_id)
|
||||
->orderBy('numero_sequencial', 'desc')
|
||||
->where('numero_sequencial', '>', 0)->first();
|
||||
$numero = $last != null ? $last->numero_sequencial : 0;
|
||||
$numero++;
|
||||
return $numero;
|
||||
}
|
||||
|
||||
public function naturezaOperacao(Request $request){
|
||||
$data = NaturezaOperacao::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'descricao', 'padrao')
|
||||
->get();
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function transportadoras(Request $request){
|
||||
$data = Transportadora::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'razao_social', 'cpf_cnpj')
|
||||
->get();
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function emitir(Request $request)
|
||||
{
|
||||
|
||||
$nfe = Nfe::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($nfe->empresa_id);
|
||||
|
||||
if ($empresa->arquivo == null) {
|
||||
return response()->json("Certificado não encontrado para este emitente", 401);
|
||||
}
|
||||
|
||||
$nfe_service = new NFeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj),
|
||||
// "schemes" => "PL_009_V4",
|
||||
"schemes" => "PL_010_V1.21",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
|
||||
$doc = $nfe_service->gerarXml($nfe);
|
||||
|
||||
if(!isset($doc['erros_xml'])){
|
||||
|
||||
$xml = $doc['xml'];
|
||||
$chave = $doc['chave'];
|
||||
|
||||
$xmlTemp = simplexml_load_string($xml);
|
||||
|
||||
$itensComErro = "";
|
||||
$regime = $empresa->tributacao;
|
||||
foreach ($xmlTemp->infNFe->det as $item) {
|
||||
if (isset($item->imposto->ICMS)) {
|
||||
$icms = (array_values((array)$item->imposto->ICMS));
|
||||
if(sizeof($icms) == 0){
|
||||
$itensComErro .= " Produto " . $item->prod->xProd . " não formando a TAG ICMS, confira se o CST do item corresponde a tributação, regime configurado: $regime";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($itensComErro){
|
||||
return response()->json($itensComErro, 403);
|
||||
}
|
||||
try{
|
||||
$signed = $nfe_service->sign($xml);
|
||||
$resultado = $nfe_service->transmitir($signed, $doc['chave']);
|
||||
|
||||
if ($resultado['erro'] == 0) {
|
||||
$nfe->chave = $doc['chave'];
|
||||
$nfe->estado = 'aprovado';
|
||||
|
||||
if($empresa->ambiente == 2){
|
||||
$empresa->numero_ultima_nfe_homologacao = $doc['numero'];
|
||||
}else{
|
||||
$empresa->numero_ultima_nfe_producao = $doc['numero'];
|
||||
}
|
||||
$nfe->numero = $doc['numero'];
|
||||
$nfe->recibo = $resultado['success'];
|
||||
$nfe->data_emissao = date('Y-m-d H:i:s');
|
||||
|
||||
$nfe->save();
|
||||
$empresa->save();
|
||||
$data = [
|
||||
'recibo' => $resultado['success'],
|
||||
'chave' => $nfe->chave
|
||||
];
|
||||
$descricaoLog = "Emitida número $nfe->numero - $nfe->chave APROVADA";
|
||||
__createLog($nfe->empresa_id, 'NFe', 'transmitir', $descricaoLog);
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_nfe/').$nfe->chave.'.xml';
|
||||
$this->emailUtil->enviarXmlContador($nfe->empresa_id, $fileDir, 'NFe', $nfe->chave);
|
||||
}catch(\Exception $e){
|
||||
}
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_nfe/').$nfe->chave.'.xml';
|
||||
$this->siegUtil->enviarXml($nfe->empresa_id, $fileDir);
|
||||
}catch(\Exception $e){
|
||||
}
|
||||
|
||||
return response()->json($data, 200);
|
||||
}else{
|
||||
$error = $resultado['error'];
|
||||
$recibo = isset($resultado['recibo']) ? $resultado['recibo'] : null;
|
||||
|
||||
$motivo = '';
|
||||
if(isset($error['protNFe'])){
|
||||
$motivo = $error['protNFe']['infProt']['xMotivo'];
|
||||
$cStat = $error['protNFe']['infProt']['cStat'];
|
||||
$nfe->motivo_rejeicao = substr("[$cStat] $motivo", 0, 200);
|
||||
}
|
||||
|
||||
if($nfe->chave == ''){
|
||||
$nfe->chave = $doc['chave'];
|
||||
}
|
||||
|
||||
$descricaoLog = "REJEITADA $nfe->chave - $motivo";
|
||||
__createLog($nfe->empresa_id, 'NFe', 'erro', $descricaoLog);
|
||||
if($nfe->signed_xml == null){
|
||||
$nfe->signed_xml = $signed;
|
||||
}
|
||||
if($nfe->recibo == null){
|
||||
$nfe->recibo = $recibo;
|
||||
}
|
||||
$nfe->estado = 'rejeitado';
|
||||
$nfe->save();
|
||||
|
||||
if(isset($error['protNFe'])){
|
||||
return response()->json("[$cStat] $motivo", 403);
|
||||
}else{
|
||||
return response()->json($error, 403);
|
||||
}
|
||||
}
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 404);
|
||||
}
|
||||
}else{
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
return response()->json($doc, 200);
|
||||
}
|
||||
|
||||
public function cancelar(Request $request)
|
||||
{
|
||||
$nfe = Nfe::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($nfe->empresa_id);
|
||||
if ($nfe != null) {
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$nfe_service = new NFeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$nfe->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj),
|
||||
// "schemes" => "PL_009_V4",
|
||||
"schemes" => "PL_010_V1.21",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$doc = $nfe_service->cancelar($nfe, $request->motivo);
|
||||
|
||||
if (!isset($doc['erro'])) {
|
||||
$nfe->estado = 'cancelado';
|
||||
$nfe->save();
|
||||
|
||||
foreach ($nfe->itens as $i) {
|
||||
if ($i->produto->gerenciar_estoque) {
|
||||
if ($nfe->tpNF == 1) {
|
||||
$this->estoqueUtil->incrementaEstoque($i->produto_id, $i->quantidade, $i->variacao_id, $nfe->local_id);
|
||||
}else{
|
||||
$this->estoqueUtil->reduzEstoque($i->produto_id, $i->quantidade, $i->variacao_id, $nfe->local_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// return response()->json($doc, 200);
|
||||
$motivo = $doc['retEvento']['infEvento']['xMotivo'];
|
||||
$cStat = $doc['retEvento']['infEvento']['cStat'];
|
||||
if($cStat == 135){
|
||||
$descricaoLog = "CANCELADA $nfe->chave";
|
||||
__createLog($nfe->empresa_id, 'NFe', 'cancelar', $descricaoLog);
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_nfe_cancelada/').$nfe->chave.'.xml';
|
||||
$this->siegUtil->enviarXml($nfe->empresa_id, $fileDir);
|
||||
}catch(\Exception $e){
|
||||
}
|
||||
|
||||
return response()->json("[$cStat] $motivo", 200);
|
||||
}else{
|
||||
$descricaoLog = "ERRO CANCELAR: $nfe->chave";
|
||||
__createLog($nfe->empresa_id, 'NFe', 'erro', $descricaoLog);
|
||||
return response()->json("[$cStat] $motivo", 401);
|
||||
}
|
||||
} else {
|
||||
$arr = $doc['data'];
|
||||
$cStat = $arr['retEvento']['infEvento']['cStat'];
|
||||
$motivo = $arr['retEvento']['infEvento']['xMotivo'];
|
||||
$descricaoLog = "ERRO CANCELAR: $nfe->chave - $motivo";
|
||||
__createLog($nfe->empresa_id, 'NFe', 'erro', $descricaoLog);
|
||||
return response()->json("[$cStat] $motivo", $doc['status']);
|
||||
}
|
||||
} else {
|
||||
return response()->json('Consulta não encontrada', 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function danfe(Request $request){
|
||||
$item = Nfe::findOrFail($request->id);
|
||||
try{
|
||||
|
||||
if (!is_dir(public_path('comprovantes_nfe'))) {
|
||||
mkdir(public_path('comprovantes_nfe'), 0777, true);
|
||||
}else{
|
||||
$destino = public_path('comprovantes_nfe');
|
||||
$this->clearFolder($destino);
|
||||
}
|
||||
|
||||
if (file_exists(public_path('xml_nfe/') . $item->chave . '.xml')) {
|
||||
$xml = file_get_contents(public_path('xml_nfe/') . $item->chave . '.xml');
|
||||
$empresa = $item->empresa;
|
||||
|
||||
$danfe = new Danfe($xml);
|
||||
if($empresa->logo){
|
||||
$logo = 'data://text/plain;base64,'. base64_encode(file_get_contents(public_path('/uploads/logos/') .
|
||||
$empresa->logo));
|
||||
$danfe->logoParameters($logo, 'L');
|
||||
}
|
||||
$danfe->exibirTextoFatura = 1;
|
||||
$pdf = $danfe->render();
|
||||
|
||||
$fileName = "nfe_$item->id.pdf";
|
||||
file_put_contents(public_path('comprovantes_nfe/') . $fileName , $pdf);
|
||||
|
||||
$url = env("APP_URL"). "/comprovantes_nfe/".$fileName;
|
||||
|
||||
return response()->json($url, 200);
|
||||
|
||||
} else {
|
||||
return response()->json("Arquivo não encontrado", 401);
|
||||
}
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function danfeCancela(Request $request){
|
||||
|
||||
if (!is_dir(public_path('comprovantes_nfe'))) {
|
||||
mkdir(public_path('comprovantes_nfe'), 0777, true);
|
||||
}else{
|
||||
$destino = public_path('comprovantes_nfe');
|
||||
$this->clearFolder($destino);
|
||||
}
|
||||
|
||||
$item = Nfe::findOrFail($request->id);
|
||||
try{
|
||||
|
||||
$xml = file_get_contents(public_path('xml_nfe_cancelada/') . $item->chave . '.xml');
|
||||
|
||||
$dadosEmitente = $this->getEmitente($item->empresa);
|
||||
|
||||
$daevento = new Daevento($xml, $dadosEmitente);
|
||||
$daevento->debugMode(true);
|
||||
$pdf = $daevento->render();
|
||||
|
||||
$fileName = "nfe_$item->id.pdf";
|
||||
file_put_contents(public_path('comprovantes_nfe/') . $fileName , $pdf);
|
||||
|
||||
$url = env("APP_URL"). "/comprovantes_nfe/".$fileName;
|
||||
return response()->json($url, 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
private function getEmitente($empresa)
|
||||
{
|
||||
return [
|
||||
'razao' => $empresa->nome,
|
||||
'logradouro' => $empresa->rua,
|
||||
'numero' => $empresa->numero,
|
||||
'complemento' => '',
|
||||
'bairro' => $empresa->bairro,
|
||||
'CEP' => preg_replace('/[^0-9]/', '', $empresa->cep),
|
||||
'municipio' => $empresa->cidade->nome,
|
||||
'UF' => $empresa->cidade->uf,
|
||||
'telefone' => $empresa->telefone,
|
||||
'email' => ''
|
||||
];
|
||||
}
|
||||
|
||||
public function imprimirPedido(Request $request){
|
||||
$item = Nfe::findOrFail($request->id);
|
||||
if (!is_dir(public_path('comprovantes_nfe'))) {
|
||||
mkdir(public_path('comprovantes_nfe'), 0777, true);
|
||||
}else{
|
||||
$destino = public_path('comprovantes_nfe');
|
||||
$this->clearFolder($destino);
|
||||
}
|
||||
$config = $item->empresa;
|
||||
|
||||
$configGeral = ConfigGeral::where('empresa_id', $item->empresa_id)->first();
|
||||
|
||||
if($configGeral && $configGeral->mensagem_padrao_impressao_venda){
|
||||
$item->observacao .= " " . $configGeral->mensagem_padrao_impressao_venda;
|
||||
}
|
||||
|
||||
$p = view('nfe.imprimir', compact('config', 'item', 'configGeral'));
|
||||
|
||||
$domPdf = new Dompdf(["enable_remote" => true]);
|
||||
$domPdf->loadHtml($p);
|
||||
$domPdf->setPaper("A4");
|
||||
$domPdf->render();
|
||||
|
||||
$fileName = "pedido_$item->id.pdf";
|
||||
file_put_contents(public_path('comprovantes_nfe/') . $fileName , $domPdf->output());
|
||||
|
||||
$url = env("APP_URL"). "/comprovantes_nfe/".$fileName;
|
||||
|
||||
return response()->json($url, 200);
|
||||
}
|
||||
|
||||
private function clearFolder($destino){
|
||||
$files = glob($destino."/*");
|
||||
foreach($files as $file){
|
||||
if(is_file($file)) unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Nfe;
|
||||
use App\Models\Nfce;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\MovimentacaoProduto;
|
||||
use App\Models\Estoque;
|
||||
use App\Models\Produto;
|
||||
use Dompdf\Dompdf;
|
||||
|
||||
class RelatorioController extends Controller
|
||||
{
|
||||
public function vendas(Request $request){
|
||||
|
||||
$tipo = $request->tipo;
|
||||
$start_date = $request->data_inicial;
|
||||
if($start_date){
|
||||
$start_date = \Carbon\Carbon::createFromFormat('d/m/Y', $start_date)->format('Y-m-d');
|
||||
}
|
||||
$end_date = $request->data_final;
|
||||
if($end_date){
|
||||
$end_date = \Carbon\Carbon::createFromFormat('d/m/Y', $end_date)->format('Y-m-d');
|
||||
}
|
||||
$local_id = $request->local_id;
|
||||
$cliente_id = $request->cliente_id;
|
||||
$start_time = $request->horario_inicial;
|
||||
$end_time = $request->horario_final;
|
||||
$estado = $request->estado;
|
||||
|
||||
if($start_date){
|
||||
if($start_time){
|
||||
$start_date .= " $start_time:59";
|
||||
}else{
|
||||
$start_date .= " 00:00:00";
|
||||
}
|
||||
}
|
||||
|
||||
if($end_date){
|
||||
if($end_time){
|
||||
$end_date .= " $end_time:59";
|
||||
}else{
|
||||
$end_date .= " 23:59:59";
|
||||
}
|
||||
}
|
||||
|
||||
$vendas = Nfe::where('empresa_id', $request->empresa_id)->where('tpNF', 1)
|
||||
->when(!empty($start_date), function ($query) use ($start_date) {
|
||||
return $query->where('created_at', '>=', $start_date);
|
||||
})
|
||||
->when(!empty($end_date), function ($query) use ($end_date) {
|
||||
return $query->where('created_at', '<=', $end_date);
|
||||
})
|
||||
// ->where('nves.estado', '!=', 'cancelado')
|
||||
->when(!empty($estado), function ($query) use ($estado) {
|
||||
return $query->where('estado', $estado);
|
||||
})
|
||||
->when(empty($estado), function ($query) use ($estado) {
|
||||
return $query->where('estado', '!=', 'cancelado');
|
||||
})
|
||||
->limit($total_resultados ?? 1000000)
|
||||
// ->when($local_id, function ($query) use ($local_id) {
|
||||
// return $query->where('local_id', $local_id);
|
||||
// })
|
||||
// ->when(!$local_id, function ($query) use ($locais) {
|
||||
// return $query->whereIn('local_id', $locais);
|
||||
// })
|
||||
->when($cliente_id, function ($query) use ($cliente_id) {
|
||||
return $query->where('cliente_id', $cliente_id);
|
||||
})
|
||||
->get();
|
||||
|
||||
$vendasCaixa = Nfce::where('empresa_id', $request->empresa_id)
|
||||
->when(!empty($start_date), function ($query) use ($start_date) {
|
||||
return $query->where('created_at', '>=', $start_date);
|
||||
})
|
||||
->when(!empty($end_date), function ($query) use ($end_date) {
|
||||
return $query->where('created_at', '<=', $end_date);
|
||||
})
|
||||
|
||||
->where('nfces.empresa_id', $request->empresa_id)
|
||||
->when(!empty($estado), function ($query) use ($estado) {
|
||||
return $query->where('estado', $estado);
|
||||
})
|
||||
->when(empty($estado), function ($query) use ($estado) {
|
||||
return $query->where('estado', '!=', 'cancelado');
|
||||
})
|
||||
->limit($total_resultados ?? 1000000)
|
||||
// ->when($local_id, function ($query) use ($local_id) {
|
||||
// return $query->where('local_id', $local_id);
|
||||
// })
|
||||
// ->when(!$local_id, function ($query) use ($locais) {
|
||||
// return $query->whereIn('local_id', $locais);
|
||||
// })
|
||||
->when($cliente_id, function ($query) use ($cliente_id) {
|
||||
return $query->where('cliente_id', $cliente_id);
|
||||
})
|
||||
->get();
|
||||
|
||||
$data = $this->uneArrayVendas($vendas, $vendasCaixa);
|
||||
|
||||
usort($data, function($a, $b){
|
||||
return $a['data'] > $b['data'] ? 1 : -1;
|
||||
});
|
||||
|
||||
$p = view('relatorios.vendas', compact('data', 'tipo'))
|
||||
->with('title', 'Relatório de Vendas');
|
||||
|
||||
$domPdf = new Dompdf(["enable_remote" => true]);
|
||||
$domPdf->loadHtml($p);
|
||||
|
||||
$pdf = ob_get_clean();
|
||||
|
||||
$domPdf->setPaper("A4", "landscape");
|
||||
$domPdf->render();
|
||||
if (!is_dir(public_path('relatorios_app'))) {
|
||||
mkdir(public_path('relatorios_app'), 0777, true);
|
||||
}
|
||||
|
||||
// limpar pasta
|
||||
|
||||
$files = glob(public_path('relatorios_app/')."/*");
|
||||
foreach($files as $file){
|
||||
if(is_file($file)) unlink($file);
|
||||
}
|
||||
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
$doc = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
|
||||
$fileName = "Relatorio_vendas_$doc.pdf";
|
||||
file_put_contents(public_path('relatorios_app/') . $fileName , $domPdf->output());
|
||||
|
||||
$url = env("APP_URL"). "/relatorios_app/".$fileName;
|
||||
return response()->json($url, 200);
|
||||
}
|
||||
|
||||
private function uneArrayVendas($vendas, $vendasCaixa)
|
||||
{
|
||||
$adicionados = [];
|
||||
$arr = [];
|
||||
foreach ($vendas as $v) {
|
||||
$temp = [
|
||||
'id' => $v->numero_sequencial,
|
||||
'data' => $v->created_at,
|
||||
'tipo' => 'Pedido',
|
||||
'total' => $v->total,
|
||||
'cliente' => $v->cliente ? $v->cliente->info : '--',
|
||||
'localizacao' => $v->localizacao
|
||||
// 'itens' => $v->itens
|
||||
];
|
||||
array_push($adicionados, $v->id);
|
||||
array_push($arr, $temp);
|
||||
}
|
||||
foreach ($vendasCaixa as $v) {
|
||||
$temp = [
|
||||
'id' => $v->numero_sequencial,
|
||||
'data' => $v->created_at,
|
||||
'tipo' => 'PDV',
|
||||
'total' => $v->total,
|
||||
'cliente' => $v->cliente ? $v->cliente->info : '--',
|
||||
'localizacao' => $v->localizacao
|
||||
// 'itens' => $v->itens
|
||||
];
|
||||
array_push($adicionados, $v->id);
|
||||
array_push($arr, $temp);
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public function estoque(Request $request){
|
||||
|
||||
$estoque_minimo = $request->estoque_minimo;
|
||||
$start_date = $request->data_inicial;
|
||||
if($start_date){
|
||||
$start_date = \Carbon\Carbon::createFromFormat('d/m/Y', $start_date)->format('Y-m-d');
|
||||
}
|
||||
$end_date = $request->data_final;
|
||||
if($end_date){
|
||||
$end_date = \Carbon\Carbon::createFromFormat('d/m/Y', $end_date)->format('Y-m-d');
|
||||
}
|
||||
$categoria_id = $request->categoria_id;
|
||||
|
||||
$data = [];
|
||||
|
||||
if($estoque_minimo == 1){
|
||||
|
||||
$produtosComEstoqueMinimo = Produto::where('produtos.empresa_id', $request->empresa_id)
|
||||
->select('produtos.*')
|
||||
->when($categoria_id, function ($query) use ($categoria_id) {
|
||||
return $query->where('categoria_id', $categoria_id);
|
||||
})
|
||||
// ->when(!empty($local_id), function ($query) use ($local_id) {
|
||||
// return $query->join('produto_localizacaos', 'produto_localizacaos.produto_id', '=', 'produtos.id')
|
||||
// ->where('produto_localizacaos.localizacao_id', $local_id);
|
||||
// })
|
||||
// ->when(!$local_id, function ($query) use ($locais) {
|
||||
// return $query->join('produto_localizacaos', 'produto_localizacaos.produto_id', '=', 'produtos.id')
|
||||
// ->whereIn('produto_localizacaos.localizacao_id', $locais);
|
||||
// })
|
||||
->when(!empty($start_date), function ($query) use ($start_date) {
|
||||
return $query->whereDate('produtos.created_at', '>=', $start_date);
|
||||
})
|
||||
->when(!empty($end_date), function ($query) use ($end_date,) {
|
||||
return $query->whereDate('produtos.created_at', '<=', $end_date);
|
||||
})
|
||||
// ->limit(20)
|
||||
->where('produtos.estoque_minimo', '>', 0)->get();
|
||||
foreach($produtosComEstoqueMinimo as $produto){
|
||||
$estoque = Estoque::where('produto_id', $produto->id)->first();
|
||||
|
||||
if($estoque == null || $estoque->quantidade <= $produto->estoque_minimo){
|
||||
|
||||
if(sizeof($produto->variacoes) == 0){
|
||||
$qtd = $estoque ? $estoque->quantidade : '0';
|
||||
if(!$local_id){
|
||||
$qtd = $produto->estoqueTotalProduto();
|
||||
}
|
||||
$linha = [
|
||||
'produto' => $produto->nome,
|
||||
'quantidade' => $qtd,
|
||||
'estoque_minimo' => $produto->estoque_minimo,
|
||||
'valor_compra' => $produto->valor_compra,
|
||||
'valor_venda' => $produto->valor_unitario,
|
||||
'categoria' => $produto->categoria ? $produto->categoria->nome : '--',
|
||||
'data_cadastro' => __data_pt($produto->created_at)
|
||||
];
|
||||
array_push($data, $linha);
|
||||
}else{
|
||||
|
||||
foreach($produto->variacoes as $v){
|
||||
$linha = [
|
||||
'produto' => $produto->nome . " " . $v->descricao,
|
||||
'quantidade' => $v->estoque ? $v->estoque->quantidade : '',
|
||||
'estoque_minimo' => $produto->estoque_minimo,
|
||||
'valor_compra' => $produto->valor_compra,
|
||||
'valor_venda' => $v->valor,
|
||||
'categoria' => $produto->categoria ? $produto->categoria->nome : '--',
|
||||
'data_cadastro' => __data_pt($produto->created_at)
|
||||
];
|
||||
array_push($data, $linha);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}else if($start_date || $end_date){
|
||||
$movimentacoes = MovimentacaoProduto::
|
||||
select('movimentacao_produtos.*')
|
||||
->when(!empty($start_date), function ($query) use ($start_date) {
|
||||
return $query->whereDate('movimentacao_produtos.created_at', '>=', $start_date);
|
||||
})
|
||||
->when(!empty($end_date), function ($query) use ($end_date,) {
|
||||
return $query->whereDate('movimentacao_produtos.created_at', '<=', $end_date);
|
||||
})
|
||||
->join('produtos', 'produtos.id', '=', 'movimentacao_produtos.produto_id')
|
||||
->when($categoria_id, function ($query) use ($categoria_id) {
|
||||
return $query->where('produtos.categoria_id', $categoria_id);
|
||||
})
|
||||
// ->when(!empty($local_id), function ($query) use ($local_id) {
|
||||
// return $query->join('produto_localizacaos', 'produto_localizacaos.produto_id', '=', 'produtos.id')
|
||||
// ->where('produto_localizacaos.localizacao_id', $local_id);
|
||||
// })
|
||||
// ->when(!$local_id, function ($query) use ($locais) {
|
||||
// return $query->join('produto_localizacaos', 'produto_localizacaos.produto_id', '=', 'produtos.id')
|
||||
// ->whereIn('produto_localizacaos.localizacao_id', $locais);
|
||||
// })
|
||||
->where('produtos.empresa_id', $request->empresa_id)
|
||||
->groupBy('produtos.id')
|
||||
->orderBy('movimentacao_produtos.created_at', 'desc')
|
||||
->get();
|
||||
|
||||
foreach($movimentacoes as $m){
|
||||
|
||||
$produto = $m->produto;
|
||||
if(sizeof($produto->variacoes) == 0){
|
||||
$linha = [
|
||||
'produto' => $m->produto->nome,
|
||||
'quantidade' => $m->produto->estoqueTotalProduto(),
|
||||
'estoque_minimo' => $m->produto->estoque_minimo,
|
||||
'valor_compra' => $m->produto->valor_compra,
|
||||
'valor_venda' => $m->produto->valor_unitario,
|
||||
'categoria' => $m->produto->categoria ? $m->produto->categoria->nome : '--',
|
||||
'data_cadastro' => __data_pt($m->produto->created_at)
|
||||
];
|
||||
array_push($data, $linha);
|
||||
}else{
|
||||
foreach($produto->variacoes as $v){
|
||||
$linha = [
|
||||
'produto' => $m->produto->nome . " " . $v->descricao,
|
||||
'quantidade' => $v->estoque ? $v->estoque->quantidade : '',
|
||||
'estoque_minimo' => $m->produto->estoque_minimo,
|
||||
'valor_compra' => $m->produto->valor_compra,
|
||||
'valor_venda' => $v->valor,
|
||||
'categoria' => $m->produto->categoria ? $m->produto->categoria->nome : '--',
|
||||
'data_cadastro' => __data_pt($m->produto->created_at)
|
||||
];
|
||||
array_push($data, $linha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}else{
|
||||
|
||||
$estoque = Estoque::
|
||||
select('estoques.*')
|
||||
->join('produtos', 'produtos.id', '=', 'estoques.produto_id')
|
||||
->groupBy('produtos.id')
|
||||
->when(!empty($local_id), function ($query) use ($local_id) {
|
||||
return $query->join('produto_localizacaos', 'produto_localizacaos.produto_id', '=', 'produtos.id')
|
||||
->where('estoques.local_id', $local_id);
|
||||
})
|
||||
->when($categoria_id, function ($query) use ($categoria_id) {
|
||||
return $query->where('produtos.categoria_id', $categoria_id);
|
||||
})
|
||||
->where('produtos.empresa_id', $request->empresa_id)->get();
|
||||
|
||||
foreach($estoque as $m){
|
||||
$produto = $m->produto;
|
||||
if(sizeof($produto->variacoes) == 0){
|
||||
$linha = [
|
||||
'produto' => $m->produto->nome,
|
||||
'quantidade' => $m->quantidade,
|
||||
'estoque_minimo' => $m->produto->estoque_minimo,
|
||||
'valor_compra' => $m->produto->valor_compra,
|
||||
'valor_venda' => $m->produto->valor_unitario,
|
||||
'categoria' => $m->produto->categoria ? $m->produto->categoria->nome : '--',
|
||||
'data_cadastro' => __data_pt($m->produto->created_at)
|
||||
];
|
||||
array_push($data, $linha);
|
||||
}else{
|
||||
foreach($produto->variacoes as $v){
|
||||
$linha = [
|
||||
'produto' => $m->produto->nome . " " . $v->descricao,
|
||||
'quantidade' => $v->estoque ? $v->estoque->quantidade : '',
|
||||
'estoque_minimo' => $m->produto->estoque_minimo,
|
||||
'valor_compra' => $m->produto->valor_compra,
|
||||
'valor_venda' => $v->valor,
|
||||
'categoria' => $m->produto->categoria ? $m->produto->categoria->nome : '--',
|
||||
'data_cadastro' => __data_pt($m->produto->created_at)
|
||||
];
|
||||
array_push($data, $linha);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$localizacao = null;
|
||||
$p = view('relatorios.estoque', compact('data', 'start_date', 'end_date', 'estoque_minimo', 'localizacao'))
|
||||
->with('title', 'Relatório de Estoque');
|
||||
$domPdf = new Dompdf(["enable_remote" => true]);
|
||||
$domPdf->loadHtml($p);
|
||||
|
||||
$pdf = ob_get_clean();
|
||||
|
||||
$domPdf->setPaper("A4", "landscape");
|
||||
$domPdf->render();
|
||||
if (!is_dir(public_path('relatorios_app'))) {
|
||||
mkdir(public_path('relatorios_app'), 0777, true);
|
||||
}
|
||||
|
||||
// limpar pasta
|
||||
|
||||
$files = glob(public_path('relatorios_app/')."/*");
|
||||
foreach($files as $file){
|
||||
if(is_file($file)) unlink($file);
|
||||
}
|
||||
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
$doc = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
|
||||
$fileName = "Relatorio_vendas_$doc.pdf";
|
||||
file_put_contents(public_path('relatorios_app/') . $fileName , $domPdf->output());
|
||||
|
||||
$url = env("APP_URL"). "/relatorios_app/".$fileName;
|
||||
return response()->json($url, 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,565 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Cte;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Cidade;
|
||||
use App\Models\ComponenteCte;
|
||||
use App\Models\ChaveNfeCte;
|
||||
use App\Models\Veiculo;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\MedidaCte;
|
||||
use App\Models\NaturezaOperacao;
|
||||
use App\Services\CTeService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use NFePHP\DA\CTe\Dacte;
|
||||
use NFePHP\DA\CTe\Daevento;
|
||||
class CTeController extends Controller
|
||||
{
|
||||
public function __construct(){
|
||||
if (!is_dir(public_path('xml_cte'))) {
|
||||
mkdir(public_path('xml_cte'), 0777, true);
|
||||
}
|
||||
if (!is_dir(public_path('xml_cte_cancelada'))) {
|
||||
mkdir(public_path('xml_cte_cancelada'), 0777, true);
|
||||
}
|
||||
if (!is_dir(public_path('xml_cte_correcao'))) {
|
||||
mkdir(public_path('xml_cte_correcao'), 0777, true);
|
||||
}
|
||||
if (!is_dir(public_path('dacte_temp'))) {
|
||||
mkdir(public_path('dacte_temp'), 0777, true);
|
||||
}
|
||||
|
||||
if (!is_dir(public_path('dacte'))) {
|
||||
mkdir(public_path('dacte'), 0777, true);
|
||||
}
|
||||
if (!is_dir(public_path('dacte_cancelamento'))) {
|
||||
mkdir(public_path('dacte_cancelamento'), 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function emitir(Request $request){
|
||||
$documento = $request->documento;
|
||||
$remetente = $request->remetente;
|
||||
$destinatario = $request->destinatario;
|
||||
$recebedor = $request->recebedor;
|
||||
$expedidor = $request->expedidor;
|
||||
$tomador = $request->tomador;
|
||||
$referencia = $request->referencia;
|
||||
$documentoAuxiliar = $request->documento_auxiliar;
|
||||
$componentes = $request->componentes;
|
||||
$medidas = $request->medidas;
|
||||
$veiculo = $request->veiculo;
|
||||
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
|
||||
// return response()->json($remetente, 200);
|
||||
|
||||
if($empresa->arquivo == null){
|
||||
return response()->json("Certificado não encontrado para este emitente", 401);
|
||||
}
|
||||
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
|
||||
$cte = DB::transaction(function () use ($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador, $referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo) {
|
||||
$cte = $this->criaCte($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador,
|
||||
$referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo);
|
||||
return $cte;
|
||||
});
|
||||
|
||||
$doc = $cte_service->gerarCTe($cte);
|
||||
|
||||
if (!isset($doc['erros_xml'])) {
|
||||
$xml = $doc['xml'];
|
||||
$chave = $doc['chave'];
|
||||
|
||||
try{
|
||||
$signed = $cte_service->sign($xml);
|
||||
$resultado = $cte_service->transmitir($signed, $doc['chave']);
|
||||
|
||||
if ($resultado['erro'] == 0) {
|
||||
$cte->chave = $doc['chave'];
|
||||
$cte->estado = 'aprovado';
|
||||
$cte->numero = $doc['nCte'];
|
||||
$cte->recibo = $resultado['success'];
|
||||
if($empresa->ambiente == 2){
|
||||
$empresa->numero_ultima_cte_homologacao = $doc['nCte'];
|
||||
}else{
|
||||
$empresa->numero_ultima_cte_producao = $doc['nCte'];
|
||||
}
|
||||
|
||||
$empresa->save();
|
||||
|
||||
$cte->save();
|
||||
|
||||
$xml = file_get_contents(public_path('xml_cte/') . $cte->chave . '.xml');
|
||||
$danfe = new Dacte($xml);
|
||||
$pdf = $danfe->render();
|
||||
file_put_contents(public_path('dacte/') . $cte->chave . '.pdf', $pdf);
|
||||
$pathPrint = env("APP_URL") . "/dacte/$cte->chave.pdf";
|
||||
$data = [
|
||||
'recibo' => $resultado['success'],
|
||||
'url_print' => $pathPrint,
|
||||
'chave' => $cte->chave
|
||||
];
|
||||
return response()->json($data, 200);
|
||||
}else{
|
||||
$error = $resultado['error'];
|
||||
// return response()->json($resultado, 401);
|
||||
|
||||
if(isset($error['protCTe'])){
|
||||
$motivo = $error['protCTe']['infProt']['xMotivo'];
|
||||
$cStat = $error['protCTe']['infProt']['cStat'];
|
||||
$cte->motivo_rejeicao = substr("[$cStat] $motivo", 0, 200);
|
||||
}
|
||||
|
||||
$cte->numero = isset($documento['numero_cte']) ? $documento['numero_cte'] :
|
||||
Nfe::lastNumero($empresa);
|
||||
$cte->chave = $doc['chave'];
|
||||
$cte->estado = 'rejeitado';
|
||||
$cte->save();
|
||||
|
||||
if(isset($error['protCTe'])){
|
||||
return response()->json("[$cStat] $motivo", 403);
|
||||
}else{
|
||||
return response()->json($error, 403);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($xml, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json(__getError($e), 404);
|
||||
}
|
||||
|
||||
}else{
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function xmlTemporario(Request $request){
|
||||
|
||||
$documento = $request->documento;
|
||||
$remetente = $request->remetente;
|
||||
$destinatario = $request->destinatario;
|
||||
$recebedor = $request->recebedor;
|
||||
$expedidor = $request->expedidor;
|
||||
$tomador = $request->tomador;
|
||||
$referencia = $request->referencia;
|
||||
$documentoAuxiliar = $request->documento_auxiliar;
|
||||
$componentes = $request->componentes;
|
||||
$medidas = $request->medidas;
|
||||
$veiculo = $request->veiculo;
|
||||
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
|
||||
// return response()->json($remetente, 200);
|
||||
|
||||
if($empresa->arquivo == null){
|
||||
return response()->json("Certificado não encontrado para este emitente", 401);
|
||||
}
|
||||
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
|
||||
$cte = DB::transaction(function () use ($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador, $referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo) {
|
||||
$cte = $this->criaCte($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador,
|
||||
$referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo);
|
||||
return $cte;
|
||||
});
|
||||
|
||||
$doc = $cte_service->gerarCTe($cte);
|
||||
|
||||
if($cte != null){
|
||||
$cte->componentes()->delete();
|
||||
$cte->medidas()->delete();
|
||||
$cte->chaves_nfe()->delete();
|
||||
$cte->delete();
|
||||
}
|
||||
if (!isset($doc['erros_xml'])) {
|
||||
$xml = $doc['xml'];
|
||||
|
||||
return response()->json($xml, 200);
|
||||
}else{
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function dacteTemporario(Request $request){
|
||||
|
||||
$documento = $request->documento;
|
||||
$remetente = $request->remetente;
|
||||
$destinatario = $request->destinatario;
|
||||
$recebedor = $request->recebedor;
|
||||
$expedidor = $request->expedidor;
|
||||
$tomador = $request->tomador;
|
||||
$referencia = $request->referencia;
|
||||
$documentoAuxiliar = $request->documento_auxiliar;
|
||||
$componentes = $request->componentes;
|
||||
$medidas = $request->medidas;
|
||||
$veiculo = $request->veiculo;
|
||||
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
|
||||
// return response()->json($remetente, 200);
|
||||
|
||||
if($empresa->arquivo == null){
|
||||
return response()->json("Certificado não encontrado para este emitente", 401);
|
||||
}
|
||||
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
|
||||
$cte = DB::transaction(function () use ($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador, $referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo) {
|
||||
$cte = $this->criaCte($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador,
|
||||
$referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo);
|
||||
return $cte;
|
||||
});
|
||||
|
||||
$doc = $cte_service->gerarCTe($cte);
|
||||
|
||||
if($cte != null){
|
||||
$cte->componentes()->delete();
|
||||
$cte->medidas()->delete();
|
||||
$cte->chaves_nfe()->delete();
|
||||
$cte->delete();
|
||||
}
|
||||
if (!isset($doc['erros_xml'])) {
|
||||
$xml = $doc['xml'];
|
||||
$chave = $doc['chave'];
|
||||
|
||||
$danfe = new Dacte($xml);
|
||||
$pdf = $danfe->render();
|
||||
file_put_contents(public_path('dacte_temp/') . $chave . '.pdf', $pdf);
|
||||
$pathPrint = env("APP_URL") . "/dacte_temp/$chave.pdf";
|
||||
return response()->json($pathPrint, 200);
|
||||
|
||||
return response()->json($xml, 200);
|
||||
}else{
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function criaCte($empresa, $documento, $remetente, $destinatario, $recebedor, $expedidor, $tomador,
|
||||
$referencia, $documentoAuxiliar, $componentes, $medidas, $veiculo){
|
||||
$remetente_id = $this->criaCliente($remetente, $empresa->id);
|
||||
$destinatario_id = $this->criaCliente($destinatario, $empresa->id);
|
||||
|
||||
$recebedor_id = null;
|
||||
$expedidor_id = null;
|
||||
if($recebedor){
|
||||
$recebedor_id = $this->criaCliente($recebedor, $empresa->id);
|
||||
}
|
||||
if($expedidor){
|
||||
$expedidor_id = $this->criaCliente($expedidor, $empresa->id);
|
||||
}
|
||||
|
||||
$natureza_id = null;
|
||||
$natureza_id = $this->criaNatureza($documento['natureza_operacao'], $empresa->id);
|
||||
$veiculo_id = $this->criaVeiculo($veiculo, $empresa->id);
|
||||
|
||||
$numero = isset($documento['numero_cte']) ? $documento['numero_cte'] : 0;
|
||||
if($numero == 0){
|
||||
$empresa = Empresa::findOrFail($empresa->id);
|
||||
$numero = Cte::lastNumero($empresa);
|
||||
}
|
||||
|
||||
$cidadeEnvio = Cidade::where('codigo', $documento['municipio_envio_ibge'])->first();
|
||||
$cidadeInicio = Cidade::where('codigo', $documento['municipio_inicio_ibge'])->first();
|
||||
$cidadeFim = Cidade::where('codigo', $documento['municipio_fim_ibge'])->first();
|
||||
$cidadeTomador = Cidade::where('codigo', $tomador['cod_municipio_ibge'])->first();
|
||||
|
||||
$data = [
|
||||
'empresa_id' => $empresa->id,
|
||||
'remetente_id' => $remetente_id,
|
||||
'destinatario_id' => $destinatario_id,
|
||||
'natureza_id' => $natureza_id,
|
||||
'veiculo_id' => $veiculo_id,
|
||||
|
||||
'chave' => "",
|
||||
'numero_serie' => $documento['numero_serie'],
|
||||
'cfop' => $documento['cfop'],
|
||||
'numero' => $numero,
|
||||
'estado' => 'novo',
|
||||
'tomador' => $tomador['tipo'],
|
||||
'logradouro_tomador' => $tomador['rua'],
|
||||
'numero_tomador' => $tomador['numero'],
|
||||
'bairro_tomador' => $tomador['bairro'],
|
||||
'cep_tomador' => $tomador['cep'],
|
||||
'municipio_tomador' => $cidadeTomador->id,
|
||||
'municipio_envio' => $cidadeEnvio->id,
|
||||
'municipio_inicio' => $cidadeInicio->id,
|
||||
'municipio_fim' => $cidadeFim->id,
|
||||
'api' => 1,
|
||||
'motivo_rejeicao' => "",
|
||||
'sequencia_cce' => 0,
|
||||
'ambiente' => $empresa->ambiente,
|
||||
|
||||
'valor_transporte' => $documento['valor_transporte'],
|
||||
'valor_receber' => $documento['valor_receber'],
|
||||
'valor_carga' => $documento['valor_carga'],
|
||||
|
||||
'produto_predominante' => $documento['produto_predominante'],
|
||||
'data_prevista_entrega' => $documento['data_prevista_entrega'],
|
||||
'observacao' => isset($documento['observacao']) ? $documento['observacao'] : '',
|
||||
'retira' => isset($documento['retira']) ? $documento['retira'] : 0,
|
||||
'detalhes_retira' => isset($documento['detalhes_retira']) ? $documento['detalhes_retira'] : '',
|
||||
|
||||
'modal' => $documento['modal'],
|
||||
|
||||
'tpDoc' => $documentoAuxiliar ? $documentoAuxiliar['tipo'] : '',
|
||||
'descOutros' => $documentoAuxiliar ? $documentoAuxiliar['descricao'] : '',
|
||||
'nDoc' => $documentoAuxiliar ? $documentoAuxiliar['numero'] : 0,
|
||||
'vDocFisc' => $documentoAuxiliar ? $documentoAuxiliar['valor'] : 0,
|
||||
'globalizado' => isset($documento['globalizado']) ? $documento['globalizado'] : '',
|
||||
|
||||
|
||||
'cst' => isset($documento['cst']) ? $documento['cst'] : '00',
|
||||
'perc_icms' => __convert_value_bd($documento['perc_icms']),
|
||||
'perc_red_bc' => __convert_value_bd($documento['perc_red_bc']),
|
||||
|
||||
];
|
||||
$item = Cte::create($data);
|
||||
|
||||
$this->criaCompenentes($componentes, $item->id);
|
||||
$this->criaMedidas($medidas, $item->id);
|
||||
if($referencia){
|
||||
$this->criaReferencia($referencia, $item->id);
|
||||
}
|
||||
return $item;
|
||||
|
||||
}
|
||||
|
||||
private function criaReferencia($referencia, $cte_id){
|
||||
foreach($referencia as $r){
|
||||
ChaveNfeCte::create([
|
||||
'chave' => $r['chave'],
|
||||
'cte_id' => $cte_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function criaCompenentes($componentes, $cte_id){
|
||||
foreach($componentes as $c){
|
||||
ComponenteCte::create([
|
||||
'nome' => $c['nome'],
|
||||
'valor' => __convert_value_bd($c['valor']),
|
||||
'cte_id' => $cte_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function criaMedidas($medidas, $cte_id){
|
||||
foreach($medidas as $m){
|
||||
MedidaCte::create([
|
||||
'cte_id' => $cte_id,
|
||||
'tipo_medida' => $m['tipo_medida'],
|
||||
'quantidade' => __convert_value_bd($m['quantidade']),
|
||||
'cod_unidade' => $m['cod_unidade']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function criaNatureza($descricao, $empresa_id){
|
||||
$natureza = NaturezaOperacao::where('descricao', $descricao)->first();
|
||||
if($natureza != null){
|
||||
return $natureza->id;
|
||||
}
|
||||
|
||||
$natureza = NaturezaOperacao::create([
|
||||
'empresa_id' => $empresa_id,
|
||||
'descricao' => $descricao,
|
||||
]);
|
||||
|
||||
return $natureza->id;
|
||||
}
|
||||
|
||||
private function criaVeiculo($veiculo, $empresa_id){
|
||||
$item = Veiculo::where('placa', $veiculo)->first();
|
||||
if($item != null){
|
||||
return $natureza->id;
|
||||
}
|
||||
|
||||
$item = Veiculo::create([
|
||||
'empresa_id' => $empresa_id,
|
||||
'tipo' => $veiculo['tipo'],
|
||||
'placa' => $veiculo['placa'],
|
||||
'uf' => $veiculo['uf'],
|
||||
'cor' => $veiculo['cor'],
|
||||
'marca' => $veiculo['marca'],
|
||||
'modelo' => $veiculo['modelo'],
|
||||
'rntrc' => isset($veiculo['rntrc']) ? $veiculo['rntrc'] : '',
|
||||
'tipo_carroceria' => $veiculo['tipo_carroceria'],
|
||||
'tipo_rodado' => $veiculo['tipo_rodado'],
|
||||
'tara' => __convert_value_bd($veiculo['tara']),
|
||||
'capacidade' => __convert_value_bd($veiculo['capacidade']),
|
||||
'proprietario_documento' => $veiculo['proprietario_documento'],
|
||||
'proprietario_nome' => $veiculo['proprietario_nome'],
|
||||
'proprietario_ie' => isset($veiculo['proprietario_ie']) ? $veiculo['proprietario_ie'] : '',
|
||||
'proprietario_uf' => $veiculo['proprietario_uf'],
|
||||
'proprietario_tp' => $veiculo['proprietario_tp'],
|
||||
'taf' => isset($veiculo['taf']) ? $veiculo['taf'] : '',
|
||||
'renavam' => isset($veiculo['renavam']) ? $veiculo['renavam'] : '',
|
||||
'numero_registro_estadual' => isset($veiculo['numero_registro_estadual']) ? $veiculo['numero_registro_estadual'] : ''
|
||||
]);
|
||||
|
||||
return $item->id;
|
||||
}
|
||||
|
||||
private function criaCliente($cliente, $empresa_id){
|
||||
$cpf_cnpj = preg_replace('/[^0-9]/', '', $cliente['cpf_cnpj']);
|
||||
if(strlen($cpf_cnpj) == 11){
|
||||
$doc = $this->setMask($cliente["cpf_cnpj"], '###.###.###-##');
|
||||
}else{
|
||||
$doc = $this->setMask($cliente["cpf_cnpj"], '##.###.###/####-##');
|
||||
}
|
||||
|
||||
$cli = Cliente::where('cpf_cnpj', $doc)->first();
|
||||
if($cli != null){
|
||||
return $cli->id;
|
||||
}
|
||||
|
||||
$cidade = Cidade::where('codigo', $cliente['cod_municipio_ibge'])->first();
|
||||
|
||||
$cli = Cliente::create([
|
||||
'empresa_id' => $empresa_id,
|
||||
'razao_social' => $cliente['nome'],
|
||||
'nome_fantasia' => $cliente['nome'],
|
||||
'cpf_cnpj' => $doc,
|
||||
'ie' => isset($cliente['ie']) ? $cliente['ie'] : '',
|
||||
'contribuinte' => $cliente['contribuinte'],
|
||||
'consumidor_final' => $cliente['consumidor_final'],
|
||||
'email' => isset($cliente['email']) ? $cliente['email'] : '',
|
||||
'telefone' => isset($cliente['telefone']) ? $cliente['telefone'] : '',
|
||||
'cidade_id' => $cidade->id,
|
||||
'rua' => $cliente['rua'],
|
||||
'cep' => $cliente['cep'],
|
||||
'numero' => $cliente['numero'],
|
||||
'bairro' => $cliente['bairro'],
|
||||
'complemento' => isset($cliente['complemento']) ? $cliente['complemento'] : '',
|
||||
]);
|
||||
return $cli->id;
|
||||
|
||||
}
|
||||
|
||||
private function setMask($val, $mask) {
|
||||
$maskared = '';
|
||||
$k = 0;
|
||||
for($i = 0; $i<=strlen($mask)-1; $i++) {
|
||||
if($mask[$i] == '#') {
|
||||
if(isset($val[$k])) $maskared .= $val[$k++];
|
||||
} else {
|
||||
if(isset($mask[$i])) {
|
||||
if($mask[$i] == $val[$k]) {
|
||||
$k++;
|
||||
}
|
||||
$maskared .= $mask[$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $maskared;
|
||||
}
|
||||
|
||||
public function cancelar(Request $request)
|
||||
{
|
||||
$chave = $request->chave;
|
||||
$cte = Cte::where('chave', $chave)->first();
|
||||
if ($cte != null) {
|
||||
|
||||
$empresa = $cte->empresa;
|
||||
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$doc = $cte_service->cancelar($cte, $request->motivo);
|
||||
|
||||
if (!isset($doc['erro'])) {
|
||||
$cte->estado = 'cancelado';
|
||||
$cte->save();
|
||||
// return response()->json($doc, 200);
|
||||
// if(!isset($doc['retEvento'])){
|
||||
// return response()->json($doc, 401);
|
||||
// }
|
||||
$motivo = $doc['infEvento']['xMotivo'];
|
||||
$cStat = $doc['infEvento']['cStat'];
|
||||
|
||||
$xml = file_get_contents(public_path('xml_cte_cancelada/') . $cte->chave . '.xml');
|
||||
$dadosEmitente = $this->getEmitente($cte->empresa);
|
||||
$daevento = new Daevento($xml, $dadosEmitente);
|
||||
$pdf = $daevento->render();
|
||||
|
||||
file_put_contents(public_path('dacte_cancelamento/') . $cte->chave . '.pdf', $pdf);
|
||||
$pathPrint = env("APP_URL") . "/dacte_cancelamento/$cte->chave.pdf";
|
||||
|
||||
$data = [
|
||||
'url_print' => $pathPrint,
|
||||
'status' => "[$cStat] $motivo"
|
||||
];
|
||||
|
||||
return response()->json($data, 200);
|
||||
|
||||
} else {
|
||||
$arr = $doc['data'];
|
||||
if(isset($arr['infEvento'])){
|
||||
|
||||
$cStat = $arr['infEvento']['cStat'];
|
||||
$motivo = $arr['infEvento']['xMotivo'];
|
||||
|
||||
return response()->json("[$cStat] $motivo", $doc['status']);
|
||||
}else{
|
||||
return response()->json($arr, $doc['status']);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return response()->json('Consulta não encontrada', 404);
|
||||
}
|
||||
}
|
||||
|
||||
private function getEmitente($empresa){
|
||||
|
||||
return [
|
||||
'razao' => $empresa->nome,
|
||||
'logradouro' => $empresa->rua,
|
||||
'numero' => $empresa->numero,
|
||||
'complemento' => '',
|
||||
'bairro' => $empresa->bairro,
|
||||
'CEP' => preg_replace('/[^0-9]/', '', $empresa->cep),
|
||||
'municipio' => $empresa->cidade->nome,
|
||||
'UF' => $empresa->cidade->uf,
|
||||
'telefone' => $empresa->telefone,
|
||||
'email' => ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CteOs;
|
||||
use App\Models\Empresa;
|
||||
use App\Services\CTeOsService;
|
||||
use Illuminate\Http\Request;
|
||||
use NFePHP\DA\CTe\Dacte;
|
||||
use NFePHP\DA\CTe\Daevento;
|
||||
|
||||
class CTeOsPainelController extends Controller
|
||||
{
|
||||
public function emitir(Request $request)
|
||||
{
|
||||
|
||||
$item = CteOs::findOrFail($request->id);
|
||||
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cte_service = new CTeOsService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
|
||||
$doc = $cte_service->gerarCTe($item);
|
||||
|
||||
if (!isset($doc['erros_xml'])) {
|
||||
$xml = $doc['xml'];
|
||||
$chave = $doc['chave'];
|
||||
|
||||
$signed = $cte_service->sign($xml);
|
||||
$resultado = $cte_service->transmitir($signed, $chave);
|
||||
// return response()->json($resultado, 403);
|
||||
|
||||
if ($resultado['erro'] == 0) {
|
||||
$item->chave = $doc['chave'];
|
||||
$item->estado_emissao = 'aprovado';
|
||||
|
||||
if ($empresa->ambiente == 2) {
|
||||
$empresa->numero_ultima_cte_homologacao = $doc['nCte'];
|
||||
} else {
|
||||
$empresa->numero_ultima_cte_producao = $doc['nCte'];
|
||||
}
|
||||
$empresa->save();
|
||||
|
||||
$item->numero_emissao = $doc['nCte'];
|
||||
$item->recibo = $resultado['success'];
|
||||
$item->save();
|
||||
$data = [
|
||||
'recibo' => $resultado['success'],
|
||||
'chave' => $item->chave
|
||||
];
|
||||
return response()->json($data, 200);
|
||||
} else {
|
||||
$error = $resultado['error'];
|
||||
|
||||
if (isset($error['protCTe'])) {
|
||||
$motivo = $error['protCTe']['infProt']['xMotivo'];
|
||||
$cStat = $error['protCTe']['infProt']['cStat'];
|
||||
$item->motivo_rejeicao = substr("[$cStat] $motivo", 0, 200);
|
||||
}
|
||||
$item->chave = $doc['chave'];
|
||||
$item->estado_emissao = 'rejeitado';
|
||||
$item->save();
|
||||
|
||||
if (isset($error['protCTe'])) {
|
||||
return response()->json("[$cStat] $motivo", 403);
|
||||
} else {
|
||||
return response()->json($error, 403);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function consultar(Request $request)
|
||||
{
|
||||
$item = CteOs::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
if ($item != null) {
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$cte_service = new CTeOsService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$consulta = $cte_service->consultar($item);
|
||||
if (!isset($consulta['erro'])) {
|
||||
$motivo = $consulta['protCTe']['infProt']['xMotivo'];
|
||||
$cStat = $consulta['protCTe']['infProt']['cStat'];
|
||||
if ($cStat == 100) {
|
||||
return response()->json("[$cStat] $motivo", 200);
|
||||
} else {
|
||||
return response()->json("[$cStat] $motivo", 401);
|
||||
}
|
||||
} else {
|
||||
return response()->json($consulta['data'], $consulta['status']);
|
||||
}
|
||||
} else {
|
||||
return response()->json('Consulta não encontrada', 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function cancelar(Request $request)
|
||||
{
|
||||
$item = CteOs::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
if ($item != null) {
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$cte_service = new CTeOsService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$doc = $cte_service->cancelar($item, $request->motivo);
|
||||
|
||||
if (!isset($doc['erro'])) {
|
||||
$item->estado_emissao = 'cancelado';
|
||||
$item->save();
|
||||
|
||||
$motivo = $doc['infEvento']['xMotivo'];
|
||||
$cStat = $doc['infEvento']['cStat'];
|
||||
if ($cStat == 135) {
|
||||
return response()->json("[$cStat] $motivo", 200);
|
||||
} else {
|
||||
return response()->json("[$cStat] $motivo", 401);
|
||||
}
|
||||
} else {
|
||||
$arr = $doc['data'];
|
||||
$cStat = $arr['infEvento']['cStat'];
|
||||
$motivo = $arr['infEvento']['xMotivo'];
|
||||
|
||||
return response()->json("[$cStat] $motivo", $doc['status']);
|
||||
}
|
||||
} else {
|
||||
return response()->json('Consulta não encontrada', 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function corrigir(Request $request)
|
||||
{
|
||||
$item = CteOs::findOrFail($request->id);
|
||||
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$cte_service = new CTeOsService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$cte = $cte_service->cartaCorrecao($item, $request->grupo, $request->campo, $request->motivo);
|
||||
|
||||
if (!isset($cte['erro'])) {
|
||||
|
||||
return response()->json($cte['infEvento']['cStat'] . " - " . $cte['infEvento']['xMotivo'], 200);
|
||||
} else {
|
||||
return response()->json($cte['data'], $cte['status']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Cte;
|
||||
use App\Models\Empresa;
|
||||
use App\Services\CTeService;
|
||||
use App\Utils\EmailUtil;
|
||||
use App\Utils\SiegUtil;
|
||||
|
||||
class CTePainelController extends Controller
|
||||
{
|
||||
|
||||
protected $emailUtil;
|
||||
protected $siegUtil;
|
||||
|
||||
public function __construct(EmailUtil $util, SiegUtil $siegUtil){
|
||||
$this->emailUtil = $util;
|
||||
$this->siegUtil = $siegUtil;
|
||||
|
||||
}
|
||||
|
||||
public function emitir(Request $request){
|
||||
|
||||
$item = Cte::findOrFail($request->id);
|
||||
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
|
||||
$doc = $cte_service->gerarCTe($item);
|
||||
|
||||
if (!isset($doc['erros_xml'])) {
|
||||
$xml = $doc['xml'];
|
||||
$chave = $doc['chave'];
|
||||
|
||||
$signed = $cte_service->sign($xml);
|
||||
$resultado = $cte_service->transmitir($signed, $chave);
|
||||
// return response()->json($resultado, 403);
|
||||
|
||||
if ($resultado['erro'] == 0) {
|
||||
$item->chave = $doc['chave'];
|
||||
$item->estado = 'aprovado';
|
||||
|
||||
if($empresa->ambiente == 2){
|
||||
$empresa->numero_ultima_cte_homologacao = $doc['nCte'];
|
||||
}else{
|
||||
$empresa->numero_ultima_cte_producao = $doc['nCte'];
|
||||
}
|
||||
$item->numero = $doc['nCte'];
|
||||
$item->recibo = $resultado['success'];
|
||||
$item->save();
|
||||
$empresa->save();
|
||||
$data = [
|
||||
'recibo' => $resultado['success'],
|
||||
'chave' => $item->chave
|
||||
];
|
||||
|
||||
$descricaoLog = "Emitida número $item->numero - $item->chave APROVADA";
|
||||
__createLog($item->empresa_id, 'CTe', 'transmitir', $descricaoLog);
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_cte/').$item->chave.'.xml';
|
||||
$this->emailUtil->enviarXmlContador($empresa->id, $fileDir, 'CTe', $item->chave);
|
||||
}catch(\Exception $e){
|
||||
|
||||
}
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_cte/').$item->chave.'.xml';
|
||||
$this->siegUtil->enviarXml($item->empresa_id, $fileDir);
|
||||
}catch(\Exception $e){
|
||||
}
|
||||
|
||||
return response()->json($data, 200);
|
||||
}else{
|
||||
$error = $resultado['error'];
|
||||
$motivo = '';
|
||||
if(isset($error['protCTe'])){
|
||||
$motivo = $error['protCTe']['infProt']['xMotivo'];
|
||||
$cStat = $error['protCTe']['infProt']['cStat'];
|
||||
$item->motivo_rejeicao = substr("[$cStat] $motivo", 0, 200);
|
||||
}
|
||||
$item->chave = $doc['chave'];
|
||||
$item->estado = 'rejeitado';
|
||||
$item->save();
|
||||
|
||||
$descricaoLog = "REJEITADA $item->chave - $motivo";
|
||||
__createLog($item->empresa_id, 'CTe', 'erro', $descricaoLog);
|
||||
if(isset($error['protCTe'])){
|
||||
return response()->json("[$cStat] $motivo", 403);
|
||||
}else{
|
||||
return response()->json($error, 403);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function consultar(Request $request)
|
||||
{
|
||||
$item = Cte::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
if ($item != null) {
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $empresa->cpf_cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$consulta = $cte_service->consultar($item);
|
||||
if (!isset($consulta['erro'])) {
|
||||
$motivo = $consulta['protCTe']['infProt']['xMotivo'];
|
||||
$cStat = $consulta['protCTe']['infProt']['cStat'];
|
||||
if($cStat == 100){
|
||||
return response()->json("[$cStat] $motivo", 200);
|
||||
}else{
|
||||
return response()->json("[$cStat] $motivo", 401);
|
||||
}
|
||||
}else{
|
||||
return response()->json($consulta['data'], $consulta['status']);
|
||||
}
|
||||
} else {
|
||||
return response()->json('Consulta não encontrada', 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function cancelar(Request $request)
|
||||
{
|
||||
$item = Cte::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
|
||||
if ($item != null) {
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$doc = $cte_service->cancelar($item, $request->motivo);
|
||||
|
||||
if (!isset($doc['erro'])) {
|
||||
$item->estado = 'cancelado';
|
||||
$item->save();
|
||||
|
||||
$motivo = $doc['infEvento']['xMotivo'];
|
||||
$cStat = $doc['infEvento']['cStat'];
|
||||
if($cStat == 135){
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_cte_cancelada/').$item->chave.'.xml';
|
||||
$this->siegUtil->enviarXml($item->empresa_id, $fileDir);
|
||||
}catch(\Exception $e){
|
||||
}
|
||||
|
||||
return response()->json("[$cStat] $motivo", 200);
|
||||
}else{
|
||||
return response()->json("[$cStat] $motivo", 401);
|
||||
}
|
||||
} else {
|
||||
|
||||
try{
|
||||
$arr = $doc['data'];
|
||||
$cStat = $arr['infEvento']['cStat'];
|
||||
$motivo = $arr['infEvento']['xMotivo'];
|
||||
|
||||
return response()->json("[$cStat] $motivo", $doc['status']);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($doc, 401);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return response()->json('Consulta não encontrada', 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function corrigir(Request $request){
|
||||
|
||||
$item = Cte::findOrFail($request->id);
|
||||
|
||||
$empresa = Empresa::findOrFail($item->empresa_id);
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
$cnpj = preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj);
|
||||
$cte_service = new CTeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => $cnpj,
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
], $empresa);
|
||||
$cte = $cte_service->cartaCorrecao($item, $request->grupo, $request->campo, $request->motivo);
|
||||
|
||||
if(!isset($cte['erro'])){
|
||||
return response()->json($cte['infEvento']['cStat']." - ". $cte['infEvento']['xMotivo'], 200);
|
||||
}else{
|
||||
|
||||
try{
|
||||
$fileDir = public_path('xml_cte_correcao/').$item->chave.'.xml';
|
||||
$this->siegUtil->enviarXml($item->empresa_id, $fileDir);
|
||||
}catch(\Exception $e){
|
||||
}
|
||||
|
||||
return response()->json($cte['data'], $cte['status']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Cardapio;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ItemCarrinhoCardapio;
|
||||
use App\Models\CarrinhoCardapio;
|
||||
|
||||
class CarrinhoController extends Controller
|
||||
{
|
||||
public function carrinhoCount(Request $request){
|
||||
$item = ItemCarrinhoCardapio::join('carrinho_cardapios', 'carrinho_cardapios.id', 'item_carrinho_cardapios.carrinho_id')
|
||||
->where('carrinho_cardapios.session_cart_cardapio', $request->session_cart_cardapio)
|
||||
->where('carrinho_cardapios.session_cart_user', $request->session_cart_user)
|
||||
->count();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function validaEstoque(Request $request){
|
||||
$item = ItemCarrinhoCardapio::findOrFail($request->item_id);
|
||||
if($item->servico){
|
||||
return response()->json("estoque ok", 200);
|
||||
}
|
||||
$quantidade = $request->quantidade;
|
||||
$produto = $item->produto;
|
||||
if($produto->gerenciar_estoque){
|
||||
if(!$produto->estoque || $produto->estoque->quantidade < $quantidade){
|
||||
return response()->json("Estoque insuficiente!", 401);
|
||||
}
|
||||
}
|
||||
return response()->json("estoque ok", 200);
|
||||
}
|
||||
|
||||
public function atualizaQuantidade(Request $request){
|
||||
$item = ItemCarrinhoCardapio::findOrFail($request->item_id);
|
||||
$item->quantidade = $request->quantidade;
|
||||
$item->sub_total = $item->quantidade * $item->valor_unitario;
|
||||
$item->save();
|
||||
|
||||
$carrinho = CarrinhoCardapio::findOrFail($item->carrinho_id);
|
||||
$carrinho->valor_total = $carrinho->itens->sum('sub_total') + $carrinho->valor_frete;
|
||||
|
||||
$carrinho->save();
|
||||
$item->total_carrinho = $carrinho->valor_total;
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function removeItem(Request $request){
|
||||
$item = ItemCarrinhoCardapio::findOrFail($request->item_id);
|
||||
$item->adicionais()->delete();
|
||||
$item->sabores()->delete();
|
||||
$item->delete();
|
||||
|
||||
$carrinho = $item->carrinho;
|
||||
$carrinho->valor_total = $carrinho->itens->sum('sub_total') + $carrinho->valor_frete - $carrinho->valor_desconto;
|
||||
$carrinho->save();
|
||||
return response()->json($carrinho, 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Cardapio;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Produto;
|
||||
use App\Models\TamanhoPizza;
|
||||
use App\Models\ConfiguracaoCardapio;
|
||||
use App\Models\CategoriaAdicional;
|
||||
use App\Models\ItemCarrinhoCardapio;
|
||||
|
||||
class ProdutoController extends Controller
|
||||
{
|
||||
public function produtoModal($hash){
|
||||
$item = Produto::where('hash_delivery', $hash)->first();
|
||||
|
||||
$tamanho = TamanhoPizza::where('empresa_id', $item->empresa_id)
|
||||
->orderBy('maximo_sabores', 'desc')->first();
|
||||
$maximo_sabores_pizza = 0;
|
||||
if($tamanho != null){
|
||||
$maximo_sabores_pizza = $tamanho->maximo_sabores;
|
||||
}
|
||||
|
||||
$config = ConfiguracaoCardapio::where('empresa_id', $item->empresa_id)->first();
|
||||
|
||||
$tamanhosPizza = TamanhoPizza::where('empresa_id', $item->empresa_id)
|
||||
->where('status', 1)
|
||||
->with('produtos')
|
||||
->get();
|
||||
|
||||
$tipoPizza = 0;
|
||||
if($item->categoria && $item->categoria->tipo_pizza){
|
||||
$tipoPizza = 1;
|
||||
}
|
||||
|
||||
$categoriasAdicional = CategoriaAdicional::where('categoria_adicionals.empresa_id', $item->empresa_id)
|
||||
->where('categoria_adicionals.status', 1)
|
||||
->select('categoria_adicionals.*')
|
||||
->join('adicionals', 'adicionals.categoria_id', '=', 'categoria_adicionals.id')
|
||||
->join('produto_adicionals', 'produto_adicionals.adicional_id', '=', 'adicionals.id')
|
||||
->where('produto_adicionals.produto_id', $item->id)
|
||||
->groupBy('categoria_adicionals.id')
|
||||
->get();
|
||||
|
||||
$link = request()->link;
|
||||
|
||||
return view('qr_code_cardapio.partials.produto_modal',
|
||||
compact('item', 'config', 'maximo_sabores_pizza', 'tamanhosPizza', 'tipoPizza', 'categoriasAdicional', 'link'))->render();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\CategoriaProduto;
|
||||
use App\Models\CarrosselCardapio;
|
||||
use App\Models\ConfiguracaoCardapio;
|
||||
use App\Models\Produto;
|
||||
use App\Models\Pedido;
|
||||
use App\Models\ItemPedido;
|
||||
use App\Models\ItemPizzaPedido;
|
||||
use App\Models\ItemAdicional;
|
||||
use App\Models\TamanhoPizza;
|
||||
use App\Models\NotificaoCardapio;
|
||||
use App\Models\ImpressoraPedidoProduto;
|
||||
use App\Models\Nfe;
|
||||
|
||||
class CardapioController extends Controller
|
||||
{
|
||||
public function setConfig(Request $request){
|
||||
$item = ConfiguracaoCardapio::where('api_token', $request->token)
|
||||
->first();
|
||||
|
||||
if($item == null){
|
||||
return response()->json("Configuração não encontrada!", 401);
|
||||
}
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function categorias(Request $request){
|
||||
$data = CategoriaProduto::where('empresa_id', $request->empresa_id)
|
||||
->orderBy('nome', 'asc')
|
||||
->where('status', 1)
|
||||
->where('categoria_id', null)
|
||||
->where('cardapio', 1)->get();
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function destaques(Request $request){
|
||||
$data = CarrosselCardapio::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function config(Request $request){
|
||||
$item = ConfiguracaoCardapio::where('empresa_id', $request->empresa_id)
|
||||
|
||||
->first();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function categoria(Request $request, $id){
|
||||
$item = CategoriaProduto::where('empresa_id', $request->empresa_id)
|
||||
->where('id', $id)
|
||||
->with('produtosCardapio')
|
||||
->first();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function produto(Request $request, $id){
|
||||
$item = Produto::where('empresa_id', $request->empresa_id)
|
||||
->where('id', $id)
|
||||
->with(['adicionais', 'ingredientes'])
|
||||
->first();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function ingredientes(Request $request){
|
||||
$produtos = [];
|
||||
foreach($request->data as $c){
|
||||
$produto = Produto::findOrFail($c['id']);
|
||||
array_push($produtos, $produto);
|
||||
}
|
||||
|
||||
$ingredientes = [];
|
||||
$ingredientesTemp = [];
|
||||
foreach($produtos as $p){
|
||||
foreach($p->ingredientes as $i){
|
||||
if(!in_array($i->ingrediente, $ingredientesTemp)){
|
||||
array_push($ingredientesTemp, $i->ingrediente);
|
||||
array_push($ingredientes, $i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return response()->json($ingredientes, 200);
|
||||
}
|
||||
|
||||
private function validaItemImpressao($produto_id){
|
||||
|
||||
$imprime = ImpressoraPedidoProduto::where('produto_id', $produto_id)->first();
|
||||
return $imprime != null ? 0 : 1;
|
||||
}
|
||||
|
||||
public function storePedido(Request $request){
|
||||
|
||||
$item = Pedido::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->where('mesa', $request->mesa)
|
||||
->first();
|
||||
|
||||
if($item == null){
|
||||
return response()->json("nada encontrado", 404);
|
||||
}
|
||||
|
||||
//salvando itens
|
||||
|
||||
foreach($request->carrinho as $cartItem){
|
||||
$cartItem = (object)$cartItem;
|
||||
|
||||
$observacao = $cartItem->observacao;
|
||||
if($cartItem->remover_ingredientes != ''){
|
||||
$observacao .= ' - remover: '. $cartItem->remover_ingredientes;
|
||||
}
|
||||
$estado = 'novo';
|
||||
$prod = Produto::findOrFail($cartItem->produto_id);
|
||||
if($prod->tempo_preparo){
|
||||
$estado = 'pendente';
|
||||
}
|
||||
$impresso = $this->validaItemImpressao($cartItem->produto_id);
|
||||
$itemPedido = ItemPedido::create([
|
||||
'pedido_id' => $item->id,
|
||||
'produto_id' => $cartItem->produto_id,
|
||||
'observacao' => $observacao,
|
||||
'quantidade' => $cartItem->quantidade,
|
||||
'valor_unitario' => $cartItem->valor_unitario > 0 ? $cartItem->valor_unitario : $cartItem->produto_valor,
|
||||
'sub_total' => $cartItem->sub_total,
|
||||
'ponto_carne' => $cartItem->ponto_carne,
|
||||
'tamanho_id' => $cartItem->tamanho_id,
|
||||
'estado' => $estado,
|
||||
'impresso' => $impresso
|
||||
]);
|
||||
|
||||
foreach($cartItem->sabores as $s){
|
||||
$s = (object)$s;
|
||||
ItemPizzaPedido::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'produto_id' => $s->id
|
||||
]);
|
||||
}
|
||||
|
||||
foreach($cartItem->adicionais as $a){
|
||||
$a = (object)$a;
|
||||
if($a){
|
||||
|
||||
$dataItemAdicional = [
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'adicional_id' => $a->adicional_id,
|
||||
];
|
||||
ItemAdicional::create($dataItemAdicional);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$item->sumTotal();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function storeMesa(Request $request){
|
||||
|
||||
$item = Pedido::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->where('mesa', $request->mesa)
|
||||
->first();
|
||||
|
||||
// dd($request->all());
|
||||
|
||||
if($item != null){
|
||||
return response()->json("esta mesa já se encontra aberta", 401);
|
||||
}
|
||||
|
||||
$item = Pedido::create([
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'cliente_nome' => $request->nome,
|
||||
'cliente_fone' => $request->telefone,
|
||||
'mesa' => $request->mesa,
|
||||
'total' => 0,
|
||||
'comanda' => 'M'.$request->mesa,
|
||||
'local_pedido' => 'App'
|
||||
]);
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function conta(Request $request){
|
||||
$item = Pedido::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->where('mesa', $request->mesa)
|
||||
->with('itens')
|
||||
->first();
|
||||
|
||||
foreach($item->itens as $it){
|
||||
if($it->produto->tempo_preparo > 0){
|
||||
$it->tempo_preparo = $it->tempoPreparoRestante();
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function chamarGarcom(Request $request){
|
||||
$item = Pedido::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('mesa', $request->mesa)
|
||||
->first();
|
||||
|
||||
if($item == null){
|
||||
return response()->json("nada encontrado", 404);
|
||||
}
|
||||
|
||||
$notificacao = NotificaoCardapio::create([
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'mesa' => $request->mesa,
|
||||
'tipo' => 'garcom'
|
||||
]);
|
||||
|
||||
return response()->json($notificacao, 200);
|
||||
|
||||
}
|
||||
|
||||
public function tiposDePagamento(){
|
||||
$data = Nfe::tiposPagamento();
|
||||
$tipos = [];
|
||||
foreach($data as $key => $t){
|
||||
array_push($tipos, [
|
||||
'codigo' => $key,
|
||||
'nome' => $t
|
||||
]);
|
||||
}
|
||||
return response()->json($tipos, 200);
|
||||
}
|
||||
|
||||
public function finalizarConta(Request $request){
|
||||
$item = Pedido::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->where('mesa', $request->mesa)
|
||||
->first();
|
||||
|
||||
if($item == null){
|
||||
return response()->json("nada encontrado", 404);
|
||||
}
|
||||
|
||||
$notificacao = NotificaoCardapio::create([
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'mesa' => $request->mesa,
|
||||
'pedido_id' => $item->id,
|
||||
'tipo' => 'fechar_mesa',
|
||||
'observacao' => $request->observacao,
|
||||
'avaliacao' => $request->avaliacao,
|
||||
'tipo_pagamento' => $request->tipo_pagamento,
|
||||
]);
|
||||
|
||||
$item->em_atendimento = 0;
|
||||
$item->save();
|
||||
|
||||
return response()->json($notificacao, 200);
|
||||
}
|
||||
|
||||
public function emAtendimento(Request $request){
|
||||
$item = Pedido::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->where('mesa', $request->mesa)
|
||||
->first();
|
||||
|
||||
if($item == null){
|
||||
return response()->json("nada encontrado", 404);
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function pesquisa(Request $request){
|
||||
$data = Produto::where('empresa_id', $request->empresa_id)
|
||||
->where('nome', 'like', "%$request->pesquisa%")
|
||||
->where('cardapio', 1)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function tamanhosPizza(Request $request){
|
||||
$data = TamanhoPizza::where('empresa_id', $request->empresa_id)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\CategoriaProduto;
|
||||
|
||||
class CategoriaProdutoController extends Controller
|
||||
{
|
||||
public function categoriaParaSubcategoria(Request $request){
|
||||
$data = CategoriaProduto::where('empresa_id', $request->empresa_id)
|
||||
->when(!is_numeric($request->pesquisa), function ($q) use ($request) {
|
||||
return $q->where('nome', 'LIKE', "%$request->pesquisa%");
|
||||
})
|
||||
->where('status', 1)
|
||||
->where('categoria_id', null)
|
||||
->get();
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
|
||||
public function subcategorias(Request $request){
|
||||
$data = CategoriaProduto::
|
||||
when(!is_numeric($request->pesquisa), function ($q) use ($request) {
|
||||
return $q->where('nome', 'LIKE', "%$request->pesquisa%");
|
||||
})
|
||||
->where('categoria_id', $request->categoria_id)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Models\CashBackConfig;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\ItemNfe;
|
||||
use App\Models\ItemNfce;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClienteController extends Controller
|
||||
{
|
||||
public function find($id)
|
||||
{
|
||||
$item = Cliente::with(['cidade', 'listaPreco', 'tributacao', 'fatura'])->findOrFail($id);
|
||||
$config = ConfigGeral::where('empresa_id', $item->empresa_id)->first();
|
||||
|
||||
if($config == null || $config->limitar_cliente_inadimplente == 0){
|
||||
$item->inadimplente = false;
|
||||
}else{
|
||||
$item->inadimplente = ContaReceber::where('cliente_id', $id)
|
||||
->where('status', 0)
|
||||
->whereDate('data_vencimento', '<', now()->toDateString())
|
||||
->exists();
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function produtosHistorico(Request $request){
|
||||
$cliente_id = $request->cliente_id;
|
||||
$pesquisa = $request->pesquisa;
|
||||
|
||||
$itensNfe = ItemNfe::select('item_nves.*')
|
||||
->join('nves', 'nves.id', '=', 'item_nves.nfe_id')
|
||||
->where('cliente_id', $cliente_id)
|
||||
->join('produtos', 'produtos.id', '=', 'item_nves.produto_id')
|
||||
->where('nves.tpNF', 1)
|
||||
->where('produtos.nome', 'LIKE', "%$pesquisa%")
|
||||
->get();
|
||||
|
||||
$itensNfce = ItemNfce::select('item_nfces.*')
|
||||
->join('nfces', 'nfces.id', '=', 'item_nfces.nfce_id')
|
||||
->where('cliente_id', $cliente_id)
|
||||
->join('produtos', 'produtos.id', '=', 'item_nfces.produto_id')
|
||||
->where('produtos.nome', 'LIKE', "%$pesquisa%")
|
||||
->get();
|
||||
|
||||
$itens = $itensNfe
|
||||
->merge($itensNfce)
|
||||
->sortByDesc('created_at')
|
||||
->values();
|
||||
|
||||
return view('clientes.partials.tabela_historico', compact('itens'))->render();
|
||||
|
||||
}
|
||||
|
||||
public function cashback($id)
|
||||
{
|
||||
$item = Cliente::with('cidade')->findOrFail($id);
|
||||
$config = CashBackConfig::where('empresa_id', $item->empresa_id)->first();
|
||||
if($config == null){
|
||||
return response()->json(null, 404);
|
||||
}
|
||||
$config->valor_cashback = $item->valor_cashback;
|
||||
return response()->json($config, 200);
|
||||
}
|
||||
|
||||
public function pesquisa(Request $request)
|
||||
{
|
||||
$pesquisa = $request->pesquisa;
|
||||
$pesquisaOriginal = trim($pesquisa);
|
||||
$pesquisaNumerica = preg_replace('/\D/', '', $pesquisaOriginal);
|
||||
|
||||
$data = Cliente::orderBy('razao_social', 'desc')
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
// ->where('razao_social', 'like', "%$request->pesquisa%")
|
||||
// ->where(function($q) use ($pesquisa){
|
||||
// $q->where('razao_social', 'like', "%$pesquisa%")->orWhere('nome_fantasia', 'like', "%$pesquisa%")
|
||||
// ->orWhere('numero_sequencial', 'LIKE', "%$pesquisa%");
|
||||
// })
|
||||
|
||||
->when($pesquisaNumerica !== '', function ($query) use ($pesquisaNumerica) {
|
||||
$clean = "%{$pesquisaNumerica}%";
|
||||
|
||||
$query->where(function ($q) use ($clean) {
|
||||
$q->whereRaw("
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(cpf_cnpj, '.', ''), '-', ''), '/', ''), ' ', '') LIKE ?
|
||||
", [$clean])
|
||||
->orWhereRaw("
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(numero_sequencial, '.', ''), '-', ''), '/', ''), ' ', '') LIKE ?
|
||||
", [$clean]);
|
||||
});
|
||||
})
|
||||
->when($pesquisaNumerica === '', function ($query) use ($pesquisaOriginal) {
|
||||
$like = "%{$pesquisaOriginal}%";
|
||||
$query->where(function($q) use ($like){
|
||||
$q->where('razao_social', 'like', $like)
|
||||
->orWhere('nome_fantasia', 'like', $like);
|
||||
});
|
||||
})
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function pesquisaDelivery(Request $request)
|
||||
{
|
||||
$data = Cliente::orderBy('razao_social', 'desc')
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('uid', '!=', '')
|
||||
->when(!is_numeric($request->pesquisa), function ($q) use ($request) {
|
||||
return $q->where('razao_social', 'LIKE', "%$request->pesquisa%");
|
||||
})
|
||||
->when(is_numeric($request->pesquisa), function ($q) use ($request) {
|
||||
return $q->where('telefone', 'LIKE', "%$request->pesquisa%");
|
||||
})
|
||||
->get();
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
$cliente = Cliente::where('empresa_id', $request->empresa_id)
|
||||
->where('cpf_cnpj', $request->cpf_cnpj)
|
||||
->first();
|
||||
if($cliente != null){
|
||||
return response()->json("Cliente já cadastrado", 401);
|
||||
}
|
||||
$cliente = Cliente::create($request->all());
|
||||
return response()->json($cliente, 200);
|
||||
}
|
||||
|
||||
public function consultaDebitos(Request $request){
|
||||
$totalVenda = $request->total;
|
||||
$somaContas = ContaReceber::where('cliente_id', $request->cliente_id)
|
||||
->where('status', 0)
|
||||
->sum('valor_integral');
|
||||
|
||||
$cliente = Cliente::findOrFail($request->cliente_id);
|
||||
if($somaContas+$totalVenda > $cliente->valor_credito){
|
||||
return response()->json("Valor ultrapassa o limite definido no cadastro do cliente", 403);
|
||||
}
|
||||
return response()->json($cliente->valor_credito, 200);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Models\Cidade;
|
||||
use App\Models\Empresa;
|
||||
|
||||
class ClienteController extends Controller
|
||||
{
|
||||
public function findCliente(Request $request){
|
||||
$mask = '(##) #####-####';
|
||||
$telefone = preg_replace('/[^0-9]/', '', $request->telefone);
|
||||
|
||||
$telefone = __mask($telefone, $mask);
|
||||
$item = Cliente::where('telefone', $telefone)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function index(Request $request){
|
||||
$data = Cliente::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'razao_social', 'cpf_cnpj', 'ie', 'rua', 'numero', 'bairro', 'telefone', 'email', 'complemento', 'cep', 'cidade_id',
|
||||
'limite_credito', 'status', 'lista_preco_id', 'created_at')
|
||||
->with('cidade')
|
||||
->orderBy('razao_social', 'desc')
|
||||
->where('status', 1)
|
||||
->get();
|
||||
|
||||
foreach($data as $c){
|
||||
$c->soma_contas = ContaReceber::where('cliente_id', $c->id)
|
||||
->where('status', 0)->sum('valor_integral');
|
||||
}
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function cidades(Request $request){
|
||||
$cidades = Cidade::select('id', 'nome', 'uf', 'codigo')
|
||||
->get();
|
||||
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
$data = [
|
||||
'cidades' => $cidades,
|
||||
'cidade_padrao' => $empresa->cidade_id
|
||||
];
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function cidades2(Request $request){
|
||||
$data = Cidade::select('id', 'nome', 'uf', 'codigo')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
Cliente::create($request->all());
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id){
|
||||
try{
|
||||
$item = Cliente::findOrFail($id);
|
||||
$item->fill($request->all())->save();
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id){
|
||||
try{
|
||||
$item = Cliente::findOrFail($id);
|
||||
$item->tributacao()->delete();
|
||||
$item->enderecosEcommerce()->delete();
|
||||
|
||||
foreach($item->carrinhosDelivery as $c){
|
||||
$c->itens()->delete();
|
||||
$c->delete();
|
||||
}
|
||||
|
||||
$item->delete();
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,467 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Pedido;
|
||||
use App\Models\ItemPedido;
|
||||
use App\Models\Produto;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\ItemAdicional;
|
||||
use App\Models\ItemPizzaPedido;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\TamanhoPizza;
|
||||
use App\Models\ProdutoPizzaValor;
|
||||
use App\Models\ConfiguracaoCardapio;
|
||||
use App\Models\MarketPlaceConfig;
|
||||
use App\Models\ImpressoraPedidoProduto;
|
||||
|
||||
class ComandaController extends Controller
|
||||
{
|
||||
|
||||
public function comandas(Request $request){
|
||||
|
||||
$abertas = Pedido::
|
||||
where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->orderBy('comanda', 'asc')
|
||||
->get();
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', request()->empresa_id)->first();
|
||||
|
||||
foreach($abertas as $item){
|
||||
if($item->comanda < 10){
|
||||
$item->comanda = "00$item->comanda";
|
||||
}elseif($item->comanda >= 10 && $item->comanda < 99){
|
||||
$item->comanda = "0$item->comanda";
|
||||
}else{
|
||||
$item->comanda = "$item->comanda";
|
||||
}
|
||||
$item->total = $item->total + $item->acrescimo - $item->desconto;
|
||||
}
|
||||
|
||||
$fechadas = [];
|
||||
if($config && $config->numero_inicial_comanda && $config->numero_final_comanda){
|
||||
for($i=$config->numero_inicial_comanda; $i<=$config->numero_final_comanda; $i++){
|
||||
$pedido = Pedido::where('empresa_id', request()->empresa_id)
|
||||
->where('status', 1)->where('comanda', $i)->first();
|
||||
|
||||
if($pedido == null){
|
||||
if($i < 10){
|
||||
$com = "00$i";
|
||||
}elseif($i >= 10 && $i < 99){
|
||||
$com = "0$i";
|
||||
}else{
|
||||
$com = "$i";
|
||||
}
|
||||
|
||||
$fechadas[] = [
|
||||
'comanda' => $com,
|
||||
'mesa' => ($pedido != null && $pedido->_mesa) ? $pedido->_mesa->nome : '',
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'abertas' => $abertas,
|
||||
'fechadas' => $fechadas
|
||||
];
|
||||
|
||||
return response()->json($abertas, 200);
|
||||
}
|
||||
|
||||
public function buscarComandas(Request $request){
|
||||
|
||||
$abertas = Pedido::
|
||||
where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->where('em_atendimento', 1)
|
||||
->orderBy('comanda', 'asc')
|
||||
->get();
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', request()->empresa_id)->first();
|
||||
|
||||
foreach($abertas as $item){
|
||||
if($item->comanda < 10){
|
||||
$item->comanda = "00$item->comanda";
|
||||
}elseif($item->comanda >= 10 && $item->comanda < 99){
|
||||
$item->comanda = "0$item->comanda";
|
||||
}else{
|
||||
$item->comanda = "$item->comanda";
|
||||
}
|
||||
$item->total = $item->total + $item->acrescimo - $item->desconto;
|
||||
}
|
||||
|
||||
$fechadas = [];
|
||||
if($config && $config->numero_inicial_comanda && $config->numero_final_comanda){
|
||||
for($i=$config->numero_inicial_comanda; $i<=$config->numero_final_comanda; $i++){
|
||||
$pedido = Pedido::where('empresa_id', request()->empresa_id)
|
||||
->where('status', 1)->where('comanda', $i)->first();
|
||||
|
||||
if($pedido == null){
|
||||
if($i < 10){
|
||||
$com = "00$i";
|
||||
}elseif($i >= 10 && $i < 99){
|
||||
$com = "0$i";
|
||||
}else{
|
||||
$com = "$i";
|
||||
}
|
||||
|
||||
$fechadas[] = [
|
||||
'comanda' => $com,
|
||||
'mesa' => ($pedido != null && $pedido->_mesa) ? $pedido->_mesa->nome : '',
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'abertas' => $abertas,
|
||||
'fechadas' => $fechadas
|
||||
];
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function find(Request $request){
|
||||
|
||||
$item = Pedido::with(['itens'])
|
||||
->findOrFail($request->comanda_id);
|
||||
|
||||
$config = ConfiguracaoCardapio::where('empresa_id', $item->empresa_id)
|
||||
->first();
|
||||
|
||||
if($item->comanda < 10){
|
||||
$item->comanda = "00$item->comanda";
|
||||
}elseif($item->comanda >= 10 && $item->comanda < 99){
|
||||
$item->comanda = "0$item->comanda";
|
||||
}else{
|
||||
$item->comanda = "$item->comanda";
|
||||
}
|
||||
|
||||
foreach($item->itens as $i){
|
||||
$i->adds = $i->adicionais->pluck('adicional_id')->toArray();
|
||||
$i->pizzas = $i->pizzas->pluck('produto_id')->toArray();
|
||||
}
|
||||
|
||||
$item->taxa_servico = 0;
|
||||
if($config){
|
||||
$item->taxa_servico = $config->percentual_taxa_servico;
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function produtos(Request $request){
|
||||
|
||||
$data = Produto::where('empresa_id', $request->empresa_id)
|
||||
->where('cardapio', 1)
|
||||
->with(['categoria', 'variacoes'])
|
||||
->select('id', 'nome', 'valor_cardapio', 'categoria_id', 'valor_unitario', 'codigo_barras')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function produto(Request $request){
|
||||
|
||||
$item = Produto::
|
||||
with(['adicionais', 'categoria'])
|
||||
->findOrFail($request->produto_id);
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
private function validaItemImpressao($produto_id){
|
||||
$imprime = ImpressoraPedidoProduto::where('produto_id', $produto_id)->first();
|
||||
return $imprime != null ? 0 : 1;
|
||||
}
|
||||
|
||||
public function destroy($id){
|
||||
$item = Pedido::findOrFail($id);
|
||||
|
||||
if($item->_mesa){
|
||||
$mesa = $item->_mesa;
|
||||
$mesa->ocupada = 0;
|
||||
$mesa->save();
|
||||
}
|
||||
try {
|
||||
foreach($item->itens as $it){
|
||||
$it->adicionais()->delete();
|
||||
$it->pizzas()->delete();
|
||||
$it->delete();
|
||||
}
|
||||
$item->notificacoes()->delete();
|
||||
$item->delete();
|
||||
|
||||
return response()->json("ok", 200);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json($e->getMessage(), 403);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeItem(Request $request){
|
||||
$produto = Produto::findOrFail($request->produto_id);
|
||||
$pedido = Pedido::findOrFail($request->pedido_id);
|
||||
try{
|
||||
$codigo_garcom = $request->codigo_operador;
|
||||
$funcionario = null;
|
||||
if($codigo_garcom){
|
||||
$funcionario = Funcionario::where('empresa_id', $request->empresa_id)
|
||||
->where('codigo', $codigo_garcom)->first();
|
||||
}
|
||||
|
||||
$valorUnitario = $produto->valor_unitario;
|
||||
if($produto->valor_cardapio > 0){
|
||||
$valorUnitario = $produto->valor_cardapio;
|
||||
}
|
||||
$impresso = $this->validaItemImpressao($request->produto_id);
|
||||
|
||||
$data = [
|
||||
'pedido_id' => $request->pedido_id,
|
||||
'produto_id' => $request->produto_id,
|
||||
'observacao' => $request->observacao,
|
||||
'estado' => 'novo',
|
||||
'quantidade' => $request->quantidade,
|
||||
'valor_unitario' => $valorUnitario,
|
||||
'sub_total' => __convert_value_bd($request->sub_total),
|
||||
'tamanho_id' => $request->tamanho_id,
|
||||
'funcionario_id' => $funcionario ? $funcionario->id : null,
|
||||
'impresso' => $impresso
|
||||
];
|
||||
$itemPedido = ItemPedido::create($data);
|
||||
foreach($request->adicionais as $a){
|
||||
ItemAdicional::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'adicional_id' => $a
|
||||
]);
|
||||
}
|
||||
|
||||
if($request->sabores && sizeof($request->sabores) >= 1){
|
||||
ItemPizzaPedido::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'produto_id' => $request->produto_id
|
||||
]);
|
||||
}
|
||||
foreach($request->sabores as $s){
|
||||
ItemPizzaPedido::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'produto_id' => $s
|
||||
]);
|
||||
}
|
||||
|
||||
$config = ConfiguracaoCardapio::where('empresa_id', $pedido->empresa_id)
|
||||
->first();
|
||||
|
||||
$pedido->total = $pedido->itens->sum('sub_total');
|
||||
|
||||
if($config && $config->percentual_taxa_servico){
|
||||
$pedido->acrescimo = $pedido->total * ($config->percentual_taxa_servico/100);
|
||||
}
|
||||
|
||||
$pedido->save();
|
||||
return response()->json($pedido, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 403);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeItensClear(Request $request){
|
||||
|
||||
$pedido = Pedido::findOrFail($request->pedido_id);
|
||||
|
||||
foreach($pedido->itens as $item){
|
||||
$item->adicionais()->delete();
|
||||
$item->pizzas()->delete();
|
||||
$item->delete();
|
||||
}
|
||||
|
||||
try{
|
||||
|
||||
$soma = 0;
|
||||
foreach($request->itens as $item){
|
||||
|
||||
$codigo_garcom = $item['codigo_operador'];
|
||||
|
||||
$produto = Produto::findOrFail($item['produto_id']);
|
||||
$funcionario = null;
|
||||
if($codigo_garcom){
|
||||
$funcionario = Funcionario::where('empresa_id', $request->empresa_id)
|
||||
->where('codigo', $codigo_garcom)->first();
|
||||
}
|
||||
|
||||
$valorUnitario = $produto->valor_unitario;
|
||||
if($produto->valor_cardapio > 0){
|
||||
$valorUnitario = $produto->valor_cardapio;
|
||||
}
|
||||
$impresso = $this->validaItemImpressao($produto->id);
|
||||
|
||||
$data = [
|
||||
'pedido_id' => $pedido->id,
|
||||
'produto_id' => $item['produto_id'],
|
||||
'observacao' => $item['observacao'],
|
||||
'estado' => 'novo',
|
||||
'quantidade' => $item['quantidade'],
|
||||
'valor_unitario' => $valorUnitario,
|
||||
'sub_total' => __convert_value_bd($item['sub_total']),
|
||||
'tamanho_id' => $item['tamanho_id'],
|
||||
'funcionario_id' => $funcionario ? $funcionario->id : null,
|
||||
'impresso' => $impresso
|
||||
];
|
||||
|
||||
$soma += __convert_value_bd($item['sub_total']);
|
||||
$itemPedido = ItemPedido::create($data);
|
||||
foreach($item['adicionais'] as $a){
|
||||
ItemAdicional::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'adicional_id' => $a
|
||||
]);
|
||||
}
|
||||
|
||||
if($item['sabores'] && sizeof($item['sabores']) >= 1){
|
||||
ItemPizzaPedido::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'produto_id' => $request->produto_id
|
||||
]);
|
||||
}
|
||||
foreach($item['sabores'] as $s){
|
||||
ItemPizzaPedido::create([
|
||||
'item_pedido_id' => $itemPedido->id,
|
||||
'produto_id' => $s
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$pedido->total = $soma;
|
||||
$pedido->save();
|
||||
return response()->json($pedido, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 403);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeItem(Request $request){
|
||||
$item = ItemPedido::findOrfail($request->item_id);
|
||||
try{
|
||||
$pedido = $item->pedido;
|
||||
$item->adicionais()->delete();
|
||||
$item->pizzas()->delete();
|
||||
$item->delete();
|
||||
$pedido->total = $pedido->itens->sum('sub_total');
|
||||
$pedido->save();
|
||||
|
||||
return response()->json("Item removido!", 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 403);
|
||||
}
|
||||
}
|
||||
|
||||
public function fecharComanda(Request $request){
|
||||
$pedido = Pedido::findOrFail($request->comanda_id);
|
||||
$pedido->em_atendimento = 0;
|
||||
$pedido->save();
|
||||
return response()->json($pedido, 200);
|
||||
}
|
||||
|
||||
public function openComanda(Request $request){
|
||||
$cliente_id = $request->cliente_id;
|
||||
$clienteNome = $request->nome;
|
||||
$clienteFone = $request->telefone;
|
||||
$comanda = $request->comanda;
|
||||
$mesa = $request->mesa;
|
||||
|
||||
$item = Pedido::where('status', 1)
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->where('comanda', $comanda)
|
||||
->first();
|
||||
|
||||
if($item != null){
|
||||
return response()->json('Comanda já está aberta', 403);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'cliente_id' => $cliente_id,
|
||||
'cliente_nome' => $clienteNome,
|
||||
'cliente_fone' => $clienteFone,
|
||||
'comanda' => $comanda,
|
||||
'mesa' => $mesa,
|
||||
'total' => 0,
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'local_pedido' => 'App'
|
||||
];
|
||||
|
||||
$pedido = Pedido::create($data);
|
||||
|
||||
return response()->json($pedido, 200);
|
||||
}
|
||||
|
||||
public function getTamanhos(Request $request){
|
||||
$data = TamanhoPizza::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function getSabores(Request $request){
|
||||
$sabores = Produto::orderBy('produtos.nome', 'desc')
|
||||
->select('produtos.*')
|
||||
->where('produtos.empresa_id', $request->empresa_id)
|
||||
->where('produtos.cardapio',1)
|
||||
->where('produtos.id', '!=', $request->sabor_principal)
|
||||
->join('categoria_produtos', 'categoria_produtos.id', '=', 'produtos.categoria_id')
|
||||
->where('categoria_produtos.tipo_pizza', 1)
|
||||
->with(['pizzaValores'])
|
||||
->get();
|
||||
|
||||
foreach($sabores as $p){
|
||||
$produtoPizzaValor = ProdutoPizzaValor::where('produto_id', $p->id)
|
||||
->where('tamanho_id', $request->tamanho_id)->first();
|
||||
if($produtoPizzaValor){
|
||||
$p->valor = $produtoPizzaValor->valor;
|
||||
}
|
||||
$p->checked = false;
|
||||
}
|
||||
|
||||
$tamanho = TamanhoPizza::findOrFail($request->tamanho_id);
|
||||
|
||||
$data = [
|
||||
'tamanho' => $tamanho,
|
||||
'sabores' => $sabores
|
||||
];
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function valorPizza(Request $request){
|
||||
$tamanho_id = $request->tamanho_id;
|
||||
$empresa_id = $request->empresa_id;
|
||||
$sabores = $request->sabores;
|
||||
$soma = 0;
|
||||
$maiorValor = 0;
|
||||
|
||||
$config = MarketPlaceConfig::where('empresa_id', $empresa_id)->first();
|
||||
foreach($sabores as $s){
|
||||
$produtoPizzaValor = ProdutoPizzaValor::where('produto_id', $s)
|
||||
->where('tamanho_id', $tamanho_id)->first();
|
||||
if($produtoPizzaValor){
|
||||
$soma += $produtoPizzaValor->valor;
|
||||
if($produtoPizzaValor->valor > $maiorValor){
|
||||
$maiorValor = $produtoPizzaValor->valor;
|
||||
}
|
||||
}
|
||||
}
|
||||
$valor = $maiorValor;
|
||||
if($config->tipo_divisao_pizza == 'divide'){
|
||||
$valor = $soma/sizeof($sabores);
|
||||
}
|
||||
|
||||
$valor = number_format($valor, 2);
|
||||
return response()->json($valor, 200);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\ConfiguracaoCardapio;
|
||||
|
||||
class ConfigController extends Controller
|
||||
{
|
||||
public function setConfig(Request $request){
|
||||
|
||||
$item = ConfiguracaoCardapio::where('api_token', $request->token)
|
||||
->first();
|
||||
|
||||
if($item == null){
|
||||
return response()->json("Configuração não encontrada!", 401);
|
||||
}
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $item->empresa_id)
|
||||
->first();
|
||||
|
||||
if($funcionario == null){
|
||||
return response()->json("Funcionário não encontrado " . $request->codigo_operador, 401);
|
||||
}
|
||||
|
||||
$item->app_name = env("APP_NAME");
|
||||
$item->app_description = env("APP_DESCRIPTION");
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Models\CategoriaConta;
|
||||
use App\Models\Localizacao;
|
||||
|
||||
class ContaReceberController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
$data = ContaReceber::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'cliente_id', 'descricao', 'valor_integral', 'valor_recebido', 'status', 'data_vencimento', 'data_recebimento',
|
||||
'observacao', 'tipo_pagamento', 'categoria_conta_id', 'created_at')
|
||||
->with(['cliente', 'categoria'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function categorias(Request $request){
|
||||
$data = CategoriaConta::where('empresa_id', $request->empresa_id)
|
||||
->select('id', 'nome')
|
||||
->orderBy('nome', 'asc')
|
||||
->where('status', 1)
|
||||
->where('tipo', 'receber')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try {
|
||||
|
||||
$local = Localizacao::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)->first();
|
||||
|
||||
$request->merge([
|
||||
'valor_integral' => $request->valor,
|
||||
'valor_original' => $request->valor,
|
||||
'valor_recebido' => $request->status ? $request->valor : 0,
|
||||
'data_recebimento' => $request->status ? $request->data_vencimento : null,
|
||||
'local_id' => $local->id
|
||||
]);
|
||||
|
||||
$conta = ContaReceber::create($request->all());
|
||||
return response()->json($conta, 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
__createLog($request->empresa_id, 'Conta a Receber', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id){
|
||||
try {
|
||||
$item = ContaReceber::findOrFail($id);
|
||||
|
||||
$request->merge([
|
||||
'valor_integral' => $request->valor,
|
||||
'valor_original' => $request->valor,
|
||||
'valor_recebido' => $request->status ? $request->valor : 0,
|
||||
'data_recebimento' => $request->status ? $request->data_vencimento : null,
|
||||
]);
|
||||
|
||||
$item->fill($request->all())->save();
|
||||
return response()->json($item, 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
__createLog($request->empresa_id, 'Conta a Receber', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function receive(Request $request){
|
||||
try {
|
||||
$item = ContaReceber::findOrFail($request->conta_id);
|
||||
|
||||
$item->status = 1;
|
||||
$item->valor_recebido = $request->valor;
|
||||
$item->data_recebimento = $request->data_recebimento;
|
||||
$item->tipo_pagamento = $request->tipo_pagamento;
|
||||
|
||||
$item->save();
|
||||
return response()->json($item, 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id){
|
||||
$item = ContaReceber::findOrFail($id);
|
||||
|
||||
try{
|
||||
$item->delete();
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
__createLog($item->empresa_id, 'Conta a Receber', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Nfce;
|
||||
use App\Models\ItemNfce;
|
||||
use App\Models\FaturaNfce;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\User;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\Produto;
|
||||
use App\Models\SuprimentoCaixa;
|
||||
use App\Models\SangriaCaixa;
|
||||
use App\Models\Caixa;
|
||||
use App\Models\ItemPedido;
|
||||
use App\Models\Mesa;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Utils\EstoqueUtil;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Pedido;
|
||||
use Dompdf\Dompdf;
|
||||
use NFePHP\DA\NFe\Danfce;
|
||||
use App\Models\VendaSuspensa;
|
||||
|
||||
class FrontboxController extends Controller
|
||||
{
|
||||
|
||||
protected $util;
|
||||
|
||||
public function __construct(EstoqueUtil $util)
|
||||
{
|
||||
$this->util = $util;
|
||||
}
|
||||
|
||||
public function tiposDePagamento(Request $request){
|
||||
|
||||
try{
|
||||
$tiposPagamento = Nfce::tiposPagamento();
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', $request->empresa_id)->first();
|
||||
$tiposPagamento = Nfce::tiposPagamento();
|
||||
|
||||
if($config != null){
|
||||
$config->tipos_pagamento_pdv = $config != null && $config->tipos_pagamento_pdv ? json_decode($config->tipos_pagamento_pdv) : [];
|
||||
$temp = [];
|
||||
if(sizeof($config->tipos_pagamento_pdv) > 0){
|
||||
foreach($tiposPagamento as $key => $t){
|
||||
if(in_array($t, $config->tipos_pagamento_pdv)){
|
||||
$temp[$key] = $t;
|
||||
}
|
||||
}
|
||||
$tiposPagamento = $temp;
|
||||
}
|
||||
}
|
||||
$data = [];
|
||||
|
||||
array_push($data, [
|
||||
'id' => '',
|
||||
'nome' => 'Selecione'
|
||||
]);
|
||||
foreach($tiposPagamento as $key => $t){
|
||||
array_push($data, [
|
||||
'id' => $key,
|
||||
'nome' => $t
|
||||
]);
|
||||
}
|
||||
return response()->json($data, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
private function getLastNumero($empresa_id){
|
||||
$last = Nfce::where('empresa_id', $empresa_id)
|
||||
->orderBy('numero_sequencial', 'desc')
|
||||
->where('numero_sequencial', '>', 0)->first();
|
||||
$numero = $last != null ? $last->numero_sequencial : 0;
|
||||
$numero++;
|
||||
return $numero;
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
$nfce = DB::transaction(function () use ($request) {
|
||||
|
||||
$empresa = Empresa::find($request->empresa_id);
|
||||
|
||||
$numero_nfce = $empresa->numero_ultima_nfce_producao+1;
|
||||
if ($empresa->ambiente == 2) {
|
||||
$numero_nfce = $empresa->numero_ultima_nfce_homologacao+1;
|
||||
}
|
||||
|
||||
$total = __convert_value_bd(str_replace("R$ ", "", $request->total));
|
||||
$desconto = __convert_value_bd(str_replace("R$ ", "", $request->desconto));
|
||||
$acrescimo = __convert_value_bd(str_replace("R$ ", "", $request->acrescimo));
|
||||
$valor_recebido = __convert_value_bd(str_replace("R$ ", "", $request->valor_recebido));
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$caixa = Caixa::where('usuario_id', $funcionario->usuario_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if($caixa == null){
|
||||
return response()->json("Caixa está fechado!", 401);
|
||||
}
|
||||
|
||||
$objetoVenda = [
|
||||
'cliente_id' => $request->cliente,
|
||||
'natureza_id' => $empresa->natureza_id_pdv,
|
||||
'ambiente' => $empresa->ambiente,
|
||||
'total' => $total,
|
||||
'desconto' => $desconto,
|
||||
'acrescimo' => $acrescimo,
|
||||
'estado' => 'novo',
|
||||
'numero' => $numero_nfce,
|
||||
'chave' => '',
|
||||
'observacao' => $request->observacao,
|
||||
'local_id' => $caixa->local_id,
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'numero_serie' => $empresa->numero_serie_nfce ?? 1,
|
||||
'cliente_cpf_cnpj' => $request->cpf_nota,
|
||||
'cliente_nome' => '',
|
||||
'troco' => $valor_recebido > 0 ? ($valor_recebido - $total) : 0,
|
||||
|
||||
'emissor_nome' => $empresa->nome,
|
||||
'emissor_cpf_cnpj' => $empresa->cpf_cnpj,
|
||||
|
||||
'caixa_id' => $caixa->id,
|
||||
'local_id' => $caixa->local_id,
|
||||
'user_id' => $funcionario->usuario_id,
|
||||
'numero_sequencial' => $this->getLastNumero($request->empresa_id)
|
||||
];
|
||||
|
||||
$nfce = Nfce::create($objetoVenda);
|
||||
|
||||
foreach($request->itens as $item){
|
||||
$product = Produto::findOrFail($item['produto_id']);
|
||||
|
||||
$variacao_id = isset($item['variacao_id']) ? $item['variacao_id'] : null;
|
||||
ItemNfce::create([
|
||||
'nfce_id' => $nfce->id,
|
||||
'produto_id' => $product->id,
|
||||
'quantidade' => $item['quantidade'],
|
||||
'valor_unitario' => $product->valor_unitario,
|
||||
'sub_total' => $item['sub_total'],
|
||||
'perc_icms' => __convert_value_bd($product->perc_icms),
|
||||
'perc_pis' => __convert_value_bd($product->perc_pis),
|
||||
'perc_cofins' => __convert_value_bd($product->perc_cofins),
|
||||
'perc_ipi' => __convert_value_bd($product->perc_ipi),
|
||||
'cst_csosn' => $product->cst_csosn,
|
||||
'cst_pis' => $product->cst_pis,
|
||||
'cst_cofins' => $product->cst_cofins,
|
||||
'cst_ipi' => $product->cst_ipi,
|
||||
'cfop' => $product->cfop_estadual,
|
||||
'ncm' => $product->ncm,
|
||||
'variacao_id' => $variacao_id,
|
||||
]);
|
||||
|
||||
if ($product->gerenciar_estoque) {
|
||||
$this->util->reduzEstoque($product->id, __convert_value_bd($request['quantidade']), $variacao_id, $caixa->local_id);
|
||||
|
||||
$tipo = 'reducao';
|
||||
$codigo_transacao = $nfce->id;
|
||||
$tipo_transacao = 'venda_nfce';
|
||||
|
||||
$this->util->movimentacaoProduto($product->id, __convert_value_bd($request['quantidade']), $tipo, $codigo_transacao, $tipo_transacao, $funcionario->usuario_id, $variacao_id);
|
||||
}
|
||||
}
|
||||
|
||||
if(sizeof($request->fatura) > 0){
|
||||
foreach($request->fatura as $i => $fatura){
|
||||
FaturaNfce::create([
|
||||
'nfce_id' => $nfce->id,
|
||||
'tipo_pagamento' => $fatura['tipo_pagamento'],
|
||||
'data_vencimento' => $fatura['vencimento'],
|
||||
'valor' => $fatura['valor']
|
||||
]);
|
||||
|
||||
$vencimento = $fatura['vencimento'];
|
||||
$dataAtual = date('Y-m-d');
|
||||
|
||||
if(strtotime($vencimento) > strtotime($dataAtual)){
|
||||
ContaReceber::create([
|
||||
'nfe_id' => null,
|
||||
'nfce_id' => $nfce->id,
|
||||
'cliente_id' => $request->cliente,
|
||||
'data_vencimento' => $fatura['vencimento'],
|
||||
'data_recebimento' => $fatura['vencimento'],
|
||||
'valor_integral' => $fatura['valor'],
|
||||
'valor_recebido' => 0,
|
||||
'status' => 0,
|
||||
'referencia' => "Parcela $i+1 da venda código $nfce->id",
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'juros' => 0,
|
||||
'multa' => 0,
|
||||
'observacao' => '',
|
||||
'tipo_pagamento' => $fatura['tipo_pagamento'],
|
||||
'local_id' => $caixa->local_id
|
||||
]);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
FaturaNfce::create([
|
||||
'nfce_id' => $nfce->id,
|
||||
'tipo_pagamento' => $request->tipo_pagamento,
|
||||
'data_vencimento' => date('Y-m-d'),
|
||||
'valor' => $nfce->total
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->pedido_id) {
|
||||
|
||||
$pedido = Pedido::findOrfail($request->pedido_id);
|
||||
$pedido->status = 0;
|
||||
$pedido->em_atendimento = 0;
|
||||
$pedido->nfce_id = $nfce->id;
|
||||
|
||||
Mesa::where('id', $pedido->mesa_id)->update(['ocupada' => 0]);
|
||||
|
||||
ItemPedido::where('pedido_id', $pedido->id)
|
||||
->update([ 'estado' => 'finalizado' ]);
|
||||
$pedido->save();
|
||||
|
||||
}
|
||||
|
||||
if ($request->venda_suspensa_id) {
|
||||
$vendaSuspensa = VendaSuspensa::findOrfail($request->venda_suspensa_id);
|
||||
$vendaSuspensa->itens()->delete();
|
||||
$vendaSuspensa->delete();
|
||||
}
|
||||
|
||||
return $nfce;
|
||||
});
|
||||
|
||||
return response()->json($nfce, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function caixa(Request $request){
|
||||
try{
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$item = Caixa::where('usuario_id', $funcionario->usuario_id)->where('status', 1)
|
||||
->first();
|
||||
|
||||
$vendas = Nfce::where('caixa_id', $item->id)
|
||||
->with(['itens', 'cliente', 'fatura'])
|
||||
->orderBy('id', 'desc')
|
||||
->select('numero_sequencial', 'total', 'cliente_id', 'desconto', 'acrescimo', 'created_at', 'estado', 'id')
|
||||
->get();
|
||||
|
||||
foreach($vendas as $v){
|
||||
$v->tipo_pagamento = Nfce::getTipoPagamento($v->tipo_pagamento);
|
||||
foreach($v->fatura as $f){
|
||||
$f->tipo_pagamento_str = Nfce::getTipoPagamento($f->tipo_pagamento);
|
||||
}
|
||||
}
|
||||
|
||||
$suprimentos = SuprimentoCaixa::where('caixa_id', $item->id)
|
||||
->get();
|
||||
foreach($suprimentos as $v){
|
||||
$v->tipo_pagamento = Nfce::getTipoPagamento($v->tipo_pagamento);
|
||||
}
|
||||
|
||||
$sangrias = SangriaCaixa::where('caixa_id', $item->id)
|
||||
->get();
|
||||
// foreach($sangrias as $v){
|
||||
// $v->tipo_pagamento = Nfce::getTipoPagamento($v->tipo_pagamento);
|
||||
// }
|
||||
|
||||
$totalDeVendas = $vendas->sum('total');
|
||||
$totalSangrias = $sangrias->sum('valor');
|
||||
$totalSuprimentos = $suprimentos->sum('valor');
|
||||
$data = [
|
||||
'valor_abertura' => $item->valor_abertura,
|
||||
'observacao' => $item->observacao,
|
||||
'data_abertura' => $item->created_at,
|
||||
'vendas' => $vendas,
|
||||
'status' => $item->status,
|
||||
'suprimentos' => $suprimentos,
|
||||
'sangrias' => $sangrias,
|
||||
'totalDeVendas' => $totalDeVendas,
|
||||
'totalSangrias' => $totalSangrias,
|
||||
'totalSuprimentos' => $totalSuprimentos,
|
||||
];
|
||||
|
||||
return response()->json($data, 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function sangriaStore(Request $request){
|
||||
try{
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$caixa = Caixa::where('usuario_id', $funcionario->usuario_id)->where('status', 1)
|
||||
->first();
|
||||
|
||||
$valor = __convert_value_bd(str_replace("R$ ", "", $request->valor));
|
||||
|
||||
$sangria = SangriaCaixa::create([
|
||||
'caixa_id' => $caixa->id,
|
||||
'valor' => $valor,
|
||||
'observacao' => $request->observacao ?? '',
|
||||
'conta_empresa_id' => null
|
||||
]);
|
||||
|
||||
return response()->json("ok", 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function suprimentoStore(Request $request){
|
||||
try{
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$caixa = Caixa::where('usuario_id', $funcionario->usuario_id)->where('status', 1)
|
||||
->first();
|
||||
|
||||
$valor = __convert_value_bd(str_replace("R$ ", "", $request->valor));
|
||||
|
||||
$sangria = SuprimentoCaixa::create([
|
||||
'caixa_id' => $caixa->id,
|
||||
'valor' => $valor,
|
||||
'tipo_pagamento' => $request->tipo_pagamento,
|
||||
'observacao' => $request->observacao ?? '',
|
||||
'conta_empresa_id' => null
|
||||
]);
|
||||
|
||||
return response()->json("ok", 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function fecharCaixa(Request $request){
|
||||
try{
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$item = Caixa::where('usuario_id', $funcionario->usuario_id)->where('status', 1)
|
||||
->first();
|
||||
|
||||
$item->status = 0;
|
||||
$item->data_fechamento = date('Y-m-d h:i:s');
|
||||
|
||||
$item->save();
|
||||
|
||||
return response()->json("ok", 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function abrirCaixa(Request $request){
|
||||
try{
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if(!$funcionario->usuario_id){
|
||||
return response()->json("Usuário não vinculado ao funcionário!", 401);
|
||||
}
|
||||
|
||||
$valor = __convert_value_bd(str_replace("R$ ", "", $request->valor));
|
||||
|
||||
$local_id = null;
|
||||
$user = User::findOrFail($funcionario->usuario_id);
|
||||
if(!$request->local_id){
|
||||
if(sizeof($user->locais) > 0){
|
||||
$local_id = $user->locais[0]->localizacao_id;
|
||||
}
|
||||
}else{
|
||||
$local_id = $request->local_id;
|
||||
}
|
||||
$data = [
|
||||
'usuario_id' => $funcionario->usuario_id,
|
||||
'valor_abertura' => $valor ?? 0,
|
||||
'observacao' => $request->observacao ?? '',
|
||||
'conta_empresa_id' => null,
|
||||
'local_id' => $local_id,
|
||||
'status' => 1,
|
||||
'valor_fechamento' => 0,
|
||||
'empresa_id' => $request->empresa_id
|
||||
];
|
||||
$item = Caixa::create($data);
|
||||
|
||||
return response()->json("ok", 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function cupomNaoFiscal(Request $request){
|
||||
$item = Nfce::findOrFail($request->id);
|
||||
try{
|
||||
if (!is_dir(public_path('comprovantes_pdv'))) {
|
||||
mkdir(public_path('comprovantes_pdv'), 0777, true);
|
||||
}else{
|
||||
$destino = public_path('comprovantes_pdv');
|
||||
$this->clearFolder($destino);
|
||||
}
|
||||
$config = Empresa::where('id', $item->empresa_id)
|
||||
->first();
|
||||
|
||||
$configGeral = ConfigGeral::where('empresa_id', $item->empresa_id)->first();
|
||||
$p = view('front_box.cupom_nao_fiscal', compact('config', 'item', 'configGeral'));
|
||||
|
||||
$domPdf = new Dompdf(["enable_remote" => true]);
|
||||
$domPdf->loadHtml($p);
|
||||
$pdf = ob_get_clean();
|
||||
$height = 320;
|
||||
|
||||
$height += sizeof($item->itens)*11;
|
||||
|
||||
if($item->observacao != ''){
|
||||
$height += 30;
|
||||
}
|
||||
|
||||
if($configGeral->mensagem_padrao_impressao_venda != ''){
|
||||
$height += 30;
|
||||
}
|
||||
|
||||
$domPdf->setPaper([0,0,244,$height]);
|
||||
$domPdf->render();
|
||||
|
||||
$fileName = "comprovante_$item->id.pdf";
|
||||
file_put_contents(public_path('comprovantes_pdv/') . $fileName , $domPdf->output());
|
||||
|
||||
$url = env("APP_URL"). "/comprovantes_pdv/".$fileName;
|
||||
return response()->json($url, 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function cupomFiscal(Request $request){
|
||||
$item = Nfce::findOrFail($request->id);
|
||||
try{
|
||||
|
||||
if (!is_dir(public_path('comprovantes_pdv'))) {
|
||||
mkdir(public_path('comprovantes_pdv'), 0777, true);
|
||||
}else{
|
||||
$destino = public_path('comprovantes_pdv');
|
||||
$this->clearFolder($destino);
|
||||
}
|
||||
|
||||
if (file_exists(public_path('xml_nfce/') . $item->chave . '.xml')) {
|
||||
$xml = file_get_contents(public_path('xml_nfce/') . $item->chave . '.xml');
|
||||
$danfe = new Danfce($xml, $item);
|
||||
$empresa = $item->empresa;
|
||||
$empresa = __objetoParaEmissao($empresa, $item->local_id);
|
||||
|
||||
if($empresa->logo){
|
||||
$logo = 'data://text/plain;base64,'. base64_encode(file_get_contents(public_path('/uploads/logos/') . $empresa->logo));
|
||||
$danfe->logoParameters($logo, 'L');
|
||||
}
|
||||
$pdf = $danfe->render();
|
||||
|
||||
$fileName = "comprovante_$item->id.pdf";
|
||||
file_put_contents(public_path('comprovantes_pdv/') . $fileName , $pdf);
|
||||
|
||||
$url = env("APP_URL"). "/comprovantes_pdv/".$fileName;
|
||||
|
||||
return response()->json($url, 200);
|
||||
|
||||
} else {
|
||||
return response()->json("Arquivo não encontrado", 401);
|
||||
}
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
private function clearFolder($destino){
|
||||
$files = glob($destino."/*");
|
||||
foreach($files as $file){
|
||||
if(is_file($file)) unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Nfe;
|
||||
use App\Models\Nfce;
|
||||
use App\Models\FaturaNfe;
|
||||
use App\Models\FaturaNfce;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Produto;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\ContaReceber;
|
||||
use App\Models\Localizacao;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
$empresa_id = $request->empresa_id;
|
||||
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $empresa_id)->first();
|
||||
|
||||
$locais = null;
|
||||
if($funcionario->usuario){
|
||||
$usuario_id = $funcionario->usuario_id;
|
||||
$locais = Localizacao::where('usuario_localizacaos.usuario_id', $usuario_id)
|
||||
->select('localizacaos.*')
|
||||
->join('usuario_localizacaos', 'usuario_localizacaos.localizacao_id', '=', 'localizacaos.id')
|
||||
->where('localizacaos.status', 1)->get();
|
||||
$locais = $locais->pluck(['id']);
|
||||
}
|
||||
|
||||
|
||||
$totalProdutos = Produto::
|
||||
where('empresa_id', $empresa_id)
|
||||
->select('produtos.*')
|
||||
->count();
|
||||
|
||||
$totalClientes = Cliente::
|
||||
where('empresa_id', $empresa_id)
|
||||
->count();
|
||||
|
||||
$somaVendas = Nfe::
|
||||
where('empresa_id', $empresa_id)
|
||||
->where('estado', '!=', 'cancelado')
|
||||
->where('tpNF', 1)
|
||||
->where('orcamento', 0)
|
||||
// ->whereDate('created_at', '>=', date('Y-m-01'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', date('Y'))
|
||||
// ->when($locais != null, function ($query) use ($locais) {
|
||||
// return $query->whereIn('local_id', $locais);
|
||||
// })
|
||||
->sum('total');
|
||||
|
||||
$somaVendasPdv = Nfce::
|
||||
where('empresa_id', $empresa_id)
|
||||
->where('estado', '!=', 'cancelado')
|
||||
// ->whereDate('created_at', '>=', date('Y-m-01'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', date('Y'))
|
||||
// ->when($locais != null, function ($query) use ($locais) {
|
||||
// return $query->whereIn('local_id', $locais);
|
||||
// })
|
||||
->sum('total');
|
||||
|
||||
$somaContaReceber = ContaReceber::
|
||||
where('empresa_id', $empresa_id)
|
||||
->where('status', 0)
|
||||
->whereMonth('data_vencimento', date('m'))
|
||||
->when($locais != null, function ($query) use ($locais) {
|
||||
return $query->whereIn('local_id', $locais);
|
||||
})
|
||||
->sum('valor_integral');
|
||||
|
||||
$somaVendasMeses = $this->somaVendasMeses($empresa_id);
|
||||
$somaUltimasVendas = $this->somaUltimasVendas($empresa_id);
|
||||
$somaTiposPagamento = $this->somaTiposPagamento($empresa_id);
|
||||
|
||||
$data = [
|
||||
'total_produtos' => $totalProdutos,
|
||||
'total_clientes' => $totalClientes,
|
||||
'soma_vendas' => $somaVendas + $somaVendasPdv,
|
||||
'contas_receber' => $somaContaReceber,
|
||||
'soma_vendas_meses' => $somaVendasMeses,
|
||||
'soma_ultimas_vendas' => $somaUltimasVendas,
|
||||
'soma_tipos_pagamento' => $somaTiposPagamento,
|
||||
'usuario_nome' => $funcionario->usuario->name
|
||||
];
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
private function somaTiposPagamento($empresa_id){
|
||||
$data = [];
|
||||
$tiposPagamento = Nfce::tiposPagamento();
|
||||
$cores = [
|
||||
'#3F51B5', '#9C27B0', '#FF9800', '#009688', '#4CAF50', '#E91E63', '#FF5722',
|
||||
'#2196F3', '#795548', '#CDDC39', '#673AB7', '#F44336', '#00BCD4', '#8BC34A'
|
||||
];
|
||||
$cont = 0;
|
||||
foreach($tiposPagamento as $key => $tipo){
|
||||
$valorNfe = FaturaNfe::select('fatura_nves.*')
|
||||
->join('nves', 'nves.id', '=', 'fatura_nves.nfe_id')
|
||||
->where('nves.empresa_id', $empresa_id)
|
||||
->where('fatura_nves.tipo_pagamento', $key)
|
||||
->whereMonth('nves.created_at', date('m'))
|
||||
->whereYear('nves.created_at', date('Y'))
|
||||
->where('nves.tpNF', 1)
|
||||
->where('nves.orcamento', 0)
|
||||
->where('nves.estado', '!=', 'cancelado')
|
||||
|
||||
->sum('valor');
|
||||
|
||||
$valorNfce = FaturaNfce::select('fatura_nfces.*')
|
||||
->join('nfces', 'nfces.id', '=', 'fatura_nfces.nfce_id')
|
||||
->where('nfces.empresa_id', $empresa_id)
|
||||
->where('fatura_nfces.tipo_pagamento', $key)
|
||||
->whereMonth('nfces.created_at', date('m'))
|
||||
->whereYear('nfces.created_at', date('Y'))
|
||||
->where('nfces.estado', '!=', 'cancelado')
|
||||
->sum('valor');
|
||||
$tipo = $tiposPagamento[$key];
|
||||
if($tipo == 'Pagamento Instantâneo (PIX)'){
|
||||
$tipo = 'PIX';
|
||||
}
|
||||
if($valorNfce + $valorNfe > 0){
|
||||
$data[] = [
|
||||
'label' => $tipo,
|
||||
'value' => $valorNfe + $valorNfce,
|
||||
'color' => $cores[$cont]
|
||||
];
|
||||
$cont++;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function somaUltimasVendas($empresa_id){
|
||||
$dia = date('Y-m-d');
|
||||
$data = [];
|
||||
|
||||
$total = 0;
|
||||
|
||||
$cores = [
|
||||
'#3F51B5', '#9C27B0', '#FF9800', '#009688', '#4CAF50', '#E91E63', '#FF5722'
|
||||
];
|
||||
for($i=0; $i<7; $i++){
|
||||
$somaVendas = Nfe::
|
||||
where('empresa_id', $empresa_id)
|
||||
->where('estado', '!=', 'cancelado')
|
||||
->where('tpNF', 1)
|
||||
->where('orcamento', 0)
|
||||
->whereDate('created_at', $dia)
|
||||
->sum('total');
|
||||
|
||||
$somaVendasPdv = Nfce::
|
||||
where('empresa_id', $empresa_id)
|
||||
->where('estado', '!=', 'cancelado')
|
||||
->whereDate('created_at', $dia)
|
||||
->sum('total');
|
||||
|
||||
$total += $somaVendas + $somaVendasPdv;
|
||||
|
||||
if($somaVendas + $somaVendasPdv > 0){
|
||||
$data[] = [
|
||||
'label' => \Carbon\Carbon::parse($dia)->format('d/m'),
|
||||
'value' => $somaVendas + $somaVendasPdv,
|
||||
'color' => $cores[$i]
|
||||
];
|
||||
}
|
||||
|
||||
$dia = date('Y-m-d', strtotime('-1 day', strtotime($dia)));
|
||||
}
|
||||
|
||||
$retorno = [
|
||||
'total' => $total,
|
||||
'data' => $data
|
||||
];
|
||||
return $retorno;
|
||||
}
|
||||
|
||||
private function somaVendasMeses($empresa_id){
|
||||
$data = [];
|
||||
$mesAtual = (int)date('m')-1;
|
||||
$cont = 0;
|
||||
$i = 0;
|
||||
$meses = 4;
|
||||
$ano = date('Y');
|
||||
while($cont < $meses){
|
||||
if(isset($this->meses()[$mesAtual])){
|
||||
$mes = $this->meses()[$mesAtual];
|
||||
}else{
|
||||
$mes = 'Dezembro';
|
||||
$mesAtual = 12;
|
||||
$ano--;
|
||||
}
|
||||
|
||||
$totalNfe = Nfe::where('empresa_id', $empresa_id)
|
||||
->where('estado', '!=', 'cancelado')
|
||||
->whereMonth('created_at', $mesAtual+1)
|
||||
->whereYear('created_at', $ano)
|
||||
->where('tpNF', 1)
|
||||
->where('orcamento', 0)
|
||||
->sum('total');
|
||||
|
||||
$totalNfce = Nfce::where('empresa_id', $empresa_id)
|
||||
->where('estado', '!=', 'cancelado')
|
||||
->whereMonth('created_at', $mesAtual+1)
|
||||
->whereYear('created_at', $ano)
|
||||
->sum('total');
|
||||
|
||||
// $totalNfce = 0;
|
||||
$mesAtual--;
|
||||
|
||||
$cont++;
|
||||
$data[] = [
|
||||
'mes' => $mes,
|
||||
'valor' => $totalNfce + $totalNfe
|
||||
];
|
||||
}
|
||||
|
||||
$data = array_reverse($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function meses(){
|
||||
return [
|
||||
'Janeiro',
|
||||
'Fevereiro',
|
||||
'Março',
|
||||
'Abril',
|
||||
'Maio',
|
||||
'Junho',
|
||||
'Julho',
|
||||
'Agosto',
|
||||
'Setembro',
|
||||
'Outubro',
|
||||
'Novembro',
|
||||
'Dezembro',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\NFCeService;
|
||||
use App\Models\Nfce;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\UsuarioEmissao;
|
||||
|
||||
class NFCeController extends Controller
|
||||
{
|
||||
public function emitir(Request $request)
|
||||
{
|
||||
|
||||
$nfce = Nfce::findOrFail($request->id);
|
||||
$empresa = Empresa::findOrFail($nfce->empresa_id);
|
||||
|
||||
if ($empresa->arquivo == null) {
|
||||
return response()->json("Certificado não encontrado para este emitente", 401);
|
||||
}
|
||||
|
||||
$nfce_service = new NFCeService([
|
||||
"atualizacao" => date('Y-m-d h:i:s'),
|
||||
"tpAmb" => (int)$empresa->ambiente,
|
||||
"razaosocial" => $empresa->nome,
|
||||
"siglaUF" => $empresa->cidade->uf,
|
||||
"cnpj" => preg_replace('/[^0-9]/', '', $empresa->cpf_cnpj),
|
||||
"schemes" => "PL_009_V4",
|
||||
"versao" => "4.00",
|
||||
"CSC" => isset($documento['csc']) ? $documento['csc'] : $empresa->csc,
|
||||
"CSCid" => isset($documento['csc_id']) ? $documento['csc_id'] : $empresa->csc_id
|
||||
], $empresa);
|
||||
|
||||
try {
|
||||
|
||||
$doc = $nfce_service->gerarXml($nfce);
|
||||
if(!isset($doc['erros_xml'])){
|
||||
$xml = $doc['xml'];
|
||||
$chave = $doc['chave'];
|
||||
|
||||
$xmlTemp = simplexml_load_string($xml);
|
||||
|
||||
$itensComErro = "";
|
||||
$regime = $empresa->tributacao;
|
||||
foreach ($xmlTemp->infNFe->det as $item) {
|
||||
if (isset($item->imposto->ICMS)) {
|
||||
$icms = (array_values((array)$item->imposto->ICMS));
|
||||
if(sizeof($icms) == 0){
|
||||
$itensComErro .= " Produto " . $item->prod->xProd . " não formando a TAG ICMS, confira se o CST do item corresponde a tributação, regime configurado: $regime";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($itensComErro){
|
||||
return response()->json($itensComErro, 403);
|
||||
}
|
||||
|
||||
$signed = $nfce_service->sign($xml);
|
||||
|
||||
$resultado = $nfce_service->transmitir($signed, $doc['chave']);
|
||||
// $nfce->contigencia = $this->getContigencia($nfce->empresa_id);
|
||||
$nfce->reenvio_contigencia = 0;
|
||||
|
||||
if ($resultado['erro'] == 0) {
|
||||
$nfce->chave = $doc['chave'];
|
||||
$nfce->estado = 'aprovado';
|
||||
if($nfce->user){
|
||||
|
||||
$configUsuarioEmissao = UsuarioEmissao::where('usuario_empresas.empresa_id', $nfce->empresa_id)
|
||||
->join('usuario_empresas', 'usuario_empresas.usuario_id', '=', 'usuario_emissaos.usuario_id')
|
||||
->select('usuario_emissaos.*')
|
||||
->where('usuario_emissaos.usuario_id', $nfce->user_id)
|
||||
->first();
|
||||
if($configUsuarioEmissao == null){
|
||||
if($empresa->ambiente == 2){
|
||||
$empresa->numero_ultima_nfce_homologacao = $doc['numero'];
|
||||
}else{
|
||||
$empresa->numero_ultima_nfce_producao = $doc['numero'];
|
||||
}
|
||||
$empresa->save();
|
||||
}else{
|
||||
$configUsuarioEmissao->numero_ultima_nfce = $doc['numero'];
|
||||
$configUsuarioEmissao->save();
|
||||
}
|
||||
}
|
||||
|
||||
$nfce->numero = $doc['numero'];
|
||||
$nfce->recibo = $resultado['success'];
|
||||
$nfce->data_emissao = date('Y-m-d H:i:s');
|
||||
|
||||
$nfce->save();
|
||||
|
||||
$data = [
|
||||
'recibo' => $resultado['success'],
|
||||
'chave' => $nfce->chave
|
||||
];
|
||||
|
||||
$descricaoLog = "Emitida número $nfce->numero - $nfce->chave APROVADA";
|
||||
__createLog($nfce->empresa_id, 'NFCe', 'transmitir', $descricaoLog);
|
||||
|
||||
// try{
|
||||
// $fileDir = public_path('xml_nfce/').$nfce->chave.'.xml';
|
||||
// $this->emailUtil->enviarXmlContador($nfce->empresa_id, $fileDir, 'NFCe', $nfce->chave);
|
||||
// }catch(\Exception $e){
|
||||
|
||||
// }
|
||||
|
||||
// try{
|
||||
// $fileDir = public_path('xml_nfce/').$nfce->chave.'.xml';
|
||||
// $this->siegUtil->enviarXml($nfce->empresa_id, $fileDir);
|
||||
// }catch(\Exception $e){
|
||||
|
||||
// }
|
||||
|
||||
return response()->json($data, 200);
|
||||
}else{
|
||||
$recibo = isset($resultado['recibo']) ? $resultado['recibo'] : null;
|
||||
|
||||
$error = $resultado['error'];
|
||||
|
||||
if($nfce->chave == ''){
|
||||
$nfce->chave = $doc['chave'];
|
||||
}
|
||||
|
||||
if($nfce->signed_xml == null){
|
||||
$nfce->signed_xml = $signed;
|
||||
}
|
||||
if($nfce->recibo == null){
|
||||
$nfce->recibo = $recibo;
|
||||
}
|
||||
$nfce->estado = 'rejeitado';
|
||||
$nfce->save();
|
||||
|
||||
if(isset($error['protNFe'])){
|
||||
$motivo = $error['protNFe']['infProt']['xMotivo'];
|
||||
$cStat = $error['protNFe']['infProt']['cStat'];
|
||||
|
||||
$nfce->motivo_rejeicao = substr("[$cStat] $motivo", 0, 200);
|
||||
$nfce->save();
|
||||
|
||||
$descricaoLog = "REJEITADA $nfce->chave - $motivo";
|
||||
__createLog($nfce->empresa_id, 'NFCe', 'erro', $descricaoLog);
|
||||
|
||||
return response()->json("[$cStat] $motivo", 403);
|
||||
}else{
|
||||
return response()->json($error, 403);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
return response()->json($doc['erros_xml'], 401);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(" error: " . $e->getMessage() . ", line: " . $e->getLine(), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Nfe;
|
||||
use App\Models\ItemNfe;
|
||||
use App\Models\Produto;
|
||||
use App\Models\NaturezaOperacao;
|
||||
use App\Models\ConfigGeral;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\Localizacao;
|
||||
use App\Models\FaturaNfe;
|
||||
use App\Models\ListaPreco;
|
||||
|
||||
class OrcamentoController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
|
||||
$controlaId = $request->id > 0 ? $request->id : 9999999999999;
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_funcionario)
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
|
||||
$data = Nfe::where('empresa_id', $request->empresa_id)
|
||||
->where('orcamento', 1)
|
||||
->where('funcionario_id', $funcionario->id)
|
||||
->orderBy('id', 'desc')
|
||||
->with(['cliente', 'itens'])
|
||||
->limit(8)
|
||||
->where('id', '<', $controlaId)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function find(Request $request){
|
||||
|
||||
$item = Nfe::where('orcamento', 1)
|
||||
->with(['cliente', 'itens', 'fatura'])
|
||||
->findOrFail($request->orcamento_id);
|
||||
|
||||
foreach($item->itens as $i){
|
||||
$i->produto_nome = $i->produto->nome;
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function produtos(Request $request){
|
||||
|
||||
$data = Produto::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->orderBy('nome')
|
||||
->with(['categoria', 'variacoes', 'estoque'])
|
||||
->select('id', 'nome', 'valor_unitario', 'categoria_id', 'codigo_barras', 'unidade', 'referencia',
|
||||
'valor_prazo', 'gerenciar_estoque', 'imagem')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function listasDePreco(Request $request){
|
||||
|
||||
$data = ListaPreco::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)->orderBy('nome')
|
||||
->with(['itens'])
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function tiposDePagamento(Request $request){
|
||||
$tiposPagamento = Nfe::tiposPagamento();
|
||||
$data = [];
|
||||
foreach($tiposPagamento as $key => $t){
|
||||
$data[] = [
|
||||
'indice' => $key,
|
||||
'valor' => $t
|
||||
];
|
||||
}
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function dadosFuncionario(Request $request){
|
||||
$item = Funcionario::where('codigo', $request->codigo_funcionario)
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
|
||||
$config = ConfigGeral::where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
$item->app_valor_aprazo = 0;
|
||||
if($config != null){
|
||||
$item->app_valor_aprazo = $config->app_valor_aprazo;
|
||||
}
|
||||
return response()->json($item, 200);
|
||||
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
$cliente = Cliente::find($request->cliente_id);
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_funcionario)
|
||||
->where('empresa_id', $empresa->id)
|
||||
->first();
|
||||
if($funcionario == null){
|
||||
return response()->json("Funcionário não encontrado!", 404);
|
||||
}
|
||||
if($funcionario->usuario_id == null){
|
||||
return response()->json("Vincule o usuário ao funcionário $funcionario->nome", 401);
|
||||
}
|
||||
|
||||
$local = Localizacao::where('empresa_id', $empresa->id)
|
||||
->first();
|
||||
$naturezaPadrao = NaturezaOperacao::where('empresa_id', $request->empresa_id)
|
||||
->where('padrao', 1)->first();
|
||||
if($naturezaPadrao == null){
|
||||
$naturezaPadrao = NaturezaOperacao::where('empresa_id', $request->empresa_id)->first();
|
||||
}
|
||||
|
||||
$last = Nfe::where('empresa_id', $request->empresa_id)
|
||||
->orderBy('numero_sequencial', 'desc')
|
||||
// ->where('orcamento', 0)
|
||||
->where('numero_sequencial', '>', 0)->first();
|
||||
$numero = $last != null ? $last->numero_sequencial : 0;
|
||||
$numero++;
|
||||
|
||||
$data = [
|
||||
'cliente_id' => $cliente->id,
|
||||
'total' => __convert_value_bd($request->total),
|
||||
'desconto' => __convert_value_bd($request->desconto),
|
||||
'acrescimo' => __convert_value_bd($request->acrescimo),
|
||||
'tpNF' => 1,
|
||||
'orcamento' => 1,
|
||||
'tipo' => 9,
|
||||
'natureza_id' => $naturezaPadrao->id,
|
||||
'estado' => 'novo',
|
||||
'local_id' => $local->id,
|
||||
'empresa_id' => $empresa->id,
|
||||
'numero_sequencial' => $numero,
|
||||
'funcionario_id' => $funcionario->id,
|
||||
'ambiente' => $empresa->ambiente
|
||||
];
|
||||
// return response()->json($data, 200);
|
||||
|
||||
$nfe = Nfe::create($data);
|
||||
foreach($request->itens as $i){
|
||||
$produto = Produto::findOrFail($i['produto_id']);
|
||||
$variacao_id = $i['variacao_id'];
|
||||
if($empresa->cidade->uf == $cliente->cidade->uf){
|
||||
$cfop = $produto->cfop_estadual;
|
||||
}else{
|
||||
$cfop = $produto->cfop_outro_estado;
|
||||
}
|
||||
$item = [
|
||||
'nfe_id' => $nfe->id,
|
||||
'produto_id' => $i['produto_id'],
|
||||
'quantidade' => __convert_value_bd($i['quantidade']),
|
||||
'valor_unitario' => __convert_value_bd($i['valor_unitario']),
|
||||
'sub_total' => __convert_value_bd($i['sub_total']),
|
||||
|
||||
'perc_icms' => $produto->perc_icms,
|
||||
'perc_pis' => $produto->perc_pis,
|
||||
'perc_cofins' => $produto->perc_cofins,
|
||||
'perc_ipi' => $produto->perc_ipi,
|
||||
'cst_csosn' => $produto->cst_csosn,
|
||||
'cst_pis' => $produto->cst_pis,
|
||||
'cst_cofins' => $produto->cst_cofins,
|
||||
'cst_ipi' => $produto->cst_ipi,
|
||||
'perc_red_bc' => $produto->perc_red_bc,
|
||||
'cfop' => $cfop,
|
||||
'ncm' => $produto->ncm,
|
||||
'codigo_beneficio_fiscal' => $produto->codigo_beneficio_fiscal,
|
||||
'variacao_id' => $variacao_id,
|
||||
'cEnq' => $produto->cEnq,
|
||||
'xPed' => '',
|
||||
'nItemPed' => '',
|
||||
'infAdProd' => $i['observacao_item'],
|
||||
];
|
||||
ItemNfe::create($item);
|
||||
}
|
||||
|
||||
foreach($request->fatura as $p){
|
||||
FaturaNfe::create([
|
||||
'nfe_id' => $nfe->id,
|
||||
'tipo_pagamento' => $p['tipo_pagamento'],
|
||||
'data_vencimento' => $p['data_vencimento'],
|
||||
'valor' => __convert_value_bd($p['valor'])
|
||||
]);
|
||||
}
|
||||
return response()->json($data, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id){
|
||||
$nfe = Nfe::findOrFail($id);
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
$cliente = Cliente::findOrFail($request->cliente_id);
|
||||
$data = [
|
||||
'cliente_id' => $request->cliente_id,
|
||||
'total' => __convert_value_bd($request->total),
|
||||
'desconto' => __convert_value_bd($request->desconto),
|
||||
'acrescimo' => __convert_value_bd($request->acrescimo),
|
||||
];
|
||||
|
||||
$nfe->fill($data)->save();
|
||||
|
||||
$nfe->itens()->delete();
|
||||
foreach($request->itens as $i){
|
||||
$produto = Produto::findOrFail($i['produto_id']);
|
||||
$variacao_id = null;
|
||||
if($empresa->cidade->uf != $cliente->cidade->uf){
|
||||
$cfop = $produto->cfop_estadual;
|
||||
}else{
|
||||
$cfop = $produto->cfop_outro_estado;
|
||||
}
|
||||
$item = [
|
||||
'nfe_id' => $nfe->id,
|
||||
'produto_id' => $i['produto_id'],
|
||||
'quantidade' => __convert_value_bd($i['quantidade']),
|
||||
'valor_unitario' => __convert_value_bd($i['valor_unitario']),
|
||||
'sub_total' => __convert_value_bd($i['sub_total']),
|
||||
|
||||
'perc_icms' => $produto->perc_icms,
|
||||
'perc_pis' => $produto->perc_pis,
|
||||
'perc_cofins' => $produto->perc_cofins,
|
||||
'perc_ipi' => $produto->perc_ipi,
|
||||
'cst_csosn' => $produto->cst_csosn,
|
||||
'cst_pis' => $produto->cst_pis,
|
||||
'cst_cofins' => $produto->cst_cofins,
|
||||
'cst_ipi' => $produto->cst_ipi,
|
||||
'perc_red_bc' => $produto->perc_red_bc,
|
||||
'cfop' => $cfop,
|
||||
'ncm' => $produto->ncm,
|
||||
'codigo_beneficio_fiscal' => $produto->codigo_beneficio_fiscal,
|
||||
'variacao_id' => $variacao_id,
|
||||
'cEnq' => $produto->cEnq,
|
||||
'xPed' => '',
|
||||
'nItemPed' => '',
|
||||
'infAdProd' => $i['observacao_item'],
|
||||
];
|
||||
ItemNfe::create($item);
|
||||
}
|
||||
|
||||
$nfe->fatura()->delete();
|
||||
|
||||
foreach($request->fatura as $p){
|
||||
FaturaNfe::create([
|
||||
'nfe_id' => $nfe->id,
|
||||
'tipo_pagamento' => $p['tipo_pagamento'],
|
||||
'data_vencimento' => $p['data_vencimento'],
|
||||
'valor' => __convert_value_bd($p['valor'])
|
||||
]);
|
||||
}
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function destroy(Request $request){
|
||||
$item = Nfe::findOrFail($request->orcamento_id);
|
||||
$item->itens()->delete();
|
||||
$item->fatura()->delete();
|
||||
$item->delete();
|
||||
return response()->json($item, 200);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Empresa;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\PreVenda;
|
||||
use App\Models\ItemPreVenda;
|
||||
use App\Models\Localizacao;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Produto;
|
||||
use App\Models\FaturaPreVenda;
|
||||
use App\Models\NaturezaOperacao;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PreVendaController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
|
||||
$controlaId = $request->id > 0 ? $request->id : 9999999999999;
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_funcionario)
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
|
||||
$data = PreVenda::where('empresa_id', $request->empresa_id)
|
||||
->where('funcionario_id', $funcionario->id)
|
||||
->orderBy('id', 'desc')
|
||||
->with(['cliente', 'itens'])
|
||||
->limit(8)
|
||||
->where('id', '<', $controlaId)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function find(Request $request){
|
||||
|
||||
$item = PreVenda::where('orcamento', 1)
|
||||
->with(['cliente', 'itens', 'fatura'])
|
||||
->findOrFail($request->orcamento_id);
|
||||
|
||||
foreach($item->itens as $i){
|
||||
$i->produto_nome = $i->produto->nome;
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function destroy(Request $request){
|
||||
$item = PreVenda::findOrFail($request->pre_venda_id);
|
||||
$item->itens()->delete();
|
||||
$item->fatura()->delete();
|
||||
$item->delete();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
$empresa = Empresa::findOrFail($request->empresa_id);
|
||||
if($request->cliente_id){
|
||||
$cliente = Cliente::find($request->cliente_id);
|
||||
}else{
|
||||
$cliente = null;
|
||||
}
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_funcionario)
|
||||
->where('empresa_id', $empresa->id)
|
||||
->first();
|
||||
if($funcionario == null){
|
||||
return response()->json("Funcionário não encontrado!", 404);
|
||||
}
|
||||
if($funcionario->usuario_id == null){
|
||||
return response()->json("Vincule o usuário ao funcionário $funcionario->nome", 401);
|
||||
}
|
||||
|
||||
$local = Localizacao::where('empresa_id', $empresa->id)
|
||||
->first();
|
||||
$naturezaPadrao = NaturezaOperacao::where('empresa_id', $request->empresa_id)
|
||||
->where('padrao', 1)->first();
|
||||
if($naturezaPadrao == null){
|
||||
$naturezaPadrao = NaturezaOperacao::where('empresa_id', $request->empresa_id)->first();
|
||||
}
|
||||
|
||||
$data = [
|
||||
'cliente_id' => $cliente ? $cliente->id : null,
|
||||
'valor_total' => __convert_value_bd($request->total),
|
||||
'desconto' => __convert_value_bd($request->desconto),
|
||||
'acrescimo' => __convert_value_bd($request->acrescimo),
|
||||
'natureza_id' => $naturezaPadrao->id,
|
||||
'local_id' => $local->id,
|
||||
'empresa_id' => $empresa->id,
|
||||
'codigo' => Str::random(8),
|
||||
'funcionario_id' => $funcionario->id,
|
||||
'usuario_id' => $funcionario->usuario_id,
|
||||
];
|
||||
|
||||
$preVenda = PreVenda::create($data);
|
||||
foreach($request->itens as $i){
|
||||
$produto = Produto::findOrFail($i['produto_id']);
|
||||
|
||||
$cfop = 0;
|
||||
ItemPreVenda::create([
|
||||
'pre_venda_id' => $preVenda->id,
|
||||
'produto_id' => $produto->id,
|
||||
'quantidade' => __convert_value_bd($i['quantidade']),
|
||||
'valor' => __convert_value_bd($i['valor_unitario']),
|
||||
'cfop' => $cfop,
|
||||
'observacao' => $i['observacao_item'] ?? '',
|
||||
]);
|
||||
|
||||
}
|
||||
if(sizeof($request->fatura) > 0){
|
||||
foreach($request->fatura as $p){
|
||||
FaturaPreVenda::create([
|
||||
'valor_parcela' => __convert_value_bd($p['valor']),
|
||||
'tipo_pagamento' => $p['tipo_pagamento'],
|
||||
'pre_venda_id' => $preVenda->id,
|
||||
'vencimento' => $p['data_vencimento']
|
||||
]);
|
||||
}
|
||||
}else{
|
||||
// FaturaPreVenda::create([
|
||||
// 'valor_parcela' => __convert_value_bd($request->valor_total),
|
||||
// 'tipo_pagamento' => $request->tipo_pagamento,
|
||||
// 'pre_venda_id' => $preVenda->id,
|
||||
// 'vencimento' => $request->data_vencimento
|
||||
// ]);
|
||||
}
|
||||
return response()->json($data, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Comanda;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Produto;
|
||||
use App\Models\CategoriaProduto;
|
||||
use App\Models\PadraoTributacaoProduto;
|
||||
use App\Models\ProdutoLocalizacao;
|
||||
use App\Models\Localizacao;
|
||||
use App\Models\Funcionario;
|
||||
use App\Models\Estoque;
|
||||
use App\Utils\EstoqueUtil;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Utils\UploadUtil;
|
||||
|
||||
class ProdutoController extends Controller
|
||||
{
|
||||
|
||||
protected $utilEstoque;
|
||||
protected $utilUpload;
|
||||
public function __construct(EstoqueUtil $utilEstoque, UploadUtil $utilUpload)
|
||||
{
|
||||
$this->utilEstoque = $utilEstoque;
|
||||
$this->utilUpload = $utilUpload;
|
||||
}
|
||||
|
||||
public function index(Request $request){
|
||||
|
||||
$data = Produto::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->orderBy('nome')
|
||||
->with(['categoria', 'variacoes', 'estoque', 'adicionais', 'pizzaValores'])
|
||||
->select('id', 'nome', 'valor_unitario', 'categoria_id', 'codigo_barras', 'unidade', 'referencia',
|
||||
'valor_prazo', 'gerenciar_estoque', 'imagem', 'valor_compra')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function categorias(Request $request){
|
||||
|
||||
$data = CategoriaProduto::where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->select('nome', 'id')
|
||||
->where('categoria_id', null)
|
||||
->orderBy('nome')
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id){
|
||||
$item = Produto::findOrFail($id);
|
||||
$valorVenda = __convert_value_bd(str_replace("R$ ", "", $request->valor_venda));
|
||||
$valorCompra = __convert_value_bd(str_replace("R$ ", "", $request->valor_compra));
|
||||
try{
|
||||
$data = [
|
||||
'nome' => $request->nome,
|
||||
'categoria_id' => $request->categoria_id,
|
||||
'codigo_barras' => $request->codigo_barras,
|
||||
'valor_unitario' => $valorVenda,
|
||||
'valor_compra' => $valorCompra,
|
||||
];
|
||||
|
||||
$item->update($data);
|
||||
|
||||
if($request->estoque_atual){
|
||||
$quantidade = $request->estoque_atual;
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if($funcionario && $funcionario->usuario){
|
||||
$estoque = Estoque::where('produto_id', $id)->first();
|
||||
|
||||
if($estoque){
|
||||
$diferenca = 0;
|
||||
$tipo = 'incremento';
|
||||
|
||||
if($estoque->quantidade > $quantidade){
|
||||
$diferenca = $estoque->quantidade - $quantidade;
|
||||
$tipo = 'reducao';
|
||||
}else{
|
||||
$diferenca = $quantidade - $estoque->quantidade;
|
||||
}
|
||||
$estoque->quantidade = $quantidade;
|
||||
$estoque->save();
|
||||
|
||||
$codigo_transacao = $estoque->id;
|
||||
$tipo_transacao = 'alteracao_estoque';
|
||||
|
||||
$this->utilEstoque->movimentacaoProduto($estoque->produto_id, $diferenca, $tipo, $codigo_transacao, $tipo_transacao,
|
||||
$funcionario->usuario->id);
|
||||
}else{
|
||||
$this->utilEstoque->incrementaEstoque($produto->id, $quantidade, null);
|
||||
$transacao = Estoque::where('produto_id', $produto->id)->orderBy('id', 'desc')->first();
|
||||
|
||||
$codigo_transacao = $transacao->id;
|
||||
$tipo_transacao = 'alteracao_estoque';
|
||||
$this->utilEstoque->movimentacaoProduto($produto->id, $quantidade, $tipo, $codigo_transacao,
|
||||
$tipo_transacao, $funcionario->usuario->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
$padraoTributacaoProduto = PadraoTributacaoProduto::where('empresa_id', $request->empresa_id)
|
||||
->where('padrao', 1)->first();
|
||||
|
||||
if($padraoTributacaoProduto == null){
|
||||
return response()->json("Defina um padrão de tributação primeiro!", 401);
|
||||
}
|
||||
|
||||
$valorVenda = __convert_value_bd(str_replace("R$ ", "", $request->valor_venda));
|
||||
$valorCompra = __convert_value_bd(str_replace("R$ ", "", $request->valor_compra));
|
||||
|
||||
$data = [
|
||||
'nome' => $request->nome,
|
||||
'categoria_id' => $request->categoria_id,
|
||||
'codigo_barras' => $request->codigo_barras,
|
||||
'valor_unitario' => $valorVenda,
|
||||
'valor_compra' => $valorCompra,
|
||||
|
||||
'empresa_id' => $request->empresa_id,
|
||||
'perc_icms' => $padraoTributacaoProduto->perc_icms,
|
||||
'perc_pis' => $padraoTributacaoProduto->perc_pis,
|
||||
'perc_cofins' => $padraoTributacaoProduto->perc_cofins,
|
||||
'perc_ipi' => $padraoTributacaoProduto->perc_ipi,
|
||||
'cst_csosn' => $padraoTributacaoProduto->cst_csosn,
|
||||
'cst_pis' => $padraoTributacaoProduto->cst_pis,
|
||||
'cst_cofins' => $padraoTributacaoProduto->cst_cofins,
|
||||
'cst_ipi' => $padraoTributacaoProduto->cst_ipi,
|
||||
'perc_red_bc' => $padraoTributacaoProduto->perc_red_bc,
|
||||
'cEnq' => $padraoTributacaoProduto->cEnq,
|
||||
'pST' => $padraoTributacaoProduto->pST,
|
||||
'cfop_estadual' => $padraoTributacaoProduto->cfop_estadual,
|
||||
'cfop_outro_estado' => $padraoTributacaoProduto->cfop_outro_estado,
|
||||
'cest' => $padraoTributacaoProduto->cest,
|
||||
'ncm' => $padraoTributacaoProduto->ncm,
|
||||
'codigo_beneficio_fiscal' => $padraoTributacaoProduto->codigo_beneficio_fiscal,
|
||||
'cfop_entrada_estadual' => $padraoTributacaoProduto->cfop_entrada_estadual,
|
||||
'cfop_entrada_outro_estado' => $padraoTributacaoProduto->cfop_entrada_outro_estado,
|
||||
'modBCST' => $padraoTributacaoProduto->modBCST,
|
||||
'pMVAST' => $padraoTributacaoProduto->pMVAST,
|
||||
'pICMSST' => $padraoTributacaoProduto->pICMSST,
|
||||
'redBCST' => $padraoTributacaoProduto->redBCST,
|
||||
];
|
||||
|
||||
$produto = Produto::create($data);
|
||||
$localPadrao = Localizacao::where('status', 1)->where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
ProdutoLocalizacao::updateOrCreate([
|
||||
'produto_id' => $produto->id,
|
||||
'localizacao_id' => $localPadrao->id
|
||||
]);
|
||||
|
||||
|
||||
if($request->estoque_inicial){
|
||||
$funcionario = Funcionario::where('codigo', $request->codigo_operador)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
if($funcionario && $funcionario->usuario){
|
||||
$this->utilEstoque->incrementaEstoque($produto->id, $request->estoque_inicial, null);
|
||||
$transacao = Estoque::where('produto_id', $produto->id)->orderBy('id', 'desc')->first();
|
||||
|
||||
$codigo_transacao = $transacao->id;
|
||||
$tipo_transacao = 'alteracao_estoque';
|
||||
$this->utilEstoque->movimentacaoProduto($produto->id, $request->estoque_inicial, $tipo, $codigo_transacao,
|
||||
$tipo_transacao, $funcionario->usuario->id);
|
||||
}
|
||||
}
|
||||
return response()->json("ok", 200);
|
||||
}
|
||||
|
||||
public function destroy($id){
|
||||
$item = Produto::findOrFail($id);
|
||||
|
||||
try{
|
||||
$descricaoLog = $item->nome;
|
||||
|
||||
DB::transaction(function () use ($item) {
|
||||
if($item->estoque){
|
||||
$item->estoque->delete();
|
||||
}
|
||||
$item->movimentacoes()->delete();
|
||||
$item->variacoes()->delete();
|
||||
$item->variacoesMercadoLivre()->delete();
|
||||
$item->itemLista()->delete();
|
||||
$item->ibpt()->delete();
|
||||
// $item->itemNfe()->delete();
|
||||
// $item->estoque()->delete();
|
||||
$item->galeria()->delete();
|
||||
|
||||
// $item->itemNfce()->delete();
|
||||
$item->itemCarrinhos()->delete();
|
||||
$item->composicao()->delete();
|
||||
$item->itemPreVenda()->delete();
|
||||
$item->itensDoCombo()->delete();
|
||||
$item->fornecedores()->delete();
|
||||
$item->locais()->delete();
|
||||
|
||||
\App\Models\ImpressoraPedidoProduto::where('produto_id', $item->id)->delete();
|
||||
|
||||
$item->delete();
|
||||
});
|
||||
$this->utilUpload->unlinkImage($item, '/produtos');
|
||||
|
||||
__createLog($item->empresa_id, 'Produto', 'excluir', $descricaoLog);
|
||||
return response()->json("ok", 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
__createLog($item->empresa_id, 'Produto', 'erro', $e->getMessage());
|
||||
return response()->json($e->getMessage(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
public function upload(Request $request){
|
||||
|
||||
if (!$request->hasFile('image')) {
|
||||
return response()->json('Nenhuma imagem enviada', 401);
|
||||
}
|
||||
|
||||
$file = $request->file('image');
|
||||
$file_name = $this->utilUpload->uploadImage($request, '/produtos');
|
||||
|
||||
if($request->produto_id == null){
|
||||
$produto = Produto::where('empresa_id', $request->empresa_id)
|
||||
->orderBy('id', 'desc')->first();
|
||||
}else{
|
||||
$produto = Produto::findOrFail($request->produto_id);
|
||||
$this->utilUpload->unlinkImage($produto, '/produtos');
|
||||
}
|
||||
$produto->imagem = $file_name;
|
||||
$produto->save();
|
||||
|
||||
return response()->json("ok", 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Produto;
|
||||
|
||||
class ComboController extends Controller
|
||||
{
|
||||
public function modelo(Request $request){
|
||||
$item = Produto::findOrFail($request->produto_id);
|
||||
return view('combo_modelo.table', compact('item'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ConfigGeral;
|
||||
|
||||
class ConfigGeralController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
$item = ConfigGeral::where('empresa_id', $request->empresa_id)->first();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ContaPagarController extends Controller
|
||||
{
|
||||
public function recorrencia(Request $request)
|
||||
{
|
||||
$data = explode("/", $request->data);
|
||||
$vencimento = $request->vencimento;
|
||||
$valor = $request->valor;
|
||||
|
||||
$dia = \Carbon\Carbon::parse($vencimento)->format('d');
|
||||
|
||||
$novaData = "20" . $data[1] . "-" . $data[0] . "-" . $dia;
|
||||
$dif = strtotime($novaData) - strtotime($vencimento);
|
||||
|
||||
$meses = floor($dif / (60 * 60 * 24 * 30));
|
||||
|
||||
$datas = [];
|
||||
$data = $vencimento;
|
||||
// return response()->json($meses, 200);
|
||||
for ($i = 0; $i < $meses; $i++) {
|
||||
$data = date('Y-m-d', strtotime("+30 days", strtotime($data)));
|
||||
array_push($datas, $data);
|
||||
}
|
||||
|
||||
return view('conta-pagar.partials.recorrencia', compact('datas', 'valor'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\ContaReceber;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ContaReceberController extends Controller
|
||||
{
|
||||
public function recorrencia(Request $request)
|
||||
{
|
||||
$data = explode("/", $request->data);
|
||||
$vencimento = $request->vencimento;
|
||||
$valor = $request->valor;
|
||||
|
||||
$dia = \Carbon\Carbon::parse($vencimento)->format('d');
|
||||
|
||||
$novaData = "20" . $data[1] . "-" . $data[0] . "-" . $dia;
|
||||
$dif = strtotime($novaData) - strtotime($vencimento);
|
||||
|
||||
$meses = floor($dif / (60 * 60 * 24 * 30));
|
||||
|
||||
$datas = [];
|
||||
$data = $vencimento;
|
||||
// return response()->json($meses, 200);
|
||||
for ($i = 0; $i < $meses; $i++) {
|
||||
$data = date('Y-m-d', strtotime("+30 days", strtotime($data)));
|
||||
array_push($datas, $data);
|
||||
}
|
||||
|
||||
return view('conta-receber.partials.recorrencia', compact('datas', 'valor'));
|
||||
}
|
||||
|
||||
public function faturasDoCliente(Request $request)
|
||||
{
|
||||
$cliente_id = $request->cliente_id;
|
||||
|
||||
$cliente = Cliente::findOrFail($cliente_id);
|
||||
$data = ContaReceber::where('status', 0)
|
||||
->where('cliente_id', $cliente_id)->orderBy('data_vencimento')
|
||||
->get();
|
||||
|
||||
$totalPendente = 0;
|
||||
$totalAtrasado = 0;
|
||||
$hoje = Carbon::today();
|
||||
foreach ($data as $c) {
|
||||
if (Carbon::parse($c->data_vencimento)->lt($hoje)) {
|
||||
$totalAtrasado += $c->valor_integral;
|
||||
} else {
|
||||
$totalPendente += $c->valor_integral;
|
||||
}
|
||||
}
|
||||
|
||||
return view('conta-receber.partials.faturas_cliente', compact('data', 'cliente', 'totalAtrasado', 'totalPendente'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ContadorEmpresa;
|
||||
use App\Models\Empresa;
|
||||
|
||||
class ContadorController extends Controller
|
||||
{
|
||||
public function empresas(Request $request){
|
||||
$contador_id = $request->contador_id;
|
||||
|
||||
$data = ContadorEmpresa::where('contador_id', $contador_id)->get();
|
||||
$contador = Empresa::findOrFail($contador_id);
|
||||
|
||||
return view('contador.partials.tabela_contador', compact('data', 'contador'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Nfe;
|
||||
use App\Models\CrmAnotacao;
|
||||
use App\Models\Notificacao;
|
||||
use App\Models\FilaEnvioCron;
|
||||
|
||||
class CrmController extends Controller
|
||||
{
|
||||
public function referenciaRegistro(Request $request){
|
||||
$tipo_registro = $request->tipo_registro;
|
||||
$data = Nfe::where('empresa_id', $request->empresa_id)
|
||||
->when($tipo_registro == 'venda', function ($q){
|
||||
return $q->where('tpNF', 1)->where('orcamento', 0);
|
||||
})
|
||||
->when($tipo_registro == 'compra', function ($q){
|
||||
return $q->where('tpNF', 0)->where('orcamento', 0);
|
||||
})
|
||||
->when($tipo_registro == 'orçamento', function ($q){
|
||||
return $q->where('orcamento', 1);
|
||||
})
|
||||
->where('numero_sequencial', 'like', "%$request->pesquisa%")
|
||||
->get();
|
||||
|
||||
foreach($data as $item){
|
||||
$razaoSocial = "";
|
||||
if($item->cliente){
|
||||
$razaoSocial = $item->cliente->info;
|
||||
}
|
||||
if($item->fornecedor){
|
||||
$razaoSocial = $item->fornecedor->info;
|
||||
}
|
||||
$item->descricao = $item->numero_sequencial . " - " . $razaoSocial . " | R$" . __moeda($item->total);
|
||||
}
|
||||
return response()->json($data, 200);
|
||||
}
|
||||
|
||||
public function modalLog(Request $request){
|
||||
$item = FilaEnvioCron::findOrFail($request->id);
|
||||
return view('mensagem_padrao_crm.partials.modal', compact('item'));
|
||||
}
|
||||
|
||||
public function modal(Request $request){
|
||||
$data = CrmAnotacao::where('empresa_id', $request->empresa_id)
|
||||
->whereDate('data_retorno', date('Y-m-d'))
|
||||
->get();
|
||||
$this->criaNotificacao($data);
|
||||
return view('crm.partials.modal', compact('data'));
|
||||
}
|
||||
|
||||
private function criaNotificacao($data){
|
||||
foreach($data as $item){
|
||||
$notificacao = Notificacao::where('empresa_id', $item->empresa_id)
|
||||
->where('tabela', 'crm_anotacaos')
|
||||
->where('referencia', $item->id)->first();
|
||||
|
||||
if($notificacao == null){
|
||||
|
||||
if($item->cliente){
|
||||
$descricaoCurta = $item->cliente->info;
|
||||
}
|
||||
if($item->fornecedor){
|
||||
$descricaoCurta = $item->fornecedor->info;
|
||||
}
|
||||
$descricao = view('notificacao.partials.crm', compact('item'));
|
||||
|
||||
Notificacao::create([
|
||||
'empresa_id' => $item->empresa_id,
|
||||
'tabela' => 'crm_anotacaos',
|
||||
'descricao' => $descricao,
|
||||
'descricao_curta' => $descricaoCurta,
|
||||
'referencia' => $item->id,
|
||||
'status' => 1,
|
||||
'por_sistema' => 1,
|
||||
'prioridade' => 'alta',
|
||||
'visualizada' => 0,
|
||||
'titulo' => 'CRM'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Delivery;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ItemCarrinhoDelivery;
|
||||
use App\Models\CarrinhoDelivery;
|
||||
use App\Models\EnderecoDelivery;
|
||||
use App\Models\MarketPlaceConfig;
|
||||
|
||||
class CarrinhoController extends Controller
|
||||
{
|
||||
public function removeItem(Request $request){
|
||||
$item = ItemCarrinhoDelivery::findOrFail($request->item_id);
|
||||
$item->adicionais()->delete();
|
||||
$item->sabores()->delete();
|
||||
$item->delete();
|
||||
|
||||
$carrinho = $item->carrinho;
|
||||
$carrinho->valor_total = $carrinho->itens->sum('sub_total') + $carrinho->valor_frete - $carrinho->valor_desconto;
|
||||
$carrinho->save();
|
||||
return response()->json($carrinho, 200);
|
||||
}
|
||||
|
||||
public function carrinhoCount(Request $request){
|
||||
$item = ItemCarrinhoDelivery::join('carrinho_deliveries', 'carrinho_deliveries.id', 'item_carrinho_deliveries.carrinho_id')
|
||||
->where('carrinho_deliveries.session_cart_delivery', $request->session_cart_delivery)->count();
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function atualizaQuantidade(Request $request){
|
||||
$item = ItemCarrinhoDelivery::findOrFail($request->item_id);
|
||||
$item->quantidade = $request->quantidade;
|
||||
$item->sub_total = $item->quantidade * $item->valor_unitario;
|
||||
$item->save();
|
||||
|
||||
$carrinho = CarrinhoDelivery::findOrFail($item->carrinho_id);
|
||||
$carrinho->valor_total = $carrinho->itens->sum('sub_total') + $carrinho->valor_frete;
|
||||
|
||||
$carrinho->save();
|
||||
$item->total_carrinho = $carrinho->valor_total;
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function validaEstoque(Request $request){
|
||||
$item = ItemCarrinhoDelivery::findOrFail($request->item_id);
|
||||
if($item->servico){
|
||||
return response()->json("estoque ok", 200);
|
||||
}
|
||||
$quantidade = $request->quantidade;
|
||||
$produto = $item->produto;
|
||||
if($produto->gerenciar_estoque){
|
||||
if(!$produto->estoque || $produto->estoque->quantidade < $quantidade){
|
||||
return response()->json("Estoque insuficiente!", 401);
|
||||
}
|
||||
}
|
||||
return response()->json("estoque ok", 200);
|
||||
}
|
||||
|
||||
public function carrinhoModal(Request $request){
|
||||
$carrinho = CarrinhoDelivery::where('session_cart_delivery', $request->session_cart)->first();
|
||||
$config = MarketPlaceConfig::where('empresa_id', $carrinho->empresa_id)->first();
|
||||
return view('food.partials.carrinho_modal', compact('config', 'carrinho'));
|
||||
}
|
||||
|
||||
public function atualizaCarrinho(Request $request){
|
||||
$carrinho = CarrinhoDelivery::findOrFail($request->carrinho_id);
|
||||
$carrinho->observacao = $request->observacoes;
|
||||
$carrinho->tipo_pagamento = $request->forma_pagamento;
|
||||
$carrinho->cupom = $request->cupom;
|
||||
$carrinho->fone = $request->whatsapp;
|
||||
$config = MarketPlaceConfig::where('empresa_id', $carrinho->empresa_id)->first();
|
||||
|
||||
$cliente = $carrinho->cliente;
|
||||
// return $cliente->id;
|
||||
|
||||
if($cliente){
|
||||
$cliente->razao_social = $request->nome;
|
||||
$cliente->cpf_cnpj = $request->cpf;
|
||||
$cliente->save();
|
||||
}
|
||||
|
||||
$endereco = null;
|
||||
if($request->endereco_id == null){
|
||||
|
||||
if($request->bairro_id){
|
||||
|
||||
$end = EnderecoDelivery::where('cliente_id', $cliente->id)
|
||||
->where('rua', $request->endereco_rua)
|
||||
->where('numero', $request->endereco_numero)
|
||||
->first();
|
||||
if($end == null){
|
||||
|
||||
$dataEndereco = [
|
||||
'rua' => $request->endereco_rua ?? '',
|
||||
'numero'=> $request->endereco_numero ?? '',
|
||||
'bairro_id'=> $request->bairro_id,
|
||||
'referencia'=> $request->endereco_referencia ?? '',
|
||||
'tipo' => $request->tipo,
|
||||
'cep' => $request->endereco_cep ?? '',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'cliente_id' => $cliente->id,
|
||||
'cidade_id' => $config->cidade_id,
|
||||
'padrao' => sizeof($cliente->enderecos) == 0 ? 1 : 0
|
||||
];
|
||||
$endereco = EnderecoDelivery::create($dataEndereco);
|
||||
$carrinho->endereco_id = $endereco->id;
|
||||
$carrinho->save();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
|
||||
$endereco = EnderecoDelivery::findOrFail($request->endereco_id);
|
||||
if($endereco){
|
||||
|
||||
$endereco->rua = $request->endereco_rua;
|
||||
$endereco->numero = $request->endereco_numero;
|
||||
$endereco->bairro_id = $request->bairro_id;
|
||||
$endereco->referencia = $request->endereco_referencia;
|
||||
$endereco->tipo = $request->tipo;
|
||||
$endereco->cep = $request->endereco_cep;
|
||||
$endereco->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if($endereco){
|
||||
// $carrinho->endereco = $endereco;
|
||||
$carrinho->valor_frete = $endereco->bairro->valor_entrega;
|
||||
if(($carrinho->itens->sum('sub_total') - $carrinho->desconto) >= $config->valor_entrega_gratis){
|
||||
$carrinho->valor_frete = 0;
|
||||
}
|
||||
$carrinho->valor_total = $carrinho->itens->sum('sub_total') + $carrinho->valor_frete - $carrinho->desconto;
|
||||
$carrinho->save();
|
||||
|
||||
}
|
||||
$trocoPara = str_replace("R$ ", "", $request->forma_pagamento_informacao);
|
||||
|
||||
$carrinho->troco_para = __convert_value_bd($trocoPara);
|
||||
$carrinho->save();
|
||||
|
||||
$carrinho = CarrinhoDelivery::
|
||||
with(['endereco'])
|
||||
->findOrFail($request->carrinho_id);
|
||||
return response()->json($carrinho, 200);
|
||||
|
||||
}
|
||||
|
||||
public function getEnderecos(Request $request){
|
||||
$carrinho = CarrinhoDelivery::findOrFail($request->carrinho_id);
|
||||
$cliente = $carrinho->cliente;
|
||||
|
||||
if($cliente){
|
||||
return response()->json($cliente->enderecosDelivery, 200);
|
||||
}
|
||||
return response()->json([], 200);
|
||||
}
|
||||
|
||||
public function comprovanteCarrinho(Request $request){
|
||||
$carrinho = CarrinhoDelivery::findOrFail($request->carrinho_id);
|
||||
$config = MarketPlaceConfig::where('empresa_id', $carrinho->empresa_id)->first();
|
||||
|
||||
return view('food.partials.comprovante', compact('carrinho', 'config'));
|
||||
}
|
||||
|
||||
public function findEndereco(Request $request){
|
||||
$endereco = EnderecoDelivery::findOrFail($request->endereco_id);
|
||||
return response()->json($endereco, 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Delivery;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\EnderecoDelivery;
|
||||
use App\Models\MarketPlaceConfig;
|
||||
use App\Models\ConfiguracaoSuper;
|
||||
use Illuminate\Support\Str;
|
||||
use Comtele\Services\TextMessageService;
|
||||
|
||||
class ClienteController extends Controller
|
||||
{
|
||||
|
||||
public function login(Request $request){
|
||||
|
||||
$cliente = Cliente::where('telefone', preg_replace('/[^0-9]/', '', $request->email_celular))
|
||||
->where('senha', md5($request->senha))
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
|
||||
if($cliente != null){
|
||||
return response()->json($cliente, 200);
|
||||
}
|
||||
|
||||
$cliente = Cliente::where('email', $request->email_celular)
|
||||
->where('senha', md5($request->senha))
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->first();
|
||||
if($cliente != null){
|
||||
return response()->json($cliente, 200);
|
||||
}
|
||||
|
||||
return response()->json("credenciais incorretas!", 404);
|
||||
|
||||
}
|
||||
|
||||
public function enderecoSave(Request $request){
|
||||
try{
|
||||
|
||||
$cliente = Cliente::where('uid', $request->uid)->first();
|
||||
$config = MarketPlaceConfig::where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
$data = [
|
||||
'rua' => $request->rua,
|
||||
'numero'=> $request->numero,
|
||||
'bairro'=> '',
|
||||
'bairro_id'=> $request->bairro,
|
||||
'referencia'=> $request->complemento ?? '',
|
||||
'tipo' => $request->tipo,
|
||||
'latitude' => $request->latitude,
|
||||
'longitude' => $request->longitude,
|
||||
'cliente_id' => $cliente->id,
|
||||
'cidade_id' => $config->cidade_id,
|
||||
'padrao' => sizeof($cliente->enderecos) == 0 ? 1 : 0
|
||||
];
|
||||
$endereco = EnderecoDelivery::create($data);
|
||||
return response()->json($endereco, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function enderecoUpdate(Request $request){
|
||||
try{
|
||||
|
||||
$endereco = EnderecoDelivery::where('id', $request->id)->first();
|
||||
|
||||
$endereco->fill($request->all())->save();
|
||||
return response()->json($endereco, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function clienteSave(Request $request){
|
||||
try{
|
||||
|
||||
$cli = Cliente::where('email', $request->email)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
$config = MarketPlaceConfig::where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
if($cli != null){
|
||||
return response()->json("email já cadastrado!", 402);
|
||||
}
|
||||
|
||||
$celular = preg_replace('/[^0-9]/', '', $request->celular);
|
||||
|
||||
$cli = Cliente::where('telefone', $celular)
|
||||
->where('empresa_id', $request->empresa_id)->first();
|
||||
if($cli != null){
|
||||
return response()->json("celular já cadastrado!", 402);
|
||||
}
|
||||
$code = rand() % 9000 + 999;
|
||||
$request->merge([
|
||||
'senha' => md5($request->senha),
|
||||
'telefone' => $celular,
|
||||
'uid' => Str::random(30),
|
||||
'token' => $code,
|
||||
'status' => !$config->autenticacao_sms,
|
||||
'razao_social' => $request->nome
|
||||
]);
|
||||
if($config->autenticacao_sms){
|
||||
$this->sendSms($celular, $code, $config);
|
||||
}
|
||||
$cli = Cliente::create($request->all());
|
||||
return response()->json($cli, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendSms($phone, $code, $config){
|
||||
|
||||
$config = ConfiguracaoSuper::first();
|
||||
if($config != null && $config->sms_key != ''){
|
||||
$nomeEmpresa = $config->nome;
|
||||
$content = $nomeEmpresa. " Cóodigo de Autorização ". $code;
|
||||
$textMessageService = new TextMessageService($config->sms_key);
|
||||
$res = $textMessageService->send("Sender", $content, [$phone]);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
public function clienteUpdate(Request $request){
|
||||
try{
|
||||
$item = Cliente::where('uid', $request->uid)->first();
|
||||
|
||||
$item->razao_social = $request->nome;
|
||||
// $item->sobre_nome = $request->sobre_nome;
|
||||
$item->email = $request->email;
|
||||
$item->telefone = $request->celular;
|
||||
$item->save();
|
||||
return response()->json($item, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function clienteUpdateSenha(Request $request){
|
||||
try{
|
||||
$item = Cliente::where('uid', $request->uid)->first();
|
||||
|
||||
$item->senha = md5($request->senha);
|
||||
$item->save();
|
||||
return response()->json($item, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function findCliente(Request $request){
|
||||
try{
|
||||
$cliente = Cliente::where('empresa_id', $request->empresa_id)
|
||||
->with(['enderecos', 'pedidos'])
|
||||
->where('uid', $request->uid)->first();
|
||||
|
||||
return response()->json($cliente, 200);
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateEnderecoPadrao(Request $request){
|
||||
$endereco = EnderecoDelivery::findOrFail($request->endereco_id);
|
||||
try{
|
||||
|
||||
EnderecoDelivery::where('cliente_id', $endereco->cliente_id)
|
||||
->update(['padrao' => 0]);
|
||||
|
||||
$endereco->padrao = 1;
|
||||
$endereco->save();
|
||||
return response()->json($endereco, 200);
|
||||
|
||||
}catch(\Exception $e){
|
||||
return response()->json("err: " . $e->getMessage(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function sendCode(Request $request){
|
||||
$code = $request->code;
|
||||
$cliente = Cliente::where('empresa_id', $request->empresa_id)
|
||||
->where('token', $code)->first();
|
||||
if($cliente != null){
|
||||
$cliente->status = 1;
|
||||
$cliente->save();
|
||||
return response()->json($cliente, 200);
|
||||
}else{
|
||||
return response()->json("erro", 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshCode(Request $request){
|
||||
$cliente = Cliente::where('uid', $request->uid)->first();
|
||||
if($cliente != null){
|
||||
|
||||
$config = MarketPlaceConfig::where('empresa_id', $request->empresa_id)->first();
|
||||
|
||||
$code = rand() % 9000 + 999;
|
||||
$cliente->token = $code;
|
||||
$this->sendSms($cliente->celular, $code, $config);
|
||||
|
||||
$cliente->save();
|
||||
return response()->json($cliente, 200);
|
||||
}else{
|
||||
return response()->json("erro", 404);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Delivery;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\MarketPlaceConfig;
|
||||
use App\Models\CupomDesconto;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\CupomDescontoCliente;
|
||||
use App\Models\BairroDelivery;
|
||||
use App\Models\FuncionamentoDelivery;
|
||||
use App\Models\TamanhoPizza;
|
||||
|
||||
class ConfigController extends Controller
|
||||
{
|
||||
public function index(){
|
||||
$item = MarketPlaceConfig::
|
||||
where('empresa_id', request()->empresa_id)
|
||||
->with('cidade')
|
||||
->first();
|
||||
|
||||
$dia = date('w');
|
||||
$hora = date('H:i');
|
||||
$dia = FuncionamentoDelivery::getDia($dia);
|
||||
$funcionamento = FuncionamentoDelivery::where('dia', $dia)
|
||||
->where('empresa_id', request()->empresa_id)->first();
|
||||
$aberto = false;
|
||||
|
||||
if($funcionamento != null){
|
||||
$item->fim_expediente = $funcionamento->fim;
|
||||
$item->inicio_expediente = $funcionamento->inicio;
|
||||
|
||||
$atual = strtotime(date('Y-m-d H:i'));
|
||||
$dataHoje = date('Y-m-d');
|
||||
$inicio = strtotime($dataHoje . " " . $funcionamento->inicio);
|
||||
$fim = strtotime($dataHoje . " " . $funcionamento->fim);
|
||||
if($atual > $inicio && $atual < $fim){
|
||||
$aberto = true;
|
||||
}
|
||||
}else{
|
||||
$item->fim_expediente = null;
|
||||
$item->inicio_expediente = null;
|
||||
}
|
||||
|
||||
|
||||
$item->aberto = $aberto;
|
||||
$item->tipo_entrega = json_decode($item->tipo_entrega);
|
||||
|
||||
$tiposPagamento = [];
|
||||
if($item != null){
|
||||
$tipos_pagamento = $item->tipos_pagamento ? json_decode($item->tipos_pagamento) : [];
|
||||
|
||||
foreach($tipos_pagamento as $tp){
|
||||
// array_push($tiposPagamento, MarketPlaceConfig::tiposPagamento()[$tp]);
|
||||
array_push($tiposPagamento, $tp);
|
||||
}
|
||||
$item->tipos_pagamento = $tiposPagamento;
|
||||
}
|
||||
$item->maximo_sabores_pizza = 0;
|
||||
|
||||
$tamanho = TamanhoPizza::where('empresa_id', request()->empresa_id)
|
||||
->orderBy('maximo_sabores', 'desc')->first();
|
||||
if($tamanho != null){
|
||||
$item->maximo_sabores_pizza = $tamanho->maximo_sabores;
|
||||
}
|
||||
|
||||
return response()->json($item, 200);
|
||||
}
|
||||
|
||||
public function cupom(Request $request){
|
||||
$item = CupomDesconto::where('codigo', $request->cupom)
|
||||
->where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
if($item == null){
|
||||
return response()->json("cupom não encontrado", 404);
|
||||
}
|
||||
$total = $request->total;
|
||||
|
||||
if($total < $item->valor_minimo_pedido){
|
||||
return response()->json("valor minímo para este cupom R$ " . __moeda($item->valor_minimo_pedido), 401);
|
||||
}
|
||||
|
||||
$cliente = Cliente::where('uid', $request->uid)->first();
|
||||
if($cliente == null){
|
||||
return response()->json("cliente não encontrado", 404);
|
||||
}
|
||||
|
||||
$cupomUsuado = CupomDescontoCliente::where('empresa_id', $request->empresa_id)
|
||||
->where('cupom_id', $item->id)
|
||||
->where('cliente_id', $cliente->id)
|
||||
->first();
|
||||
if($cupomUsuado != null){
|
||||
return response()->json("cupom já utilizado no pedido #$cupomUsuado->pedido_id", 404);
|
||||
}
|
||||
|
||||
$desconto = 0;
|
||||
if($item->tipo_desconto == 'valor'){
|
||||
$desconto = $item->valor;
|
||||
}else{
|
||||
$desconto = $total * ($item->valor/100);
|
||||
}
|
||||
|
||||
return response()->json($desconto, 200);
|
||||
|
||||
}
|
||||
|
||||
public function bairros(Request $request){
|
||||
$data = BairroDelivery::
|
||||
where('empresa_id', $request->empresa_id)
|
||||
->where('status', 1)
|
||||
->get();
|
||||
|
||||
return response()->json($data, 200);
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue