diff --git a/.gitignore b/.gitignore
index e458ed5..4018f95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,9 @@
.worktrees/
+
+# Secrets — NUNCA commitar
+.env
+.env.prod
+.env.local
+back-end/.env
+back-end/.env.local
+!back-end/.env.example
diff --git a/.playwright-mcp/console-2026-04-02T18-39-05-247Z.log b/.playwright-mcp/console-2026-04-02T18-39-05-247Z.log
new file mode 100644
index 0000000..e3381c7
--- /dev/null
+++ b/.playwright-mcp/console-2026-04-02T18-39-05-247Z.log
@@ -0,0 +1,23 @@
+[ 44110ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://163.176.236.167/api/_nuxt_icon/lucide.json?icons=loader-circle:0
+[ 44111ms] [WARNING] [Icon] failed to load icon `lucide:loader-circle` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45020ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://163.176.236.167/api/_nuxt_icon/heroicons.json?icons=arrow-right-on-rectangle%2Cbuilding-library%2Cbuilding-office-2%2Cchart-bar%2Cclipboard-document-list%2Cclock%2Ccog-6-tooth%2Cdocument-text%2Cfolder%2Chome%2Cmagnifying-glass%2Cmegaphone%2Cpencil-square%2Cplay%2Cscale%2Ctrophy%2Cusers%2Cview-columns%2Cx-circle:0
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:home` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:home` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:clipboard-document-list` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:magnifying-glass` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:pencil-square` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:megaphone` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:play` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:scale` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:trophy` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:x-circle` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:view-columns` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:folder` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:clock` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:building-library` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:building-office-2` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:document-text` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:chart-bar` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:users` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:cog-6-tooth` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
+[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:arrow-right-on-rectangle` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5
diff --git a/.playwright-mcp/page-2026-04-02T18-39-06-804Z.yml b/.playwright-mcp/page-2026-04-02T18-39-06-804Z.yml
new file mode 100644
index 0000000..be04548
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-02T18-39-06-804Z.yml
@@ -0,0 +1,20 @@
+- generic [ref=e4]:
+ - generic [ref=e5]:
+ - heading "Bem-vindo de volta" [level=1] [ref=e6]
+ - paragraph [ref=e7]: Informe suas credenciais para acessar o sistema
+ - generic [ref=e8]:
+ - generic [ref=e9]:
+ - generic [ref=e12]: E-mail
+ - textbox "E-mail" [ref=e15]:
+ - /placeholder: usuario@orgao.gov.br
+ - generic [ref=e16]:
+ - generic [ref=e19]: Senha
+ - textbox "Senha" [ref=e22]:
+ - /placeholder: ••••••••
+ - link "Esqueceu a senha?" [ref=e24] [cursor=pointer]:
+ - /url: "#"
+ - button "Entrar" [ref=e25]
+ - paragraph [ref=e26]:
+ - text: Não tem conta?
+ - link "Criar conta" [ref=e27] [cursor=pointer]:
+ - /url: /register
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-02T18-39-10-569Z.png b/.playwright-mcp/page-2026-04-02T18-39-10-569Z.png
new file mode 100644
index 0000000..4f0cb1c
Binary files /dev/null and b/.playwright-mcp/page-2026-04-02T18-39-10-569Z.png differ
diff --git a/.playwright-mcp/page-2026-04-02T18-39-22-154Z.yml b/.playwright-mcp/page-2026-04-02T18-39-22-154Z.yml
new file mode 100644
index 0000000..65cfdca
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-02T18-39-22-154Z.yml
@@ -0,0 +1,29 @@
+- generic [ref=e28]:
+ - generic [ref=e29]:
+ - heading "Criar conta" [level=1] [ref=e30]
+ - paragraph [ref=e31]: Preencha os dados para começar a usar o Licitatche
+ - generic [ref=e32]:
+ - generic [ref=e33]:
+ - button "Pessoa Jurídica" [ref=e34] [cursor=pointer]
+ - button "Pessoa Física" [ref=e35] [cursor=pointer]
+ - generic [ref=e36]:
+ - generic [ref=e39]: Razão Social
+ - textbox "Razão Social" [ref=e42]:
+ - /placeholder: Razão Social completa
+ - generic [ref=e43]:
+ - generic [ref=e46]: CNPJ
+ - textbox "CNPJ" [ref=e49]:
+ - /placeholder: 00.000.000/0001-00
+ - generic [ref=e50]:
+ - generic [ref=e53]: E-mail
+ - textbox "E-mail" [ref=e56]:
+ - /placeholder: voce@empresa.com.br
+ - generic [ref=e57]:
+ - generic [ref=e60]: Senha
+ - textbox "Senha" [ref=e63]:
+ - /placeholder: Mínimo 8 caracteres
+ - button "Criar conta" [ref=e64]
+ - paragraph [ref=e65]:
+ - text: Já tem conta?
+ - link "Fazer login" [ref=e66] [cursor=pointer]:
+ - /url: /login
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-02T18-39-51-372Z.yml b/.playwright-mcp/page-2026-04-02T18-39-51-372Z.yml
new file mode 100644
index 0000000..fd1be27
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-02T18-39-51-372Z.yml
@@ -0,0 +1,151 @@
+- generic [ref=e67]:
+ - complementary [ref=e68]:
+ - generic [ref=e72]: Licitatche
+ - navigation [ref=e73]:
+ - link "Dashboard" [ref=e74] [cursor=pointer]:
+ - /url: /
+ - generic [ref=e76]: Dashboard
+ - paragraph [ref=e77]: Oportunidades
+ - link "Todos os Editais" [ref=e78] [cursor=pointer]:
+ - /url: /oportunidades
+ - generic [ref=e80]: Todos os Editais
+ - link "Mapeamento" [ref=e81] [cursor=pointer]:
+ - /url: /oportunidades/em-analise
+ - generic [ref=e83]: Mapeamento
+ - link "Termo de Referência" [ref=e84] [cursor=pointer]:
+ - /url: /oportunidades/elaborando-proposta
+ - generic [ref=e86]: Termo de Referência
+ - link "Edital Publicado" [ref=e87] [cursor=pointer]:
+ - /url: /oportunidades/edital-publicado
+ - generic [ref=e89]: Edital Publicado
+ - link "Fase de Lances" [ref=e90] [cursor=pointer]:
+ - /url: /oportunidades/fase-lances
+ - generic [ref=e92]: Fase de Lances
+ - link "Recurso" [ref=e93] [cursor=pointer]:
+ - /url: /oportunidades/recurso
+ - generic [ref=e95]: Recurso
+ - link "Vencidas" [ref=e96] [cursor=pointer]:
+ - /url: /oportunidades/vencidas
+ - generic [ref=e98]: Vencidas
+ - link "Perdidas" [ref=e99] [cursor=pointer]:
+ - /url: /oportunidades/perdidas
+ - generic [ref=e101]: Perdidas
+ - paragraph [ref=e103]: Pipeline
+ - link "Kanban de Processos" [ref=e104] [cursor=pointer]:
+ - /url: /pipeline
+ - generic [ref=e106]: Kanban de Processos
+ - paragraph [ref=e108]: Gestão
+ - link "Documentos" [ref=e109] [cursor=pointer]:
+ - /url: /gestao/documentos
+ - generic [ref=e111]: Documentos
+ - link "Prazos" [ref=e112] [cursor=pointer]:
+ - /url: /gestao/prazos
+ - generic [ref=e114]: Prazos
+ - link "Órgãos Públicos" [ref=e115] [cursor=pointer]:
+ - /url: /gestao/orgaos
+ - generic [ref=e117]: Órgãos Públicos
+ - link "Concorrentes" [ref=e118] [cursor=pointer]:
+ - /url: /gestao/concorrentes
+ - generic [ref=e120]: Concorrentes
+ - link "Contratos" [ref=e121] [cursor=pointer]:
+ - /url: /gestao/contratos
+ - generic [ref=e123]: Contratos
+ - paragraph [ref=e125]: Inteligência
+ - link "Inteligência de Mercado" [ref=e126] [cursor=pointer]:
+ - /url: /inteligencia
+ - generic [ref=e128]: Inteligência de Mercado
+ - paragraph [ref=e130]: Sistema
+ - link "Usuários" [ref=e131] [cursor=pointer]:
+ - /url: /sistema/usuarios
+ - generic [ref=e133]: Usuários
+ - link "Configurações" [ref=e134] [cursor=pointer]:
+ - /url: /sistema/configuracoes
+ - generic [ref=e136]: Configurações
+ - generic [ref=e139]:
+ - generic [ref=e141]: a
+ - generic [ref=e142]:
+ - paragraph [ref=e143]: admin
+ - paragraph [ref=e144]: admin
+ - button [ref=e145]
+ - generic [ref=e148]:
+ - banner [ref=e149]:
+ - generic [ref=e150]:
+ - heading "Dashboard" [level=1] [ref=e151]
+ - paragraph [ref=e152]: Visão geral · 2 de abril de 2026
+ - link "Ver Oportunidades" [ref=e154] [cursor=pointer]:
+ - /url: /oportunidades
+ - generic [ref=e155]:
+ - generic [ref=e156]:
+ - generic [ref=e157]:
+ - paragraph [ref=e158]: Total de Editais
+ - paragraph [ref=e159]: "0"
+ - paragraph [ref=e160]: Cadastrados
+ - generic [ref=e161]:
+ - paragraph [ref=e162]: Taxa de Vitória
+ - paragraph [ref=e163]: 0%
+ - paragraph [ref=e164]: Processos finalizados
+ - generic [ref=e165]:
+ - paragraph [ref=e166]: Contratos Ativos
+ - paragraph [ref=e167]: R$ 0
+ - paragraph [ref=e168]: 0 contratos
+ - generic [ref=e169]:
+ - paragraph [ref=e170]: Alertas Ativos
+ - paragraph [ref=e171]: "0"
+ - paragraph [ref=e172]: Requerem atenção
+ - generic [ref=e173]:
+ - generic [ref=e174]:
+ - heading "Pipeline de Oportunidades" [level=3] [ref=e175]
+ - link "Ver kanban →" [ref=e176] [cursor=pointer]:
+ - /url: /pipeline
+ - generic [ref=e178]:
+ - generic [ref=e179]:
+ - generic [ref=e181]: "1"
+ - paragraph [ref=e182]: Mapeamento
+ - paragraph [ref=e183]: 0 editais
+ - generic [ref=e184]:
+ - generic [ref=e186]: "2"
+ - paragraph [ref=e187]: Termo de Referência
+ - paragraph [ref=e188]: 0 editais
+ - generic [ref=e189]:
+ - generic [ref=e191]: "3"
+ - paragraph [ref=e192]: Edital Publicado
+ - paragraph [ref=e193]: 0 editais
+ - generic [ref=e194]:
+ - generic [ref=e196]: "4"
+ - paragraph [ref=e197]: Fase de Lances
+ - paragraph [ref=e198]: 0 editais
+ - generic [ref=e199]:
+ - generic [ref=e201]: "5"
+ - paragraph [ref=e202]: Habilitação
+ - paragraph [ref=e203]: 0 editais
+ - generic [ref=e204]:
+ - generic [ref=e206]: "6"
+ - paragraph [ref=e207]: Recursos
+ - paragraph [ref=e208]: 0 editais
+ - generic [ref=e209]:
+ - generic [ref=e211]: "7"
+ - paragraph [ref=e212]: Adjudicado
+ - paragraph [ref=e213]: 0 editais
+ - generic [ref=e214]:
+ - generic [ref=e216]: "8"
+ - paragraph [ref=e217]: Contrato
+ - paragraph [ref=e218]: 0 editais
+ - generic [ref=e219]:
+ - generic [ref=e220]:
+ - generic [ref=e221]:
+ - heading "Alertas de Prazo" [level=3] [ref=e222]
+ - link "Ver todos →" [ref=e223] [cursor=pointer]:
+ - /url: /gestao/prazos
+ - paragraph [ref=e225]: Nenhum prazo urgente nos próximos 30 dias
+ - generic [ref=e226]:
+ - generic [ref=e227]:
+ - heading "Documentos com Alerta" [level=3] [ref=e228]
+ - link "Gerenciar →" [ref=e229] [cursor=pointer]:
+ - /url: /gestao/documentos
+ - paragraph [ref=e231]: Todos os documentos estão em dia
+ - generic [ref=e232]:
+ - generic [ref=e233]:
+ - heading "Editais Recentes" [level=3] [ref=e234]
+ - link "Ver todos →" [ref=e235] [cursor=pointer]:
+ - /url: /oportunidades
+ - paragraph [ref=e237]: Nenhum edital cadastrado ainda
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-02T18-39-56-786Z.png b/.playwright-mcp/page-2026-04-02T18-39-56-786Z.png
new file mode 100644
index 0000000..2e03faf
Binary files /dev/null and b/.playwright-mcp/page-2026-04-02T18-39-56-786Z.png differ
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b3cec63
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,52 @@
+.PHONY: help run stop up down db migrate api front logs
+
+MIGRATE=$(HOME)/go/bin/migrate
+
+# ─── Defaults ───────────────────────────────────────────────────────────────
+help:
+ @echo ""
+ @echo " Licitatche — comandos disponíveis"
+ @echo ""
+ @echo " make up Sobe tudo: DB + API + Front"
+ @echo " make down Para tudo e remove containers"
+ @echo " make db Sobe apenas o PostgreSQL"
+ @echo " make migrate Roda as migrations"
+ @echo " make api Sobe apenas a API Go"
+ @echo " make front Sobe apenas o front Nuxt"
+ @echo " make logs Tail dos logs da API"
+ @echo ""
+
+# ─── Infra ───────────────────────────────────────────────────────────────────
+db:
+ cd back-end && docker compose up -d
+ @echo "⏳ Aguardando PostgreSQL ficar pronto..."
+ @until docker exec $$(cd back-end && docker compose ps -q postgres) pg_isready -U licitatche > /dev/null 2>&1; do sleep 1; done
+ @echo "✅ PostgreSQL pronto"
+
+migrate: db
+ cd back-end && $(MIGRATE) -path migrations -database "postgres://licitatche:licitatche@localhost:5432/licitatche?sslmode=disable" up
+
+down:
+ cd back-end && docker compose down
+
+# ─── Serviços ────────────────────────────────────────────────────────────────
+api:
+ cd back-end && go run ./cmd/api/...
+
+front:
+ cd front-end/app && npm run dev
+
+# ─── Tudo junto (processos paralelos) ────────────────────────────────────────
+up: migrate
+ @echo "🚀 Iniciando API e Front em paralelo..."
+ @trap 'kill 0' SIGINT SIGTERM; \
+ (cd back-end && go run ./cmd/api/... 2>&1 | sed 's/^/[api] /') & \
+ (cd front-end/app && npm run dev 2>&1 | sed 's/^/[front] /') & \
+ wait
+
+logs:
+ cd back-end && docker compose logs -f
+
+# ─── Aliases ─────────────────────────────────────────────────────────────────
+run: up
+stop: down
diff --git a/back-end b/back-end
index 6480a28..ffa085e 160000
--- a/back-end
+++ b/back-end
@@ -1 +1 @@
-Subproject commit 6480a285f5a0ad3f0c96500f4bb6fe2403ffb28a
+Subproject commit ffa085e6ade3e1fd96cb882fff5e1b71c4f683cc
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..c16540c
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,100 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: ${DB_USER:-licitatche}
+ POSTGRES_PASSWORD: ${DB_PASSWORD:-licitatche}
+ POSTGRES_DB: ${DB_NAME:-licitatche}
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-licitatche}"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ networks:
+ - internal
+
+ migrate:
+ image: migrate/migrate:v4.17.0
+ depends_on:
+ postgres:
+ condition: service_healthy
+ volumes:
+ - ./back-end/migrations:/migrations
+ command: [
+ "-path", "/migrations",
+ "-database", "postgres://${DB_USER:-licitatche}:${DB_PASSWORD:-licitatche}@postgres:5432/${DB_NAME:-licitatche}?sslmode=disable",
+ "up"
+ ]
+ networks:
+ - internal
+
+ api:
+ build:
+ context: ./back-end
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ migrate:
+ condition: service_completed_successfully
+ environment:
+ APP_PORT: "8080"
+ ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:3000}
+ DB_HOST: postgres
+ DB_PORT: "5432"
+ DB_USER: ${DB_USER:-licitatche}
+ DB_PASSWORD: ${DB_PASSWORD:-licitatche}
+ DB_NAME: ${DB_NAME:-licitatche}
+ JWT_SECRET: ${JWT_SECRET}
+ JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
+ OPENAI_API_KEY: ${OPENAI_API_KEY:-}
+ FILES_DIR: /app/data/files
+ volumes:
+ - api_files:/app/data/files
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/editais", "-o", "/dev/null", "-s", "-w", "%{http_code}", "||", "true"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+ networks:
+ - internal
+
+ front:
+ build:
+ context: ./front-end
+ args:
+ NUXT_PUBLIC_API_BASE: ${PUBLIC_API_BASE:-http://163.176.236.167:8080/api/v1}
+ restart: unless-stopped
+ depends_on:
+ - api
+ environment:
+ NUXT_PUBLIC_API_BASE: ${PUBLIC_API_BASE:-http://163.176.236.167:8080/api/v1}
+ ports:
+ - "3000:3000"
+ networks:
+ - internal
+
+ nginx:
+ image: nginx:alpine
+ restart: unless-stopped
+ depends_on:
+ - api
+ - front
+ ports:
+ - "80:80"
+ volumes:
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ networks:
+ - internal
+
+volumes:
+ postgres_data:
+ api_files:
+
+networks:
+ internal:
+ driver: bridge
diff --git a/edital-real.pdf b/edital-real.pdf
new file mode 100644
index 0000000..12c4f40
Binary files /dev/null and b/edital-real.pdf differ
diff --git a/edital-teste.pdf b/edital-teste.pdf
new file mode 100644
index 0000000..359059c
--- /dev/null
+++ b/edital-teste.pdf
@@ -0,0 +1,74 @@
+%PDF-1.3
+% ReportLab Generated PDF document (opensource)
+1 0 obj
+<<
+/F1 2 0 R /F2 3 0 R
+>>
+endobj
+2 0 obj
+<<
+/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
+>>
+endobj
+3 0 obj
+<<
+/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
+>>
+endobj
+4 0 obj
+<<
+/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
+/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>> /Rotate 0 /Trans <<
+
+>>
+ /Type /Page
+>>
+endobj
+5 0 obj
+<<
+/PageMode /UseNone /Pages 7 0 R /Type /Catalog
+>>
+endobj
+6 0 obj
+<<
+/Author (anonymous) /CreationDate (D:20260323201142-03'00') /Creator (anonymous) /Keywords () /ModDate (D:20260323201142-03'00') /Producer (ReportLab PDF Library - \(opensource\))
+ /Subject (unspecified) /Title (untitled) /Trapped /False
+>>
+endobj
+7 0 obj
+<<
+/Count 1 /Kids [ 4 0 R ] /Type /Pages
+>>
+endobj
+8 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 564
+>>
+stream
+GasJO:N)aW&B4,6'^sC9>HM"s/DHED-Da_=,cnGMpAm@<;o$m@rVR]8U^9pkKS7:ZqosBB6UL8ga*Z>:QNag)-"Mto*c`(a:g>4qcGk/=\KC.0U\MAPB7ps#T+Z^L_>E7Aqf^&C?"_,*^\rVHj#2MA-/2P?q[Y(r;1/:sID+G<)c,,a`7,J[VDUPNB.ObU#PC\BR1$-aZo;#_D%nFiiop":=_+oMcF0UUs-N1-k18Xh%Q;;bGAk-n=KOCmrh-Pkf*>4)C!S[:/(b=P3C;V`mJI<=5J]u2OFnO%3"9t^@$Lm;d[=KuPI=F.gJT&oi-#e%-e6cko>*7G=,PHg`"C<6#pD2nX8qBGJ"]-F0ZUo/Gq_F!$CP_Lq()FroO=daPhLiJ;2Y/.D07V;qH1*%hR\Ga=1hg/5'jG_U=-/WkMYIcfE^4:hOu]UC0k>$Nn7]0"$rTH%VgIVW*t6_nL6JjkU\FT3*BVhK'DWcjVrYIe\,\M[>$KVVJWhejB#ZXE9P&LG:b&FK;bN-_f.Vp3t-rLY_!.%M^Ug1Gf"206;]N^Wh$4)#~>endstream
+endobj
+xref
+0 9
+0000000000 65535 f
+0000000061 00000 n
+0000000102 00000 n
+0000000209 00000 n
+0000000321 00000 n
+0000000524 00000 n
+0000000592 00000 n
+0000000853 00000 n
+0000000912 00000 n
+trailer
+<<
+/ID
+[]
+% ReportLab generated PDF document -- digest (opensource)
+
+/Info 6 0 R
+/Root 5 0 R
+/Size 9
+>>
+startxref
+1566
+%%EOF
diff --git a/front-end/.dockerignore b/front-end/.dockerignore
new file mode 100644
index 0000000..6ac1524
--- /dev/null
+++ b/front-end/.dockerignore
@@ -0,0 +1,5 @@
+node_modules
+.nuxt
+.data
+app
+public
diff --git a/front-end/Dockerfile b/front-end/Dockerfile
new file mode 100644
index 0000000..fb21a1d
--- /dev/null
+++ b/front-end/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:22-alpine
+
+RUN apk add --no-cache curl
+ENV NODE_ENV=production
+ENV TZ=America/Sao_Paulo
+
+WORKDIR /app
+COPY .output ./.output
+
+RUN cd .output/server && npm install --omit=dev
+
+EXPOSE 3000
+CMD ["node", ".output/server/index.mjs"]
diff --git a/front-end/app/components/AppSidebar.vue b/front-end/app/components/AppSidebar.vue
index f042232..d749830 100644
--- a/front-end/app/components/AppSidebar.vue
+++ b/front-end/app/components/AppSidebar.vue
@@ -1,21 +1,33 @@
-
-
- {{ modalidadeLabel[row.original.modalidade] }}
-
-
- {{ new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(row.original.valorEstimado) }}
-
-
-
-
-
- {{ row.original.dataAbertura.toLocaleDateString('pt-BR') }}
-
-
+
+
+
+ | Nº Edital |
+ Órgão |
+ Objeto |
+ Modalidade |
+ Valor Est. |
+ Abertura |
+ Status |
+
+
+
+
+ | Nenhum edital encontrado. |
+
+
+ | {{ e.Numero }} |
+ {{ e.Orgao }} |
+ {{ e.Objeto }} |
+ {{ MODALIDADE_LABEL[e.Modalidade] ?? e.Modalidade }} |
+ {{ formatBRL(e.ValorEstimado) }} |
+ {{ formatDate(e.DataAbertura) }} |
+
+ {{ STATUS_CFG[e.Status].label }}
+ {{ e.Status }}
+ |
+
+
+
+
+
diff --git a/front-end/app/composables/useApi.ts b/front-end/app/composables/useApi.ts
index 55dba40..ea0402c 100644
--- a/front-end/app/composables/useApi.ts
+++ b/front-end/app/composables/useApi.ts
@@ -1,16 +1,45 @@
// Wrapper sobre $fetch que injeta o Authorization header automaticamente.
+// Em caso de 401, tenta renovar o token via refresh e repete a requisição.
export function useApi() {
const { public: { apiBase } } = useRuntimeConfig()
const token = useCookie('auth_token')
+ const refreshToken = useCookie('refresh_token')
- function apiFetch(path: string, options: Parameters[1] = {}): Promise {
- return $fetch(`${apiBase}${path}`, {
+ async function tryRefresh(): Promise {
+ if (!refreshToken.value) return false
+ try {
+ const res = await $fetch<{ access_token: string; refresh_token: string }>(
+ `${apiBase}/auth/refresh`,
+ { method: 'POST', body: { refresh_token: refreshToken.value } }
+ )
+ token.value = res.access_token
+ refreshToken.value = res.refresh_token
+ return true
+ } catch {
+ token.value = null
+ refreshToken.value = null
+ navigateTo('/login')
+ return false
+ }
+ }
+
+ async function apiFetch(path: string, options: Parameters[1] = {}): Promise {
+ const doFetch = () => $fetch(`${apiBase}${path}`, {
...options,
headers: {
...(options.headers as Record || {}),
...(token.value ? { Authorization: `Bearer ${token.value}` } : {}),
},
})
+
+ try {
+ return await doFetch()
+ } catch (err: any) {
+ if (err?.status === 401 && await tryRefresh()) {
+ return await doFetch()
+ }
+ throw err
+ }
}
return { apiFetch }
diff --git a/front-end/app/composables/useAuth.ts b/front-end/app/composables/useAuth.ts
index 87eacfc..1bff5e0 100644
--- a/front-end/app/composables/useAuth.ts
+++ b/front-end/app/composables/useAuth.ts
@@ -4,6 +4,17 @@ interface AuthUser {
papel: string
}
+export interface TenantOption {
+ id: string
+ name: string
+ slug: string
+}
+
+export type LoginResult =
+ | { success: true }
+ | { success: false; error: string }
+ | { needsTenantSelect: true; tenants: TenantOption[]; email: string; password: string }
+
export function useAuth() {
const { public: { apiBase } } = useRuntimeConfig()
@@ -23,23 +34,48 @@ export function useAuth() {
}
}
- async function login(email: string, password: string, slug: string): Promise<{ success: boolean; error?: string }> {
+ function _applyTokens(access: string, refresh: string) {
+ token.value = access
+ refreshToken.value = refresh
+ const payload = JSON.parse(atob(access.split('.')[1]))
+ user.value = { nome: payload.email.split('@')[0], email: payload.email, papel: payload.role }
+ }
+
+ async function login(email: string, password: string): Promise {
try {
- const res = await $fetch<{ access_token: string; refresh_token: string }>(`${apiBase}/auth/login`, {
+ const res = await $fetch<{
+ tokens?: { access_token: string; refresh_token: string }
+ tenants?: TenantOption[]
+ }>(`${apiBase}/auth/login`, {
method: 'POST',
- body: { email, password, slug },
+ body: { email, password },
})
- token.value = res.access_token
- refreshToken.value = res.refresh_token
+ if (res.tokens) {
+ _applyTokens(res.tokens.access_token, res.tokens.refresh_token)
+ return { success: true }
+ }
- const payload = JSON.parse(atob(res.access_token.split('.')[1]))
- user.value = { nome: payload.email.split('@')[0], email: payload.email, papel: payload.role }
+ if (res.tenants && res.tenants.length > 0) {
+ return { needsTenantSelect: true, tenants: res.tenants, email, password }
+ }
+ return { success: false, error: 'Resposta inesperada do servidor.' }
+ } catch (err: any) {
+ return { success: false, error: err?.data?.error || 'E-mail ou senha incorretos.' }
+ }
+ }
+
+ async function selectTenant(email: string, password: string, tenantId: string): Promise<{ success: boolean; error?: string }> {
+ try {
+ const res = await $fetch<{ access_token: string; refresh_token: string }>(`${apiBase}/auth/login/select-tenant`, {
+ method: 'POST',
+ body: { email, password, tenant_id: tenantId },
+ })
+ _applyTokens(res.access_token, res.refresh_token)
return { success: true }
} catch (err: any) {
- const msg = err?.data?.error || 'E-mail, senha ou organização incorretos.'
- return { success: false, error: msg }
+ return { success: false, error: err?.data?.error || 'Erro ao selecionar empresa.' }
}
}
@@ -57,5 +93,5 @@ export function useAuth() {
navigateTo('/login')
}
- return { user, isAuthenticated, login, logout }
+ return { user, isAuthenticated, login, selectTenant, logout }
}
diff --git a/front-end/app/pages/gestao/documentos.vue b/front-end/app/pages/gestao/documentos.vue
index 43118f4..480265b 100644
--- a/front-end/app/pages/gestao/documentos.vue
+++ b/front-end/app/pages/gestao/documentos.vue
@@ -1,6 +1,8 @@
+
-
-
-
-
-
{{ prazo.titulo }}
-
{{ prazo.descricao }}
-
-
-
- {{ urgenciaConfig[prazo.urgencia].label }}
-
-
{{ prazo.dataLimite.toLocaleDateString('pt-BR') }} · {{ diasRestantes(prazo.dataLimite) }}
-
+
+
+
+ {{ stats.vencidos }}
+ Vencidos
+
+ {{ stats.hoje }}
+ Hoje
+
+
+ {{ stats.semana }}
+ Próx. 7 dias
+
+
+ {{ stats.mes }}
+ Próx. 30 dias
+
+
+ {{ stats.total }}
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nenhum prazo encontrado para os filtros selecionados.
+
+
+
+
+
+
+
+
+
diff --git a/front-end/app/pages/index.vue b/front-end/app/pages/index.vue
index 55365fe..a551bcf 100644
--- a/front-end/app/pages/index.vue
+++ b/front-end/app/pages/index.vue
@@ -1,65 +1,232 @@
- Importar Edital
- + Nova Oportunidade
+ Ver Oportunidades
-
-
-
-
+
+
Total de Editais
+
{{ totalEditais }}
+
Cadastrados
+
+
+
Taxa de Vitória
+
{{ taxaVitoria }}%
+
Processos finalizados
+
+
+
Contratos Ativos
+
{{ formatBRL(valorContratosAtivos) }}
+
{{ contratos.filter(c => c.Status === 'ativo').length }} contratos
+
+
+
Alertas Ativos
+
{{ alertasAtivos }}
+
Requerem atenção
+
@@ -68,8 +235,8 @@ const modalidadeLabel: Record
= {
Pipeline de Oportunidades
Ver kanban →
-
@@ -81,33 +248,49 @@ const modalidadeLabel: Record
= {
Alertas de Prazo
Ver todos →
-
-
+
+
Nenhum prazo urgente nos próximos 30 dias
+
+
+
{{ p.titulo }}
-
{{ p.descricao }}
+
{{ p.sub }}
- {{ urgenciaLabel(p) }}
-
+
+ {{ diasLabel(p.diff) }}
+ {{ formatDate(p.dataIso) }}
+
+
-
-
-
{{ d.nome }}
-
- {{ d.status === 'vencida' ? 'Vencida' : 'Vencendo em breve' }}
-
-
-
- {{ d.status === 'vencida' ? 'Vencida' : 'Vencendo' }}
-
+
+
Todos os documentos estão em dia
+
+
+
{{ d.Nome || TIPO_DOC[d.Tipo] || d.Tipo }}
+
{{ TIPO_DOC[d.Tipo] || d.Tipo }} · {{ formatDate(d.DataVencimento!) }}
+
+ {{ docStatus(d).label }}
+
@@ -117,57 +300,134 @@ const modalidadeLabel: Record
= {
Editais Recentes
Ver todos →
-
-
- {{ modalidadeLabel[row.original.modalidade] }}
-
-
- {{ new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(row.original.valorEstimado) }}
-
-
-
-
-
- {{ row.original.dataAbertura.toLocaleDateString('pt-BR') }}
-
-
+
+
Nenhum edital cadastrado ainda
+
+
+
+
+ | Nº Edital |
+ Órgão |
+ Objeto |
+ Modalidade |
+ Valor Est. |
+ Abertura |
+ Status |
+
+
+
+
+ | {{ e.Numero }} |
+ {{ e.Orgao }} |
+ {{ e.Objeto }} |
+ {{ MODALIDADE[e.Modalidade] ?? e.Modalidade }} |
+ {{ formatBRL(e.ValorEstimado) }} |
+ {{ formatDate(e.DataAbertura) }} |
+
+ {{ STATUS_CFG[e.Status].label }}
+ |
+
+
+
diff --git a/front-end/app/pages/login.vue b/front-end/app/pages/login.vue
index 09d0c06..e088f40 100644
--- a/front-end/app/pages/login.vue
+++ b/front-end/app/pages/login.vue
@@ -1,23 +1,50 @@
@@ -52,17 +79,6 @@ async function handleSubmit() {
/>
-
-
-
-
@@ -84,6 +100,29 @@ async function handleSubmit() {
+
+
+
+
+
Selecione a empresa
+
Seu acesso está vinculado a mais de uma empresa.
+
+
+
+
+
+
{{ tenantError }}
+
+
diff --git a/front-end/app/pages/oportunidades/[id].vue b/front-end/app/pages/oportunidades/[id].vue
new file mode 100644
index 0000000..9176522
--- /dev/null
+++ b/front-end/app/pages/oportunidades/[id].vue
@@ -0,0 +1,1097 @@
+
+
+
+
+
+
+
+
+
+
+
Oportunidades / Detalhe
+
{{ edital?.Numero }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ STATUS_CFG[edital.Status]?.icon ?? '•' }}
+ {{ STATUS_CFG[edital.Status]?.label ?? edital.Status }}
+
+ {{ MODALIDADE_LABEL[edital.Modalidade] ?? edital.Modalidade }}
+
+
+
+
{{ edital.Orgao }}
+
{{ edital.Objeto }}
+
+
+
+ Valor Estimado
+ {{ formatBRL(edital.ValorEstimado) }}
+
+
+
+ Publicação
+ {{ formatDate(edital.DataPublicacao) }}
+
+
+
+ Abertura
+
+ {{ formatDate(edital.DataAbertura) }}
+ {{ formatDateRelative(edital.DataAbertura) }}
+
+
+
+
+ Plataforma
+ {{ edital.Plataforma || '—' }}
+
+
+
+
+
+
+
+ Progresso no Pipeline
+
+
+
+
+
+
+
+ Informações do Edital
+
+
+ Número
+ {{ edital.Numero }}
+
+
+ Órgão
+ {{ edital.Orgao }}
+
+
+ Modalidade
+ {{ MODALIDADE_LABEL[edital.Modalidade] ?? edital.Modalidade }}
+
+
+ Plataforma
+ {{ edital.Plataforma || '—' }}
+
+
+ Valor Estimado
+ {{ formatBRL(edital.ValorEstimado) }}
+
+
+ Data Publicação
+ {{ formatDate(edital.DataPublicacao) }}
+
+
+ Data Abertura
+ {{ formatDate(edital.DataAbertura) }}
+
+
+ Status
+ {{ STATUS_CFG[edital.Status]?.label ?? edital.Status }}
+
+
+ Objeto
+ {{ edital.Objeto }}
+
+
+
+
+
+
+
+
+
+
+
Nenhum arquivo anexado
+
+
+
+
+
+
+ {{ fileExtension(f.Nome) }}
+
+
+ {{ f.Nome }}
+ {{ formatFileSize(f.Size) }} · {{ formatDate(f.CreatedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+ {{ t.icon }}
+ {{ t.message }}
+
+
+
+
+
+
+
+
+
+
+
Excluir Oportunidade
+
+ Tem certeza que deseja excluir o edital
+ {{ edital?.Numero }}?
+
Esta ação não pode ser desfeita.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ erroModal }}
+
+
+
+
+
+
+
+
+
diff --git a/front-end/app/pages/oportunidades/edital-publicado.vue b/front-end/app/pages/oportunidades/edital-publicado.vue
new file mode 100644
index 0000000..6b13c39
--- /dev/null
+++ b/front-end/app/pages/oportunidades/edital-publicado.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/front-end/app/pages/oportunidades/elaborando-proposta.vue b/front-end/app/pages/oportunidades/elaborando-proposta.vue
index 35c1411..9c98464 100644
--- a/front-end/app/pages/oportunidades/elaborando-proposta.vue
+++ b/front-end/app/pages/oportunidades/elaborando-proposta.vue
@@ -1,11 +1,13 @@
diff --git a/front-end/app/pages/oportunidades/index.vue b/front-end/app/pages/oportunidades/index.vue
index cc9d66c..0fec73f 100644
--- a/front-end/app/pages/oportunidades/index.vue
+++ b/front-end/app/pages/oportunidades/index.vue
@@ -1,19 +1,451 @@
+
- + Nova Oportunidade
+
+
+
+
{{ importError }}
-
+
+
+
+ | Nº Edital |
+ Órgão |
+ Objeto |
+ Modalidade |
+ Valor Est. |
+ Abertura |
+ Status |
+ |
+
+
+
+
+ | Nenhum edital cadastrado. |
+
+
+ | {{ e.Numero }} |
+ {{ e.Orgao }} |
+ {{ e.Objeto }} |
+ {{ MODALIDADE_LABEL[e.Modalidade] ?? e.Modalidade }} |
+ {{ formatBRL(e.ValorEstimado) }} |
+ {{ formatDate(e.DataAbertura) }} |
+
+ {{ STATUS_CFG[e.Status].label }}
+ {{ e.Status }}
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📄
+
+
Importado do PDF: {{ importedFileName }}
+
A IA extraiu os dados abaixo. Complete os campos que ficaram vazios e confira os preenchidos.
+
+
+
+
+
+
+
+
+
+
+
{{ erroModal }}
+
+
+
+
+
+
+
+
+
+
+
+
Excluir Oportunidade
+
+ Tem certeza que deseja excluir o edital
+ {{ deleteTarget?.Numero }}?
+
Esta ação não pode ser desfeita.
+
+
+
+
+
+
+
+
@@ -21,5 +453,119 @@ import { editais } from '~/data/mock/editais'
.page { display: flex; flex-direction: column; height: 100vh; }
.content { padding: 20px 22px; flex: 1; overflow-y: auto; }
.card { background: white; border-radius: 11px; border: 1px solid #e2e8f0; overflow: hidden; }
-.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2) !important; }
+
+.btn-import {
+ background: linear-gradient(135deg, #10b981, #059669);
+ color: white; border: none; border-radius: 7px;
+ padding: 7px 16px; font-size: 13px; font-weight: 600; cursor: pointer;
+ margin-right: 8px;
+}
+.btn-import:hover { opacity: 0.9; }
+.btn-import.disabled { opacity: 0.6; pointer-events: none; }
+
+.btn-primary {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white; border: none; border-radius: 7px;
+ padding: 7px 16px; font-size: 13px; font-weight: 600; cursor: pointer;
+}
+.btn-primary:hover { opacity: 0.9; }
+
+.import-error {
+ background: #fef2f2; color: #dc2626; border: 1px solid #fecaca;
+ border-radius: 8px; padding: 10px 16px; font-size: 13px; margin-bottom: 12px;
+}
+
+/* table */
+.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
+.tbl thead tr { border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
+.tbl th { padding: 10px 14px; text-align: left; font-weight: 600; color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: .4px; white-space: nowrap; }
+.tbl td { padding: 11px 14px; color: #1e293b; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
+.tbl tbody tr:last-child td { border-bottom: none; }
+.tbl tbody tr:hover td { background: #f8fafc; }
+.numero { font-weight: 600; white-space: nowrap; }
+.numero.link { color: #667eea; cursor: pointer; }
+.numero.link:hover { color: #764ba2; text-decoration: underline; }
+.objeto { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.empty { text-align: center; color: #94a3b8; padding: 40px; }
+.actions-cell { text-align: right; white-space: nowrap; }
+.btn-menu { background: none; border: none; cursor: pointer; font-size: 18px; color: #94a3b8; padding: 2px 6px; border-radius: 4px; }
+.btn-menu:hover { background: #f1f5f9; color: #475569; }
+.badge { display: inline-block; padding: 3px 9px; border-radius: 20px; font-size: 11.5px; font-weight: 600; }
+
+/* dropdown */
+.overlay-menu { position: fixed; inset: 0; z-index: 999; }
+.dropdown { position: fixed; z-index: 1000; background: white; border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,.12); min-width: 140px; overflow: hidden; }
+.dropdown button { display: block; width: 100%; padding: 9px 14px; text-align: left; background: none; border: none; cursor: pointer; font-size: 13px; color: #334155; }
+.dropdown button:hover { background: #f8fafc; }
+.drop-danger { color: #dc2626 !important; }
+
+/* modal */
+.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 1000; display: flex; align-items: center; justify-content: center; }
+.modal { background: white; border-radius: 12px; width: 600px; max-width: 95vw; max-height: 90vh; overflow-y: auto; box-shadow: 0 8px 40px rgba(0,0,0,.2); }
+.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid #e2e8f0; }
+.modal-header h3 { margin: 0; font-size: 15px; font-weight: 700; color: #1e293b; }
+.close-btn { background: none; border: none; cursor: pointer; font-size: 16px; color: #64748b; }
+.modal-body { padding: 20px 22px; display: flex; flex-direction: column; gap: 18px; }
+.modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid #f1f5f9; }
+.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+.field { display: flex; flex-direction: column; gap: 5px; }
+.field label { font-size: 12px; font-weight: 600; color: #475569; }
+.inp { border: 1px solid #e2e8f0; border-radius: 7px; padding: 8px 10px; font-size: 13px; color: #1e293b; width: 100%; box-sizing: border-box; }
+.inp:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,.1); }
+.btn-cancel { background: #f1f5f9; color: #475569; border: none; border-radius: 7px; padding: 8px 18px; font-size: 13px; font-weight: 600; cursor: pointer; }
+.btn-save { background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 7px; padding: 8px 18px; font-size: 13px; font-weight: 600; cursor: pointer; }
+.btn-save:disabled { opacity: .6; cursor: not-allowed; }
+.erro-msg { color: #dc2626; font-size: 12px; margin: 0; }
+
+/* delete confirm modal */
+.modal-delete {
+ background: white; border-radius: 16px;
+ width: 400px; max-width: 90vw;
+ padding: 32px 28px 24px;
+ box-shadow: 0 20px 60px rgba(0,0,0,.25);
+ text-align: center;
+ animation: modalPopIn .2s ease;
+}
+@keyframes modalPopIn {
+ from { opacity: 0; transform: scale(.92); }
+ to { opacity: 1; transform: scale(1); }
+}
+.modal-delete-icon {
+ width: 56px; height: 56px; border-radius: 50%;
+ background: #fef2f2; color: #dc2626;
+ display: flex; align-items: center; justify-content: center;
+ margin: 0 auto 16px;
+}
+.modal-delete-title {
+ font-size: 16px; font-weight: 700; color: #0f172a;
+ margin: 0 0 8px;
+}
+.modal-delete-text {
+ font-size: 13.5px; color: #64748b; line-height: 1.6;
+ margin: 0 0 24px;
+}
+.modal-delete-text strong { color: #1e293b; }
+.modal-delete-actions {
+ display: flex; gap: 10px; justify-content: center;
+}
+.btn-delete {
+ background: #dc2626; color: white;
+ border: none; border-radius: 9px;
+ padding: 9px 22px; font-size: 13px; font-weight: 700;
+ cursor: pointer; transition: all .15s;
+}
+.btn-delete:hover { background: #b91c1c; }
+.btn-delete:disabled { opacity: .5; cursor: not-allowed; }
+
+/* import notice */
+.import-notice {
+ display: flex; align-items: flex-start; gap: 12px;
+ padding: 14px 16px; border-radius: 10px;
+ background: #eff6ff; border: 1px solid #bfdbfe;
+ margin-bottom: 4px;
+}
+.import-notice-icon { font-size: 24px; flex-shrink: 0; margin-top: 2px; }
+.import-notice strong { font-size: 13px; color: #1e40af; }
+.import-notice p { font-size: 12px; color: #3b82f6; margin: 4px 0 0; line-height: 1.4; }
+
diff --git a/front-end/app/pages/oportunidades/participando.vue b/front-end/app/pages/oportunidades/participando.vue
index b3f36bc..3411792 100644
--- a/front-end/app/pages/oportunidades/participando.vue
+++ b/front-end/app/pages/oportunidades/participando.vue
@@ -1,11 +1,13 @@
diff --git a/front-end/app/types/index.ts b/front-end/app/types/index.ts
index 64e0ce0..01d148a 100644
--- a/front-end/app/types/index.ts
+++ b/front-end/app/types/index.ts
@@ -3,9 +3,12 @@
export type StatusEdital =
| 'em_analise'
| 'elaborando_proposta'
- | 'impugnacao'
+ | 'edital_publicado'
+ | 'fase_lances'
+ | 'habilitacao'
| 'recurso'
- | 'participando'
+ | 'adjudicado'
+ | 'contrato'
| 'vencida'
| 'perdida'
| 'deserta'
@@ -90,23 +93,26 @@ export interface Usuario {
}
export const STATUS_EDITAL_CONFIG: Record = {
- em_analise: { label: 'Em Análise', bg: '#eff6ff', color: '#0284c7' },
- elaborando_proposta: { label: 'Elaborando Proposta', bg: '#faf5ff', color: '#7c3aed' },
- impugnacao: { label: 'Impugnação', bg: '#fff7ed', color: '#ea580c' },
- recurso: { label: 'Recurso', bg: '#fffbeb', color: '#d97706' },
- participando: { label: 'Participando', bg: '#eff6ff', color: '#3b82f6' },
+ em_analise: { label: 'Mapeamento', bg: '#eff6ff', color: '#0284c7' },
+ elaborando_proposta: { label: 'Termo de Referência', bg: '#faf5ff', color: '#7c3aed' },
+ edital_publicado: { label: 'Edital Publicado', bg: '#fff7ed', color: '#ea580c' },
+ fase_lances: { label: 'Fase de Lances', bg: '#eff6ff', color: '#3b82f6' },
+ habilitacao: { label: 'Habilitação', bg: '#fef3c7', color: '#d97706' },
+ recurso: { label: 'Recursos', bg: '#fffbeb', color: '#d97706' },
+ adjudicado: { label: 'Adjudicado', bg: '#ecfdf5', color: '#059669' },
+ contrato: { label: 'Contrato', bg: '#f0fdf4', color: '#16a34a' },
vencida: { label: 'Vencida', bg: '#f0fdf4', color: '#16a34a' },
perdida: { label: 'Perdida', bg: '#fef2f2', color: '#dc2626' },
deserta: { label: 'Deserta / Fracassada', bg: '#f8fafc', color: '#64748b' },
}
export const PIPELINE_ETAPAS = [
- 'Identificado',
- 'Análise de Viabilidade',
- 'Elaborando Proposta',
- 'Disputa / Lances',
+ 'Mapeamento',
+ 'Termo de Referência',
+ 'Edital Publicado',
+ 'Fase de Lances',
'Habilitação',
- 'Recurso / Contrarrazões',
- 'Adjudicação',
+ 'Recursos',
+ 'Adjudicado',
'Contrato',
] as const
diff --git a/front-end/nuxt.config.ts b/front-end/nuxt.config.ts
index a28aaf3..a1c59d5 100644
--- a/front-end/nuxt.config.ts
+++ b/front-end/nuxt.config.ts
@@ -12,7 +12,7 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'],
routeRules: {
- '/': { prerender: true }
+ '/': { prerender: process.env.NODE_ENV !== 'production' }
},
runtimeConfig: {
@@ -21,6 +21,19 @@ export default defineNuxtConfig({
},
},
+ devServer: {
+ host: '0.0.0.0',
+ },
+
+ vite: {
+ server: {
+ allowedHosts: true,
+ hmr: {
+ protocol: 'ws',
+ },
+ },
+ },
+
compatibilityDate: '2025-01-15',
eslint: {
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..29bccb4
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,35 @@
+upstream frontend {
+ server front:3000;
+}
+
+upstream backend {
+ server api:8080;
+}
+
+server {
+ listen 80;
+ server_name _;
+
+ client_max_body_size 50M;
+
+ # API
+ location /api/ {
+ proxy_pass http://backend;
+ 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;
+ }
+
+ # Frontend
+ location / {
+ proxy_pass http://frontend;
+ 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_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+}
diff --git a/plano.pdf b/plano.pdf
new file mode 100644
index 0000000..b7dd027
Binary files /dev/null and b/plano.pdf differ
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
new file mode 100755
index 0000000..fe1878d
--- /dev/null
+++ b/scripts/deploy.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ─── Config ──────────────────────────────────────────────────────────────────
+SERVER="opc@163.176.236.167"
+REMOTE_DIR="/home/opc/licitatche"
+PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+
+echo "🚀 Deploy Licitatche → $SERVER"
+echo ""
+
+# ─── 1. Build Go binary para linux/arm64 ────────────────────────────────────
+echo "🔨 [1/6] Compilando API Go para linux/arm64..."
+cd "$PROJECT_DIR/back-end"
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o api-linux-arm64 ./cmd/api/...
+echo " ✅ Binário Go compilado ($(du -h api-linux-arm64 | cut -f1))"
+
+# ─── 2. Build Frontend Nuxt ─────────────────────────────────────────────────
+echo ""
+echo "🔨 [2/6] Compilando Frontend Nuxt..."
+cd "$PROJECT_DIR/front-end"
+NUXT_PUBLIC_API_BASE="http://163.176.236.167/api/v1" npx nuxi build 2>&1 | tail -5
+echo " ✅ Frontend compilado"
+
+# ─── 3. Verificar Docker no servidor ────────────────────────────────────────
+echo ""
+echo "📦 [3/6] Verificando Docker no servidor..."
+ssh "$SERVER" 'bash -s' <<'INSTALL_DOCKER'
+if command -v docker &>/dev/null && docker compose version &>/dev/null; then
+ echo " ✅ Docker e Docker Compose já instalados"
+else
+ echo " 📥 Instalando Docker..."
+ sudo dnf install -y dnf-utils 2>/dev/null || true
+ sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo 2>/dev/null || true
+ sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null
+ sudo systemctl enable docker
+ sudo systemctl start docker
+ sudo usermod -aG docker "$USER"
+ echo " ✅ Docker instalado"
+fi
+INSTALL_DOCKER
+
+# ─── 4. Enviar arquivos ─────────────────────────────────────────────────────
+echo ""
+echo "📤 [4/6] Enviando projeto para o servidor..."
+ssh "$SERVER" "mkdir -p $REMOTE_DIR"
+
+rsync -avz --progress --delete \
+ --exclude 'node_modules' \
+ --exclude '.nuxt' \
+ --exclude '.data' \
+ --exclude '.git' \
+ --exclude '*.pdf' \
+ --exclude 'back-end/.env' \
+ --exclude 'front-end/app' \
+ --exclude 'front-end/public' \
+ --exclude 'vendor' \
+ "$PROJECT_DIR/" "$SERVER:$REMOTE_DIR/"
+
+# ─── 5. Enviar .env de produção ─────────────────────────────────────────────
+echo ""
+echo "🔐 [5/6] Configurando variáveis de ambiente..."
+scp "$PROJECT_DIR/.env.prod" "$SERVER:$REMOTE_DIR/.env"
+
+# ─── 6. Build containers e start ────────────────────────────────────────────
+echo ""
+echo "🐳 [6/6] Construindo e iniciando containers..."
+ssh "$SERVER" "bash -s" </dev/null || true
+sudo firewall-cmd --permanent --add-port=3000/tcp 2>/dev/null || true
+sudo firewall-cmd --permanent --add-port=8080/tcp 2>/dev/null || true
+sudo firewall-cmd --reload 2>/dev/null || true
+sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || true
+
+# Build & deploy
+docker compose -f docker-compose.prod.yml down 2>/dev/null || true
+docker compose -f docker-compose.prod.yml build
+docker compose -f docker-compose.prod.yml up -d
+
+echo ""
+echo "⏳ Aguardando serviços ficarem prontos..."
+sleep 15
+
+echo ""
+echo "=== Status dos containers ==="
+docker compose -f docker-compose.prod.yml ps
+echo ""
+echo "=== Logs API (últimas 10 linhas) ==="
+docker compose -f docker-compose.prod.yml logs --tail=10 api 2>&1 || true
+echo ""
+echo "=== Logs Frontend (últimas 10 linhas) ==="
+docker compose -f docker-compose.prod.yml logs --tail=10 front 2>&1 || true
+echo ""
+echo "=== Logs Migrate ==="
+docker compose -f docker-compose.prod.yml logs migrate 2>&1 || true
+
+echo ""
+echo "✅ Deploy concluído!"
+echo "🌐 Acesse: http://163.176.236.167"
+REMOTE_START
+
+# Limpar binário local
+rm -f "$PROJECT_DIR/back-end/api-linux-arm64"
+
+echo ""
+echo "═══════════════════════════════════════════"
+echo " ✅ Deploy finalizado!"
+echo " 🌐 http://163.176.236.167"
+echo "═══════════════════════════════════════════"
diff --git a/visualizar-pdf-btn.png b/visualizar-pdf-btn.png
new file mode 100644
index 0000000..4357b7d
Binary files /dev/null and b/visualizar-pdf-btn.png differ