Files
claude-plugins/crm-ops/skills/proposta-visual/SKILL.md
T

476 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.*