--- name: proposta-visual description: Criação completa de propostas comerciais visuais — página web (propostas.descomplicar.pt) + PDF exportável + PPTX editável. Recolhe dados CRM, gera JSON, renderiza condicionalmente e faz deploy. --- # Skill /proposta-visual — Propostas Comerciais Visuais Cria propostas comerciais profissionais com 3 outputs: página web interactiva, PDF exportável e PPTX editável. ## Triggers - `/proposta-visual` - "proposta visual", "proposta web", "criar proposta", "nova proposta", "proposta para [cliente]" ## Quando usar - Criar proposta comercial completa para um cliente - Gerar página web + PDF + PPTX de uma proposta - Actualizar proposta existente com novos dados ## Quando NÃO usar - Apenas orçamento sem visual (usar `/orcamento`) - Apenas PPTX sem página web (usar `/proposal-deck`) - Análise de lead sem proposta (usar `/lead-approach`) --- ## Projecto base | Campo | Valor | |-------|-------| | **Código fonte** | `/media/ealmeida/Dados/Dev/Propostas/` | | **Stack** | React 18 + Vite + Tailwind + @react-pdf/renderer + Recharts + Framer Motion + Lucide React | | **Live** | `propostas.descomplicar.pt/{slug}` | | **Deploy** | Docker Swarm EasyPanel, serviço `descomplicar_propostas`, ficheiros `/opt/propostas/` | | **DNS** | `propostas.descomplicar.pt` → 5.9.90.70 (Cloudflare proxied) | | **Template PPTX** | `/media/ealmeida/Dados/Hub/90-Templates/Comercial/descomplicar-proposal-template.pptx` | --- ## REGRAS CRÍTICAS (ler antes de qualquer implementação) ### 1. PT-PT obrigatório — acentos em TODO o texto Todos os textos no JSON, componentes web e componentes PDF DEVEM ter acentos PT-PT correctos: - ✓ migração, tradução, integração, opções, condições, execução, acção - ✗ migracao, traducao, integracao, opcoes, condicoes, execucao, accao **Verificar no PDF:** labels hardcoded em `PDFWorkDone.jsx` e `PDFDocument.jsx` — já corrigidos mas confirmar sempre. ### 2. JSON data-driven — renderização condicional obrigatória O `ProposalView.jsx` e o `PDFDocument.jsx` renderizam secções **condicionalmente** com base nos dados. Se um campo é `null`, `[]` ou não existe, a secção NÃO aparece. **Regra:** NUNCA assumir que todos os campos existem. Cada proposta pode ter um subconjunto diferente de secções. | Campo JSON | Secção web | Secção PDF | Condição | |-----------|-----------|-----------|----------| | `context.brandStrategy` | BrandStrategy | PageBrandStrategy | `!= null` | | `salesData` | SalesChart | PageSales | `.length > 0` | | `context.diagnosis` | Context | — | `.length > 0` | | `context.scores` | ScoreGrid | PDFScores | `.length > 0` | | `service360` | Service360 | PageService360 | `!= null` | | `plans` | PricingCards | PDFPricing | `.length > 0` | | `licenses` | LicensePack | PageLicenses | `!= null` | | `workDone` | WorkDone | PDFWorkDone | `.length > 0` | | `roadmap` | Roadmap | PDFWorkDone | `.length > 0` | | `conditions` | Conditions | PDFWorkDone (pág. separada) | `.length > 0` | ### 3. Título dinâmico — campo `title` no JSON O título da proposta (Hero web + capa PDF) vem do campo `title` no JSON: - Proposta 360°: `"title": "Descomplicar 360°"` - Migração técnica: `"title": "Migração Bookeo"` - Website: `"title": "Website Profissional"` - Fallback: `"Proposta Comercial"` **NUNCA** hardcodar "Descomplicar 360°" — usar `data.title`. ### 4. Preços flexíveis — não assumir mensalidades Os planos suportam dois formatos de preço: **Mensalidade (avença):** ```json { "monthly": 459, "hours": 8, "originalPrice": 540, "discount": 15 } ``` Mostra: "459 €/mês" + "8h/mês incluídas · contrato anual" **Preço único (investimento):** ```json { "price": 750, "priceLabel": "EUR + IVA", "priceSubtitle": "Investimento único · prazo 1-2 semanas", "monthly": null, "hours": null } ``` Mostra: "750 EUR + IVA" + "Investimento único · prazo 1-2 semanas" **Campos usados:** `plan.price ?? plan.monthly` para o valor, `plan.priceLabel ?? "€/mês"` para a unidade, `plan.priceSubtitle` para a descrição abaixo. ### 5. react-pdf — bugs conhecidos e PROIBIÇÕES | Proibição | Porquê | Alternativa | |-----------|--------|-------------| | `gap` + `flexWrap` juntos | Crasha silenciosamente | `marginRight` + `marginBottom` nos items | | `borderColor` com `rgba()` | Renderiza verde | Cor sólida (ex: `#D4A020`, `#E8E8E8`) | | Import de componentes SVG (``, ``) | Crasha web worker do PDFDownloadLink | Implementar gráficos inline com `` | | `Font.register` com fontes locais/CDN em multi-página | Falha silenciosa | Usar Helvetica/Helvetica-Bold (built-in) | | `href` vazio no PDFDownloadLink | PDF não gerou — verificar consola | Cache do browser — Ctrl+Shift+R | ### 6. Página 360° (service360) — contextualizar A secção service360 no PDF usa o label "O QUE PROPOMOS" (genérico) e o título "As nossas soluções para o seu negócio". Os dados vêm do JSON `service360.areas[]`. Para propostas que NÃO são 360°, adaptar os `areas` ao contexto: - Migração técnica: features da migração - Website: áreas do serviço web - SEO: pilares do trabalho SEO Se `service360` for `null`, a secção não aparece. ### 7. Condições comerciais — página separada no PDF As condições estão numa página PDF separada (não na mesma que o roadmap) para evitar corte entre páginas. O componente `PDFWorkDone` retorna um Fragment com 2 Pages. --- ## Protocolo ### Sintaxe ``` /proposta-visual [cliente] [tipo-serviço] ``` ### Fase 1: Análise do contexto **ANTES de criar o JSON, responder a estas perguntas:** 1. **Que tipo de proposta é?** (360°, website, migração técnica, SEO, eCommerce, outro) 2. **O preço é mensal ou único?** → define `monthly` vs `price`/`priceLabel`/`priceSubtitle` 3. **Que secções se aplicam?** → define que campos preencher no JSON 4. **Há dados quantitativos?** (vendas, scores SEO, KPIs) → define `salesData`, `context.scores` 5. **Há trabalho já realizado?** → define `workDone` 6. **Quantas opções de preço?** → define `plans[]` **Mapa de secções por tipo de proposta:** | Secção | 360° | Website | Migração | SEO | eCommerce | |--------|:----:|:-------:|:--------:|:---:|:---------:| | `title` | Descomplicar 360° | Website Profissional | [contexto] | Optimização SEO | Loja Online | | `brandStrategy` | ✓ | Opcional | — | — | Opcional | | `salesData` | Se existir | — | — | — | Se existir | | `scores` | ✓ (audit) | Opcional | — | ✓ (audit) | Opcional | | `service360` | ✓ | ✓ | ✓ | ✓ | ✓ | | `plans` | ✓ (3 opções) | ✓ | ✓ | ✓ | ✓ | | `licenses` | Se aplicável | — | — | — | Se aplicável | | `workDone` | Se existir | Se existir | ✓ | Se existir | Se existir | | `roadmap` | ✓ | ✓ | ✓ | ✓ | ✓ | | `conditions` | ✓ | ✓ | ✓ | ✓ | ✓ | ### Fase 2: Recolha de dados do CRM ``` mcp__desk-crm-v3__search_customers query="[nome]" mcp__desk-crm-v3__get_customer customer_id=[id] mcp__desk-crm-v3__get_estimates client_id=[id] ``` ### Fase 3: Criar ficheiro JSON **Localização:** `src/data/[slug].json` **Gerar slug:** iniciais do cliente + tipo + hash curto. Ex: `ccv-bookeo-a3f7c1d2` **Schema mínimo obrigatório:** ```json { "title": "Título da Proposta", "client": { "name": "Nome Legal Lda.", "brand": "Nome Comercial", "website": "exemplo.pt", "contact": "Nome Contacto", "email": "email@exemplo.pt", "phone": "+351 900 000 000" }, "company": { "name": "Descomplicar, Lda.", "brand": "Descomplicar", "nif": "514 785 691", "email": "info@descomplicar.pt", "phone": "911 510 005", "website": "descomplicar.pt" }, "date": "2026-03-23", "validDays": 30, "context": { "description": "Descrição da situação...", "diagnosis": [], "scores": [], "brandStrategy": null }, "service360": null, "plans": [], "licenses": null, "workDone": [], "roadmap": [], "conditions": [], "salesData": [], "salesProjection": [] } ``` **Campos opcionais para planos com preço único:** ```json { "price": 750, "priceLabel": "EUR + IVA", "priceSubtitle": "Investimento único · prazo 1-2 semanas", "monthly": null, "hours": null } ``` **Checklist PT-PT antes de guardar o JSON:** - [ ] Todos os textos têm acentos correctos (ã, ç, ã, é, ê, í, ó, õ, ú) - [ ] Zero brasileirismos - [ ] Caractere → (seta) em vez de -> nos textos visíveis - [ ] Monetário: "EUR" ou "€" (não "R$" nem "$") ### Fase 4: Registar no App.jsx ```jsx import novaData from "./data/[slug].json" const proposals = { // existentes... "[novo-slug]": novaData, } ``` ### Fase 5: Verificar antes do build **Checklist pré-build:** - [ ] JSON válido (sem trailing commas, sem campos undefined) - [ ] Campos null/[] para secções que não se aplicam (NÃO omitir) - [ ] `title` definido (não depender do fallback) - [ ] `plans[].price` OU `plans[].monthly` definido (não ambos null) - [ ] `plans[].priceLabel` definido se preço único - [ ] Slug registado em App.jsx - [ ] Nenhum `flexWrap` + `gap` nos componentes PDF - [ ] Nenhum `borderColor` com `rgba()` nos componentes PDF ### Fase 6: Build e deploy ```bash cd /media/ealmeida/Dados/Dev/Propostas npm run build # Limpar dist cd dist rm -rf assets/template-media assets/template-referencia.pptx assets/slide-referencia.svg cd assets/brochura && ls *.{jpg,png} 2>/dev/null | grep -v image8.jpg | xargs rm -f 2>/dev/null # Empacotar cd /media/ealmeida/Dados/Dev/Propostas/dist tar czf /tmp/propostas-dist.tar.gz . # Upload + deploy mcp__ssh-unified__sftp_upload server=easy localPath=/tmp/propostas-dist.tar.gz remotePath=/tmp/propostas-dist.tar.gz overwrite=true mcp__ssh-unified__ssh_execute server=easy command="cd /opt/propostas && rm -rf assets/* index.html; tar xzf /tmp/propostas-dist.tar.gz && docker service update --force descomplicar_propostas && rm -f /tmp/propostas-dist.tar.gz" # Verificar curl -sI https://propostas.descomplicar.pt/[slug] | head -3 # Deve retornar HTTP/2 200 # Limpar rm -f /tmp/propostas-dist.tar.gz ``` ### Fase 7: Testar PDF **OBRIGATÓRIO antes de entregar:** 1. Abrir `https://propostas.descomplicar.pt/[slug]` com Ctrl+Shift+R 2. Clicar "Exportar PDF" 3. Verificar que `href` do link não está vazio (PDF gerou) 4. Abrir PDF e verificar: - [ ] Título correcto (não "Descomplicar 360°" hardcoded) - [ ] Preços correctos (não "€/mês" se é preço único) - [ ] Sem borders verdes (rgba renderiza verde no react-pdf) - [ ] Sem secções que não se aplicam (360°, vendas, licenças, scores) - [ ] Acentos PT-PT em todo o texto - [ ] Condições comerciais numa página inteira (sem corte) - [ ] Todas as 3 opções de preço com layout uniforme **Se o PDF não gera (href vazio):** 1. Verificar consola do browser (F12) 2. Causa mais provável: import de componente SVG dos infographics 3. Verificar que PDFDocument não importa de `./infographics/` 4. Verificar que não há `gap` + `flexWrap` no PDF 5. Ctrl+Shift+R e tentar novamente (cache) ### Fase 8: Entrega e registo 1. Guardar em `Hub/03-Propostas/[Cliente]/` 2. Registar estimate no CRM (se não existe) 3. Comunicar URL ao utilizador 4. Comentar tarefa CRM --- ## Design system ACIDA 2.0 | Token | Valor | |-------|-------| | Dourado | #C88900 | | Dourado claro | #EED59F | | Dourado border PDF | #D4A020 (usar em vez de rgba no PDF) | | Dark | #262626 | | Background | #F8F8F8 | | Fonte web | Inter (Google Fonts) | | Fonte PDF | Helvetica / Helvetica-Bold | | Logo escuro | `/assets/logo-descomplicar.png` | | Logo claro | `/assets/logo-descomplicar-white.png` | --- ## Propostas existentes | Slug | Cliente | Tipo | Data | |------|---------|------|------| | `ljm-360-61a8aecc` | A Loja da Maria | 360° (mensal) | 2026-03-20 | | `ccv-bookeo-a3f7c1d2` | Carvoeiro Caves | Migração técnica (único) | 2026-03-23 | --- ## Skills relacionadas - `/proposal-deck` — PPTX standalone (16 layouts, XML editing) - `/orcamento` — Estimate no CRM (sem visual) - `/lead-approach` — Estratégia de abordagem de lead - `/crm` — Operações CRM genéricas --- ## Changelog ### v2.0.0 (2026-03-23) - Regras críticas documentadas (7 regras) - Renderização condicional obrigatória (ProposalView + PDFDocument) - Título dinâmico via campo `title` no JSON - Preços flexíveis (mensal vs único) com `price`/`priceLabel`/`priceSubtitle` - Bugs react-pdf documentados com proibições explícitas - Mapa de secções por tipo de proposta - Checklist pré-build e teste PDF obrigatório - Condições comerciais em página PDF separada ### v1.0.0 (2026-03-23) - Versão inicial --- **Versão:** 2.0.0 | **Data:** 2026-03-23 --- ## Healing Log Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar. ```jsonl {"date":"","issue":"","fix":"","source":"user|auto"} ``` *Adicionar nova linha após cada erro corrigido.*