--- 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. layer: wiki --- # 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 | | `width: "100%"` em bars/elementos dentro de flex container | Não resolve — fica invisível | Usar `alignSelf: "stretch"` no elemento e no container pai | | Imagens com path absoluto (`/home/...`) | react-pdf não acede ao filesystem local | Colocar em `public/assets/` e referenciar como `"assets/foto.png"` (s