// ProductTour.jsx — interactive product showcase with auto-rotating screen slideshow. // Renders 5 stylised product screens inside a browser-frame viewport with // thumbnails on the side, an auto-advance progress bar, and crossfade transitions. // // We only showcase screens that are *fully built* in the real app and that earn // their slot in a marketing tour. Obras (basic list, no detail page yet) and the // AI assistant (already represented in the dedicated "IA" section of the landing // via ChatPreviewCard) are intentionally omitted. // // The five screens mirror the real ConstructHub admin + portal UI: // 1. Painel → frontend/src/features/dashboard/layouts/construction-v1.tsx // 2. Compras → frontend/src/routes/_app/compras/index.tsx // 3. Catálogo → frontend/src/routes/_app/catalog/index.tsx // 4. Faturação → frontend/src/routes/_app/faturacao/index.tsx // 5. Portal B2B → frontend/src/routes/portal/catalog.tsx + portal-shell.tsx // // Visual rhythm matches the real app: shadcn neutrals + indigo accent (#4F6EF7), // rounded-xl cards, sticky white/85 backdrop-blur header, h-14 top row, real // nav labels (Painel · Catálogo · Clientes · Armazém · Compras · Vendas · Faturação · Relatórios). const TOUR_INDIGO = '#4F6EF7'; const TOUR_INDIGO_SOFT = '#EEF1FF'; // ─── Browser frame wrapper ────────────────────────────────────────── function BrowserFrame({ url, children, dark }) { return (
{url}
{children}
); } // ─── Tiny inline sparkline ───────────────────────────────────────── function Sparkline({ data, color = TOUR_INDIGO, width = 80, height = 24 }) { const max = Math.max(...data); const min = Math.min(...data); const range = max - min || 1; const step = width / (data.length - 1); const points = data.map((v, i) => `${i * step},${height - ((v - min) / range) * height}`).join(' '); return ( ); } // ─── ConstructHub logomark (real SVG from frontend/src/components/brand/logomark.tsx) ─ function Logomark({ size = 16 }) { return ( ); } // ─── Shared app header (mirrors components/layout/app-header.tsx) ─── // h-14 sticky top bar, max-w-1400 inner, brand + horizontal nav + right cluster. // Nav items map 1:1 to NAV_ITEMS in lib/nav-config.ts. const APP_NAV = [ { l: 'Painel', i: 'layout-dashboard' }, { l: 'Catálogo', i: 'package' }, { l: 'Clientes', i: 'users', chev: true }, { l: 'Armazém', i: 'warehouse' }, { l: 'Compras', i: 'truck' }, { l: 'Vendas', i: 'shopping-cart' }, { l: 'Faturação', i: 'file-text' }, { l: 'Relatórios', i: 'bar-chart-3' }, ]; function AppHeader({ active }) { return (
{/* Brand */}
ConstructHub
{/* Horizontal nav */} {/* Right cluster: search · lang · bell · help · avatar */}
Procurar ⌘K
3 owner JP
); } // ─── Status pill (mirrors design-system tone tokens) ─── function StatusPill({ tone, label }) { const bg = { neutral:'#F4F4F5', info:'#F0F9FF', success:'#ECFDF5', warning:'#FFFBEB', destructive:'#FEF2F2' }[tone]; const fg = { neutral:'#3F3F46', info:'#0369A1', success:'#047857', warning:'#B45309', destructive:'#B91C1C' }[tone]; const dot = { neutral:'#A1A1AA', info:'#0EA5E9', success:'#10B981', warning:'#F59E0B', destructive:'#EF4444' }[tone]; return ( {label} ); } // ─── KPI tile (mirrors components/shared/kpi-card.tsx) ─── function KpiTile({ label, value, icon, iconBg = '#EEF1FF', iconColor = TOUR_INDIGO, sparkData, sparkColor, description, highlight }) { return (

{label}

{value}

{sparkData && (
)} {description &&

{description}

}
); } // ══════════════════════════════════════════════════════════════════ // SCREEN 1 — Painel (Dashboard) // Mirrors features/dashboard/layouts/construction-v1.tsx: // • 5 KPI tiles (Receita mensal / A receber / Stock / Orçamentos / Alertas) // • Tendência de receita — 6-month bar chart // • 3 quick-link cards (Vendas / Stock / Recebimentos) // ══════════════════════════════════════════════════════════════════ function DashboardScreen() { // Sample 6-month series in cents → euros (compact format like the real app). const revenueMonths = [ { m: 'Dez', v: 38 }, { m: 'Jan', v: 42 }, { m: 'Fev', v: 51 }, { m: 'Mar', v: 48 }, { m: 'Abr', v: 67 }, { m: 'Mai', v: 82 }, ]; const maxRev = Math.max(...revenueMonths.map(r => r.v)); return (
{/* Page header */}

Bem-vindo, João

Painel de controlo — segunda-feira, 18 de maio de 2026

{/* KPI strip — 5 cards, real labels */}
r.v)} description="mês corrente" />
{/* Tendência de receita */}
Tendência de receita
Ver relatório completo →
{revenueMonths.map((r, i) => { const h = (r.v / maxRev) * 100; return (
{r.m}
); })}
{/* Quick links */}
{[ { i: 'bar-chart-3', t: 'Relatório de vendas', s: 'Análise por período e produto' }, { i: 'package', t: 'Avaliação de stock', s: 'Valor de stock por armazém' }, { i: 'wallet', t: 'Conta corrente', s: 'Recebimentos e pendentes' }, ].map((q, i) => (
{q.t}
{q.s}
))}
); } // ══════════════════════════════════════════════════════════════════ // SCREEN 2 — Compras (Purchase Orders) // Mirrors routes/_app/compras/index.tsx: // • Page header "Compras" + subtitle + Filtros / Nova encomenda // • 4 KPIs (Encomendas / Valor total / Atrasadas / Lead time) // • Search input INSIDE table card header // • Columns: Nº · Fornecedor · Emissão · Prev. entrega · Estado · Linhas · Total // • Status pills with dot (Rascunho/Aberta/Confirmada/Parcial/Recebida/Cancelada) // ══════════════════════════════════════════════════════════════════ function ComprasScreen() { const rows = [ ['EC-0241', 'Cimentos Mafra', '12 mai', '20 mai', 'Aberta', 'info', 4, '4 280 €'], ['EC-0240', 'Tintas do Norte', '11 mai', '22 mai', 'Confirmada', 'info', 2, '1 940 €'], ['EC-0239', 'Sika Portugal', '10 mai', '21 mai', 'Parcial', 'warning', 6, '3 510 €'], ['EC-0238', 'Tabique, Lda.', '08 mai', '14 mai', 'Recebida', 'success', 3, '986 €'], ['EC-0237', 'Cimentos Mafra', '07 mai', '12 mai', 'Recebida', 'success', 5, '5 240 €'], ['EC-0236', 'Pladur Iberia', '05 mai', '11 mai', 'Recebida', 'success', 2, '1 120 €'], ['EC-0235', 'Tabique, Lda.', '03 mai', '09 mai', 'Cancelada', 'neutral', 1, '180 €'], ]; return (
{/* Page header */}

Compras

Gestão de encomendas, recepções e folgas com fornecedores.

{/* KPI strip */}
{/* Table card */}
{/* Search bar inside card */}
Procurar por nº ou fornecedor… 7 / 247
{rows.map((r, i) => ( ))}
Fornecedor Emissão Prev. entrega Estado Linhas Total
{r[0]} {r[1]} {r[2]} {r[3]} {r[6]} {r[7]}
); } // ══════════════════════════════════════════════════════════════════ // SCREEN 3 — Catálogo (Products) // Mirrors routes/_app/catalog/index.tsx: // • Page header + Adicionar produto // • 4 KPIs (Total / Activos / Categorias / Lista por defeito) // • Search + Mostrar inactivos switch + Category facet chips // • Table: SKU · Nome · Categoria · Unidade · Preço base · IVA · Estado // ══════════════════════════════════════════════════════════════════ function CatalogScreen() { const products = [ ['CIM-3225R-25', 'Cimento Portland II 32,5 R · 25kg', 'Cimentos', 'saco', '4,85 €', '23%', 'success', 'Activo'], ['ARE-FIN-T', 'Areia fina · tonelada', 'Areias e britas', 'ton', '38,00 €', '23%', 'success', 'Activo'], ['TIN-ESM-10L', 'Esmalte sintético branco · 10L', 'Tintas', 'lata', '52,40 €', '23%', 'success', 'Activo'], ['SIK-290DC', 'Sikaflex 290 DC · Marítimo', 'Selantes', 'un', '14,90 €', '23%', 'success', 'Activo'], ['PLA-RF13-60', 'Pladur RF 13mm · 1200×2500', 'Pladur e gesso', 'placa','19,30 €', '23%', 'success', 'Activo'], ['PER-70-3M', 'Perfis metálicos 70mm · 3m', 'Pladur e gesso', 'un', '6,80 €', '23%', 'success', 'Activo'], ['ADI-PLA-20', 'Aditivo plastificante · 20L', 'Selantes', 'bidão','42,00 €', '23%', 'success', 'Activo'], ['PAR-35-500', 'Parafusos 35mm · 500un', 'Ferragens', 'cx', '12,50 €', '6%', 'inactive','Inactivo'], ]; return (

Catálogo

847 produtos · 12 categorias · 3 listas de preço

{/* KPIs */}
{/* Toolbar */}
Procurar produto…
Mostrar inactivos
{['Tudo (847)', 'Cimentos (42)', 'Areias (18)', 'Tintas (124)', 'Pladur (64)'].map((c, i) => ( {c} ))}
{/* Table */}
{products.map((p, i) => ( ))}
SKU Nome Categoria Un. Preço base IVA Estado
{p[0]} {p[1]} {p[2]} {p[3]} {p[4]} {p[5]}
); } // ══════════════════════════════════════════════════════════════════ // SCREEN 4 — Faturação (Invoicing — AT compliance differentiator) // Mirrors routes/_app/faturacao/index.tsx: // • Page header "Faturação" + subtitle + Filtros · Exportar SAF-T · Nova fatura ▼ // • 4 KPIs (Emitidas / Recebidas / Em atraso / Prazo médio recebimento) // • Search input INSIDE table card header // • Columns: Nº · Cliente · Emissão · Vencimento · Estado · Total // • Status pills with dot (Rascunho/Emitida/Paga/Vencida/Anulada) // ══════════════════════════════════════════════════════════════════ function FaturacaoScreen() { const invoices = [ ['FT 2026/0512', 'Construções Faria, Lda.', '14 mai', '14 jun', 'Emitida', 'info', '8 240 €'], ['FT 2026/0511', 'Empreitadas do Centro', '12 mai', '12 jun', 'Paga', 'success', '4 980 €'], ['FT 2026/0510', 'JNS Construções', '10 mai', '09 mai', 'Vencida', 'destructive', '2 415 €'], ['FT 2026/0509', 'Materiais Faria, Lda.', '08 mai', '07 jun', 'Emitida', 'info', '5 720 €'], ['FT 2026/0508', 'Norte Construções', '06 mai', '06 jun', 'Paga', 'success', '12 340 €'], ['FS 2026/0238', 'Consumidor final', '05 mai', '—', 'Paga', 'success', '186 €'], ['FT 2026/0507', 'Reabilitar Lisboa, S.A.', '02 mai', '01 jun', 'Paga', 'success', '9 120 €'], ['FT 2026/0506', 'JNS Construções', '02 mai', '01 mai', 'Vencida', 'destructive', '3 280 €'], ]; return (
{/* Page header */}

Faturação

Documentos de venda emitidos, comunicados à AT (SAF-T).

{/* KPI strip */}
{/* Table card */}
Procurar por nº ou cliente… 8 / 32
{invoices.map((r, i) => ( ))}
Cliente Emissão Vencimento Estado Total
{r[0]} {r[1]} {r[2]} {r[3]} {r[6]}
); } // ══════════════════════════════════════════════════════════════════ // SCREEN 6 — Portal B2B (customer-facing) // Mirrors routes/portal/catalog.tsx + components/layout/portal-shell.tsx: // • max-w-1100 header: TenantLogo + tenant name + "Portal B2B" + nav + cart + avatar // • Page header: Catálogo + subtitle "Preços especiais para clientes B2B…" // • Search input // • ProductGrid: aspect-square image area · SKU font-mono · name · price · cart button // ══════════════════════════════════════════════════════════════════ function PortalScreen() { const products = [ { sku: 'CIM-3225R-25', n: 'Cimento Portland II 32,5 R · 25kg', p: '4,85 €', s: 'Em stock (340)', ico: 'package' }, { sku: 'PLA-RF13-60', n: 'Pladur RF 13mm · 1200×2500', p: '19,30 €', s: 'Em stock (62)', ico: 'layers' }, { sku: 'SIK-290DC', n: 'Sikaflex 290 DC · Marítimo', p: '14,90 €', s: 'Crítico (8)', ico: 'flask-conical' }, { sku: 'TIN-ESM-10L', n: 'Esmalte sintético branco · 10L', p: '52,40 €', s: 'Em stock (24)', ico: 'paint-bucket' }, { sku: 'PER-70-3M', n: 'Perfis metálicos 70mm · 3m', p: '6,80 €', s: 'Em stock (180)', ico: 'ruler' }, { sku: 'ARE-FIN-T', n: 'Areia fina · tonelada', p: '38,00 €', s: 'Em stock (12)', ico: 'mountain' }, { sku: 'ADI-PLA-20', n: 'Aditivo plastificante · 20L', p: '42,00 €', s: 'Em stock (40)', ico: 'flask-conical' }, { sku: 'PAR-35-500', n: 'Parafusos 35mm · 500un', p: '12,50 €', s: 'Em stock (96)', ico: 'wrench' }, ]; return (
{/* Page header */}

Catálogo

Preços especiais para clientes B2B. Entrega em 24–48h úteis.

Ver carrinho
{/* Search */}
Procurar produto…
{/* Product grid — 4 columns, aspect-square image area. Photos are deterministic-by-SKU via picsum.photos/seed/; the icon sits underneath so it shows during load and degrades cleanly if the CDN is unreachable. Real product photos uploaded by the tenant replace these in the actual app. */}
{products.map((p) => (
{p.n}
{p.sku}
{p.n}
{p.s}
{p.p}
))}
); } // ══════════════════════════════════════════════════════════════════ // Slideshow controller // ══════════════════════════════════════════════════════════════════ // One consolidated set of real screens. The faux browser-bar `url` shows the // brand + section only — never the internal infra domain. const TOUR_SCREENS = [ { id: 'dash', label: 'Painel', sub: 'Tarefas, obras em curso, recebimentos e alertas — num só ecrã.', icon: 'layout-dashboard', url: 'ConstructHub · Painel', shot: 'assets/screenshots/tour-painel.png' }, { id: 'compras', label: 'Compras', sub: 'Encomendas a fornecedores com KPIs, filtros e estados auditáveis.', icon: 'truck', url: 'ConstructHub · Compras', shot: 'assets/screenshots/tour-compras.png' }, { id: 'catalog', label: 'Catálogo', sub: 'Produtos, categorias e listas de preço — operação completa.', icon: 'package', url: 'ConstructHub · Catálogo', shot: 'assets/screenshots/tour-catalogo.png' }, { id: 'fatura', label: 'Faturação', sub: 'Comunicação directa à AT, SAF-T num clique, 6 tipos de documento.', icon: 'file-text', url: 'ConstructHub · Faturação', shot: 'assets/screenshots/tour-faturacao.png' }, { id: 'crm', label: 'CRM · Comercial', sub: 'Clientes, oportunidades e pipeline comercial ponderado.', icon: 'users', url: 'ConstructHub · Clientes', shot: 'assets/screenshots/gallery-crm.png' }, { id: 'sched', label: 'Cronograma', sub: 'Gantt com caminho crítico (CPM) — paridade Primavera P6.', icon: 'bar-chart-3', url: 'ConstructHub · Cronograma', shot: 'assets/screenshots/gallery-scheduler.png' }, { id: 'cal', label: 'Calendários', sub: 'Calendários de trabalho por obra — semanas, turnos e feriados.', icon: 'calendar', url: 'ConstructHub · Calendários', shot: 'assets/screenshots/gallery-calendars.png' }, { id: 'portal', label: 'Portal B2B', sub: 'A loja online dos seus clientes empreiteiros e construtoras.', icon: 'store', url: 'Portal B2B · marca do cliente', shot: 'assets/screenshots/tour-portal.png' }, { id: 'ai', label: 'Assistente IA', sub: 'Pergunte em português; analisa obras, faturas e margens, com citações.', icon: 'sparkles', url: 'ConstructHub · Assistente', shot: 'assets/screenshots/gallery-ai.png' }, ]; const AUTO_ADVANCE_MS = 7000; function ProductTour() { const [active, setActive] = React.useState(0); const [paused, setPaused] = React.useState(false); const [progress, setProgress] = React.useState(0); const [zoom, setZoom] = React.useState(false); // Auto-advance + progress React.useEffect(() => { if (paused) return; setProgress(0); const tick = setInterval(() => setProgress(p => Math.min(100, p + 100 / (AUTO_ADVANCE_MS / 50))), 50); const advance = setTimeout(() => setActive(a => (a + 1) % TOUR_SCREENS.length), AUTO_ADVANCE_MS); return () => { clearInterval(tick); clearTimeout(advance); }; }, [active, paused]); // Keyboard nav React.useEffect(() => { const handler = (e) => { if (e.key === 'ArrowLeft') setActive(a => (a - 1 + TOUR_SCREENS.length) % TOUR_SCREENS.length); if (e.key === 'ArrowRight') setActive(a => (a + 1) % TOUR_SCREENS.length); if (e.key === 'Escape') setZoom(false); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); const go = (i) => { setActive(i); setProgress(0); }; return (
Veja por dentro

Os ecrãs reais.
Um ERP completo.

Estes são os ecrãs do ConstructHub — desenhados para a forma como o sector trabalha, com a densidade certa, atalhos de teclado (⌘K para procurar), e o assistente IA a um clique de distância. Carregue numa miniatura para fixar; clique no ecrã para ampliar.
setPaused(true)} onMouseLeave={() => setPaused(false)}> {/* Viewport — full width */}
{TOUR_SCREENS.map((s, i) => { const isActive = i === active; const offset = i - active; return (
0 ? 20 : -20}px) scale(0.98)`, pointerEvents: isActive ? 'auto' : 'none', zIndex: isActive ? 2 : 1, }}> {s.label} setZoom(true)} className="block w-full h-full object-cover object-top cursor-zoom-in" />
); })}
{/* Bottom controls */}
{TOUR_SCREENS.map((_, i) => ( ))}
{String(active + 1).padStart(2, '0')} / {String(TOUR_SCREENS.length).padStart(2, '0')}
{/* Thumbnail cards — horizontal, under the viewport */}
{TOUR_SCREENS.map((s, i) => { const isActive = i === active; return ( ); })}
Passe o rato para pausar · Navegar · Carregue numa miniatura para fixar
{zoom && (
setZoom(false)} className="fixed inset-0 z-[300] grid place-items-center p-4 sm:p-8" style={{ background: 'rgba(9,9,11,0.92)' }}> {TOUR_SCREENS[active].label}
{TOUR_SCREENS[active].label}
)}
); } Object.assign(window, { ProductTour });