---
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 (`<Svg>`, `<Circle>`) | Crasha web worker do PDFDownloadLink | Implementar gráficos inline com `<View>` |
| `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"` (sem `/` inicial) |
| `borderBottomWidth` no header + `borderTopWidth` em componente filho | Duplica a linha dourada visualmente | Usar apenas `borderTopWidth: 2` no próprio header; remover `borderBottom` |

**Hifenização:** Para desactivar em todo o PDF, adicionar ao `<Document>`:
```jsx
hyphenationCallback={(word) => [word]}
```

**Ícones no PDF:** Usar `PDFIcons.jsx` (`src/pdf/sections/`) com `Svg/Path/Circle/Line/Polyline` de `@react-pdf/renderer`. NÃO importar lucide-react nos componentes PDF — crasha o worker.

**Ícones na web:** Usar `lucide-react`. Padrão: `FALLBACK_ICONS[i % FALLBACK_ICONS.length]` para rotação automática por índice quando o JSON não tem campo `icon`.

### 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.

### 8. Apresentação 16:9 — PDFPresentation.jsx

Existe um segundo output PDF em formato 1920×1080 (slides), activado por um botão "Apresentação 16:9" no canto inferior direito da proposta web.

**Ficheiros envolvidos:**
- `src/pdf/PDFPresentation.jsx` — documento completo com todos os slides
- `src/pdf/sections/PDFSlide.jsx` — wrapper de slide (Page 1440×810pt, header com logo, rodapé com numeração)

**Dimensões:** 1920×1080 @ 72dpi → `[1440, 810]` em pontos (unidade do react-pdf).

**Estrutura de slides:**

| Slide | Componente | Dados |
|-------|-----------|-------|
| Capa | `SlideCover` | `data.title`, `client`, `date`, `company` |
| Diagnóstico | `SlideProblem` | `context.description`, `context.diagnosis` |
| Descomplicar 360° | `Slide360` | `educationSections` com `label:"Descomplicar 360°"` |
| Educação | `SlideEducation` | restantes `educationSections` |
| Soluções | `SlideService360` | `service360.areas` |
| Previsão de leads | `SlideLeadForecast` | `leadForecast` |
| Preços | `SlidePricing` | `plans` |
| Roadmap | `SlideRoadmap` | `roadmap` |
| Contacto | `SlideContact` | `company`, `client.contact` |

**Helper components internos (PDFPresentation.jsx):**
- `GoldLine` — linha dourada decorativa
- `Tag` — label uppercase dourado (fontSize 13)
- `SlideTitle` — título principal do slide (fontSize 52 default)
- `SlideBody` — corpo do slide (fontSize 19)
- `Card` — caixa de conteúdo com border esquerda dourada (title 16, body 14)
- `CardGrid` — wrapper flexWrap para cards

**Tamanhos de referência para legibilidade em sala:**
- Tag: 13pt | SlideTitle: 42-58pt | SlideBody: 19pt
- Card title: 16pt | Card body: 14pt
- Cover logo: height 120 (3× maior que header normal)

**Separação Slide360 vs SlideEducation:**
```jsx
const slide360 = educationSections?.find(s => s.label?.toLowerCase().includes("360"))
const otherSections = educationSections?.filter(s => !s.label?.toLowerCase().includes("360"))
```

**Botão no App.jsx:**
```jsx
<PDFDownloadLink
  document={<PDFPresentation data={currentProposal} />}
  fileName={`apresentacao-descomplicar-${...}.pdf`}
>
  {({ loading }) => <button>Apresentação 16:9</button>}
</PDFDownloadLink>
```

**PROIBIÇÕES específicas do 16:9:**
- NÃO usar `gap` em `flexWrap` (mesmo bug do A4)
- NÃO usar `rgba()` em `borderColor`
- Manter `hyphenationCallback` no `<Document>` do PDFPresentation também

---

## 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 |
| `pbk-autoridade-local-2026` | Equipa Sérgio Oliveira e Paula Barros · KW Portugal | SEO + conteúdo hiperlocal (mensal) | 2026-04-27 |

---

## 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

### v2.2.0 (2026-04-27)
- Formato 16:9 documentado: `PDFPresentation.jsx` + `PDFSlide.jsx`
- Regra 8 adicionada: estrutura de slides, dimensões, helper components, tamanhos de referência
- Separação Slide360 vs SlideEducation por label
- Botão "Apresentação 16:9" no App.jsx documentado
- Proibições específicas 16:9 adicionadas

### v2.1.0 (2026-04-27)
- Novos bugs react-pdf documentados: `width:"100%"` em flex, paths absolutos em imagens, `borderBottom` duplicado
- Hifenização: `hyphenationCallback` no `<Document>`
- Padrão de ícones: `PDFIcons.jsx` para PDF, lucide-react para web, fallback por índice
- Proposta `pbk-autoridade-local-2026` adicionada ao registo
- Healing Log com 5 erros reais corrigidos

### v1.0.0 (2026-03-23)
- Versão inicial

---

**Versão:** 2.2.0 | **Data:** 2026-04-27

---

## Healing Log

Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.

```jsonl
{"date":"2026-04-27","issue":"Barras de gráfico invisíveis no PDF","fix":"width:'100%' não funciona em flex containers — substituir por alignSelf:'stretch' no container e na barra","source":"user"}
{"date":"2026-04-27","issue":"Linha dourada duplicada no topo das páginas","fix":"PDFPage tinha borderBottomWidth no header E os componentes filhos tinham borderTopWidth — remover borderBottom do header, manter apenas borderTopWidth:2","source":"user"}
{"date":"2026-04-27","issue":"Foto do gestor de conta não aparece no PDF","fix":"react-pdf não carrega paths absolutos do filesystem — copiar para public/assets/ e referenciar sem / inicial","source":"user"}
{"date":"2026-04-27","issue":"Border verde em vez de dourado no PDF","fix":"rgba() em borderColor renderiza verde — usar hex sólido (#D4A020)","source":"user"}
{"date":"2026-04-27","issue":"Hifenização indesejada no PDF","fix":"Adicionar hyphenationCallback={(word) => [word]} ao <Document>","source":"user"}
```

*Adicionar nova linha após cada erro corrigido.*
