Dify foi removido 06-03-2026. Skills brainstorm/discover ainda referenciam-no no corpo. Bump v1.2 + nota top-of-file. Reescrita workflow para próxima sessão. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
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 |
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:
- Que tipo de proposta é? (360°, website, migração técnica, SEO, eCommerce, outro)
- O preço é mensal ou único? → define
monthlyvsprice/priceLabel/priceSubtitle - Que secções se aplicam? → define que campos preencher no JSON
- Há dados quantitativos? (vendas, scores SEO, KPIs) → define
salesData,context.scores - Há trabalho já realizado? → define
workDone - 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)
titledefinido (não depender do fallback)plans[].priceOUplans[].monthlydefinido (não ambos null)plans[].priceLabeldefinido se preço único- Slug registado em App.jsx
- Nenhum
flexWrap+gapnos componentes PDF - Nenhum
borderColorcomrgba()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:
- Abrir
https://propostas.descomplicar.pt/[slug]com Ctrl+Shift+R - Clicar "Exportar PDF"
- Verificar que
hrefdo link não está vazio (PDF gerou) - 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):
- Verificar consola do browser (F12)
- Causa mais provável: import de componente SVG dos infographics
- Verificar que PDFDocument não importa de
./infographics/ - Verificar que não há
gap+flexWrapno PDF - Ctrl+Shift+R e tentar novamente (cache)
Fase 8: Entrega e registo
- Guardar em
Hub/03-Propostas/[Cliente]/ - Registar estimate no CRM (se não existe)
- Comunicar URL ao utilizador
- 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
titleno 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.
{"date":"","issue":"","fix":"","source":"user|auto"}
Adicionar nova linha após cada erro corrigido.