// Templates empty-state for a brand-new Record. // // Surfaces a curated grid of starter templates ("GET STARTED WITH A TEMPLATE"). // Clicking a card opens a full-screen preview modal showing an *anonymized* // version of the Cedar Narrows WST Project Overview dashboard — i.e. the template the // real output was built from, with representative/sample data and a banner. // // The modal header carries a black "+ Add" button. Clicking it reveals a dashed // upload zone (drop worksheets / docs / presentations) plus a secondary // "Create empty" action: // • Upload → simulate a file upload, close the modal, and trigger the same // agent dashboard-generation loader used elsewhere, ending on the // real Cedar Narrows dashboard. // • Create empty → insert the template (representative data) into the record // as an editable artifact, no AI fill. // // Self-contained: only depends on window globals (React, icons, ProxaP, // EditableBlock, useOutput). const { useState: useStateTpl, useEffect: useEffectTpl, useRef: useRefTpl } = React; /* ===== Template catalogue (renamed for the tunnel / construction domain) ===== */ const TEMPLATE_CARDS = [ { key: 'overview', title: 'Project overview', thumb: 'kpi' }, { key: 'safety', title: 'Safety performance scorecard', thumb: 'area' }, { key: 'monthly', title: 'Monthly progress report', thumb: 'bars' }, { key: 'daily', title: 'Daily field report', thumb: 'grid' }, { key: 'cost', title: 'Cost & commercial summary', thumb: 'waterfall' }, { key: 'weekly', title: 'Weekly status update', thumb: 'kpi' }, ]; /* ===== Card thumbnail — a small abstract dashboard sketch ===== Pure CSS/SVG; `variant` lightly changes the chart so the grid has rhythm. */ function TemplateThumb({ variant = 'kpi' }) { const sparkPts = '2,30 16,24 30,26 44,18 58,21 72,12 86,15 100,7 114,9 126,4'; const areaPts = `${sparkPts} 126,38 2,38`; return ( ); } /* ===== Anonymized dashboard — the body of the preview modal ===== Mirrors the structure of SectionOverview but with representative/sample data and bracketed placeholders for anything project-specific. Editable when rendered as an inserted record artifact (Create empty). */ function TplStatTile({ label, value, sub, tone }) { return (
{label}
{value}
{sub &&
{sub}
}
); } function TplProgressBar({ value, total, label }) { const pct = total ? Math.round((value / total) * 100) : 0; return (
{label} {value} / {total} · {pct}%
); } function TemplateDashboard({ editable = false }) { // EB — conditionally wrap a block in EditableBlock (same idiom as the real // dashboard sections). When not editable, just render the defaults. const EB = ({ fields, render, className = '' }) => { const init = Object.fromEntries(fields.map((f) => [f.id, f.default])); if (!editable || typeof EditableBlock === 'undefined') return render(init); return ( {(v) => render(v)} ); }; const KPI_TILES = [ { label: 'Lost-time incidents (period)', value: '0', sub: 'Goal · 0 · 0 YTD', tone: 'ok' }, { label: 'Hours worked (period)', value: '18,500', sub: '760,000 hrs project to date' }, { label: 'NLTI rate (period)', value: '0.00', sub: 'YTD · 12.0 · Goal 3.9', tone: 'warn' }, { label: 'Environmental incidents', value: '1', sub: 'Period · 2 YTD · 40 PTD' }, ]; const WORKFRONTS = [ { site: '[Site A]', t: '[Primary workfront]', s: 'Representative summary of the activity, crew and this period’s progress. Connect a worksheet to replace.', tag: 'On track' }, { site: '[Site B]', t: '[Secondary workfront]', s: 'Representative summary of the activity, crew and this period’s progress. Connect a worksheet to replace.', tag: 'In progress' }, { site: '[Off-site]', t: '[Fabrication / delivery]', s: 'Representative summary of the activity, crew and this period’s progress. Connect a worksheet to replace.', tag: 'In progress' }, ]; return (
{/* Header */} (
{v.kicker}

{v.title}

{v.lede}

{v.chip1 && {v.chip1}} {v.chip2 && {v.chip2}} {v.chip3 && {v.chip3}}
)} /> {/* KPI strip */}
{KPI_TILES.map((k, i) => ( } /> ))}
{/* Status callout */} (

{v.title}

{v.body}

)} /> {/* Two-column: progress + manpower */}
{ const T = parseInt(v.total, 10) || 200; const n = (s) => parseInt(s, 10) || 0; return (
{v.title}
{v.sub}
); }} /> { const rows = [1, 2, 3, 4, 5, 6].map((i) => ({ l: v[`r${i}l`], v: parseInt(v[`r${i}v`], 10) || 0 })); return (
{v.title}
{v.sub}
{rows.map((r, i) => (
{r.l} {r.v}
))}
); }} />
{/* Active workfronts */}
Active workfronts — this period
From your latest weekly report
{WORKFRONTS.map((w, i) => ( (
{v.site} {v.tag}
{v.t}
{v.s}
)} /> ))}
{/* Commercial + recent reports */}
Open commercial items
Representative · connect your register
    {[ { id: 'PCO-00X', t: '[Change description] — under review', s: 'Disputed' }, { id: 'PCO-00Y', t: '[Change description] — negotiation', s: 'Disputed' }, { id: 'CO-0XX', t: '[Approved change order]', s: 'Executed' }, { id: 'CO-0YY', t: '[Approved change order]', s: 'Executed' }, ].map((it, i) => (
  • {it.id} {it.t} {it.s}
  • ))}
Latest reports
Most recent across the collection
  • Monthly
    Monthly Report No. [n]
    [Month] [Year] · Issued [date]
  • Weekly
    Weekly Report No. [n]
    Week ending [date]
  • Daily
    Shift Report [n] — [site]
    [date] · Day & Night shift
  • Daily
    Shift Report [n] — [site]
    [date] · Day shift
); } /* ===== Preview modal ===== */ const TPL_UPLOAD_FILES = [ { name: 'AWST_Weekly Report_220.pdf', kind: 'doc' }, { name: 'Cost-Tracker-March-2026.xlsx', kind: 'sheet' }, { name: 'Manpower_Week_220.xlsx', kind: 'sheet' }, { name: 'GP-Meeting-14-Deck.pptx', kind: 'slide' }, ]; function TplFileIcon({ kind }) { if (kind === 'sheet') { return ; } if (kind === 'slide') { return ; } return ; } function TemplatePreviewModal({ template, onClose, onGenerate, onCreateEmpty }) { // phase: 'preview' | 'add' (dropzone shown) | 'uploading' const [phase, setPhase] = useStateTpl('preview'); const scrollRef = useRefTpl(null); useEffectTpl(() => { const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; const onKey = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); return () => { document.body.style.overflow = prev; document.removeEventListener('keydown', onKey); }; }, []); // Show the dashed upload zone. const startAdd = () => { setPhase('add'); // Scroll the body up so the upload zone is in view. if (scrollRef.current) scrollRef.current.scrollTo({ top: 0, behavior: 'smooth' }); }; // Simulate a file upload, then hand off to the agent generation flow. const handleUpload = () => { if (phase === 'uploading') return; setPhase('uploading'); window.setTimeout(() => { onGenerate(); // parent closes the modal + starts the loader }, 1150); }; return (
{/* Sticky header */}
Template
{template.title}
{/* Representative-data banner */} {/* Body */}
{phase !== 'preview' && (
{phase === 'add' && ( )} {phase === 'uploading' && (
Uploading {TPL_UPLOAD_FILES.length} files…
    {TPL_UPLOAD_FILES.map((f, i) => (
  • {f.name}
  • ))}
)} {phase === 'add' && (
or Insert the template as-is and edit it yourself.
)}
)}
); } /* ===== Templates empty state ===== */ function EmptyVariantTemplates({ onOpenAgentChat }) { const ctx = (typeof useOutput === 'function') ? useOutput() : null; const [active, setActive] = useStateTpl(null); // selected template card const handleGenerate = () => { setActive(null); if (ctx && ctx.onTemplateGenerate) ctx.onTemplateGenerate(); }; const handleCreateEmpty = (tpl) => { setActive(null); if (ctx && ctx.onTemplateCreateEmpty) ctx.onTemplateCreateEmpty(tpl); }; const handleAgent = (e) => { if (e) e.preventDefault(); if (onOpenAgentChat) onOpenAgentChat(); }; return (
Get started with a template
{TEMPLATE_CARDS.map((tpl) => ( ))}
or
{active && ( setActive(null)} onGenerate={handleGenerate} onCreateEmpty={handleCreateEmpty} /> )}
); } Object.assign(window, { EmptyVariantTemplates, TemplateDashboard, TEMPLATE_CARDS, TemplateThumb });