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

18 KiB
Raw Blame History

name, description
name description
proposta-visual 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):

{
  "monthly": 459,
  "hours": 8,
  "originalPrice": 540,
  "discount": 15
}

Mostra: "459 €/mês" + "8h/mês incluídas · contrato anual"

Preço único (investimento):

{
  "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>:

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:

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:

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

{
  "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:

{
  "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

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

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.

{"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.