// Collection (overview dashboard) page — Cedar Narrows Water Supply Tunnel.
// Built from awst_gp_meeting_13_overview.json. Pure Proxa DS.
const { useState: useStateCol } = React;
// Special prompt: when a user submits this from the empty-record state we
// simulate the agent generating the Cedar Narrows WST Project Overview dashboard.
window.DASHBOARD_GEN_PROMPT = 'Create a project dashboard from hub knowledge base';
window.DASHBOARD_GEN_STAGES = [
'Scanning sources in this hub\u2026',
'Reading Monthly Report No. 50 \u00b7 March 2026\u2026',
'Extracting data points: safety, progress, finance\u2026',
'Validating numbers against weekly + daily reports\u2026',
'Structuring sections and KPI tiles\u2026',
'Composing the interactive dashboard\u2026',
];
/* ===== Top bar ===== */
function CollTopBar({ onBack, onOpenRecord, onOpenGraph }) {
const [menuOpen, setMenuOpen] = useStateCol(false);
const wrapRef = React.useRef(null);
React.useEffect(() => {
if (!menuOpen) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setMenuOpen(false);
};
const onKey = (e) => {if (e.key === 'Escape') setMenuOpen(false);};
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [menuOpen]);
const handleGraph = () => {setMenuOpen(false);if (onOpenGraph) onOpenGraph();};
return (
Traylor Bros., Inc. · Content Library
Cedar Narrows Water Supply Tunnel
James Anderson · Marketing Team
·
4 min ago
{menuOpen &&
}
);
}
/* ===== Sidebar — sections nav ===== */
const SECTIONS = [
{ num: 1, key: 'overview', label: 'Overview' },
{ num: 2, key: 'monthly', label: 'Monthly reports', sub: '3 items' },
{ num: 3, key: 'weekly', label: 'Weekly reports', sub: '3 items' },
{ num: 4, key: 'daily', label: 'Daily reports', sub: '438 items' },
{ num: 5, key: 'photos', label: 'Photos', sub: '6 items' },
{ key: 'all', label: 'All items', sub: '526 items' }];
function CollSidebar({ active, onPick }) {
return (
);
}
/* ===== Inline charts ===== */
function AreaChart({ values, labels, tone = 'neutral', height = 120 }) {
const w = 280;
const h = height;
const pad = { top: 12, right: 8, bottom: 22, left: 8 };
const max = Math.max(...values, 1);
const min = Math.min(...values, 0);
const range = max - min || 1;
const xs = values.map((_, i) => pad.left + i * (w - pad.left - pad.right) / Math.max(values.length - 1, 1));
const ys = values.map((v) => pad.top + (h - pad.top - pad.bottom) * (1 - (v - min) / range));
const line = xs.map((x, i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${ys[i].toFixed(1)}`).join(' ');
const area = `${line} L ${xs[xs.length - 1].toFixed(1)} ${(h - pad.bottom).toFixed(1)} L ${xs[0].toFixed(1)} ${(h - pad.bottom).toFixed(1)} Z`;
const stroke = tone === 'pos' ? '#52B26E' : tone === 'neg' ? '#D04A3F' : '#A09792';
const fill = tone === 'pos' ? 'rgba(82,178,110,0.18)' : tone === 'neg' ? 'rgba(208,74,63,0.16)' : 'rgba(160,151,146,0.18)';
return (
);
}
function BarChart({ values, labels, highlightedIndices = [], height = 140 }) {
const w = 320;
const h = height;
const pad = { top: 12, right: 4, bottom: 24, left: 4 };
const max = Math.max(...values, 1);
const n = values.length;
const slot = (w - pad.left - pad.right) / n;
const barW = Math.min(slot * 0.6, 26);
const innerH = h - pad.top - pad.bottom;
return (
);
}
function GroupedBarChart({ groups, series, height = 160 }) {
// groups: ['Original estimate', ...]; series: [{key,label,values,tone}]
const w = 320;
const h = height;
const pad = { top: 12, right: 4, bottom: 26, left: 4 };
const max = Math.max(...series.flatMap((s) => s.values), 1);
const groupCount = groups.length;
const groupSlot = (w - pad.left - pad.right) / groupCount;
const seriesCount = series.length;
const barW = Math.min(groupSlot * 0.7 / seriesCount, 16);
const innerH = h - pad.top - pad.bottom;
const tones = { base: '#33302E', muted: '#A09792', lightMuted: '#D5CFCB' };
return (
);
}
/* ===== Headline with a single colored accent phrase ===== */
function HeadlineText({ text, accent, color }) {
if (!accent || !text || !text.includes(accent)) return text || null;
const idx = text.indexOf(accent);
return (
{text.slice(0, idx)}
{accent}
{text.slice(idx + accent.length)}
);
}
/* ===== Rich trend chart (gridlines, right axis, goal band, annotations) ===== */
function RichTrendChart({
values, xLabels, yMax, yTicks, color = '#D04A3F',
fill = 'rgba(208,74,63,0.12)', area = false,
goal = null, goalLabel = '', zoneFill = null,
annotations = [], height = 280 }) {
const W = 460, H = height;
const pad = { top: 22, right: 42, bottom: 30, left: 14 };
const innerW = W - pad.left - pad.right;
const innerH = H - pad.top - pad.bottom;
const n = values.length;
const x = (i) => pad.left + (n <= 1 ? innerW / 2 : i * innerW / (n - 1));
const y = (v) => pad.top + innerH * (1 - Math.max(0, Math.min(v, yMax)) / yMax);
const linePath = values.map((v, i) => `${i === 0 ? 'M' : 'L'} ${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(' ');
const areaPath = `${linePath} L ${x(n - 1).toFixed(1)} ${y(0).toFixed(1)} L ${x(0).toFixed(1)} ${y(0).toFixed(1)} Z`;
return (
);
}
/* ===== Analytical chart card (bold headline intro + chart + legend) ===== */
function AnalysisCard({ headline, accent, accentColor, sub, children, legend }) {
return (
{sub &&
{sub}
}
{children}
{legend &&
{legend.map((l, i) =>
{l.label}
)}
}
);
}
/* ===== KPI Edit Side Panel ===== */
function KpiEditPanel({ open, kpi, onClose, onChange }) {
if (!open || !kpi) return null;
const update = (k, v) => onChange({ ...kpi, [k]: v });
return (
);
}
/* ===== Section: Overview (live dashboard) ===== */
function StatTile({ label, value, sub, tone }) {
return (
{label}
{value}
{sub &&
{sub}
}
);
}
function ProgressBar({ value, total, label }) {
const pct = Math.round(value / total * 100);
return (
{label}
{value} / {total} · {pct}%
);
}
/* ===== Sources for the Overview dashboard (AI-extracted) ===== */
const OV_SRC_SAFETY_KPI = {
kind: 'xlsx',
file: 'Safety-Stats-March-2026.xlsx',
pages: 'Sheet: KPI',
by: 'Reu De Los Santos',
ago: '3 d ago',
agent: 'spreadsheet agent',
extractedAgo: '3 days ago',
excerpts: [
{
label: 'KPI sheet — March 2026',
kind: 'table',
headers: ['Metric', 'March', 'YTD', 'PTD'],
rows: [
['Lost-time incidents', 0, 0, 0],
['Hours worked', '20,407', '60,118', '792,145'],
['NLTI rate', 0.00, 14.3, '—'],
['Environmental incidents', 2, 3, 48]]
}]
};
const OV_SRC_MONTHLY = {
kind: 'pdf',
file: '2026-03 Monthly Report-R0.pdf',
pages: 'p.2,3',
by: 'Sarah Chen',
ago: '3h ago',
agent: 'node agent',
extractedAgo: '3 days ago',
excerpts: [
{ label: 'Page 2 — Safety', text: 'No lost-time incidents recorded in March. Zero LTI YTD. Total hours worked this period: 20,407 (792,145 project-to-date). NLTI rate for the period is 0.00 against a project goal of 3.9 (2026 YTD 14.3).' },
{ label: 'Page 3 — Environmental', text: 'Two environmental incidents recorded in March — both minor hydrocarbon releases on the south site, contained and remediated within the shift. YTD 3, project-to-date 48.' }]
};
const OV_SRC_WEEKLY = {
kind: 'pdf',
file: 'AWST_Weekly Report_220_29Mar2026.pdf',
pages: 'p.4,6',
by: 'Mike Reyes',
ago: '1 d ago',
agent: 'node agent',
extractedAgo: '1 day ago',
excerpts: [
{ label: 'Section 1 — Status', text: 'Tunneling complete. Focus is on steel pipe installation, cellular grout backfill (4.5% complete with 647 m³ placed this week), South Valve Chamber dewatering and excavation prep, and North Valve Chamber East Wall Lift 2 concrete.' },
{ label: 'Section 2 — Ground improvement', text: 'Stone columns SC15, SC16, SC26 installed by Menard remediating SVC SOE non-conformance flagged in Weekly 218. CFA pre-drill by Henry continues.' }]
};
const OV_SRC_MANPOWER = {
kind: 'xlsx',
file: 'AWST_Manpower_Week_220.xlsx',
pages: 'Sheet: Headcount',
by: 'Mike Reyes',
ago: '1 d ago',
agent: 'spreadsheet agent',
extractedAgo: '1 day ago',
excerpts: [
{
label: 'Headcount — week 220',
kind: 'table',
headers: ['Role', 'Count'],
rows: [
['Laborers', 28],
['Operators', 3],
['Electricians', 2],
['Welders / Mechanics', 3],
['Blue Max Drilling (sub)', 4],
['ILP Archaeology (sub)', 4]]
},
{
label: 'Totals',
kind: 'table',
headers: ['Bucket', 'Count'],
rows: [
['Indirect / Overhead', 36],
['Staff', 23],
['Direct craft', 44]]
}]
};
const OV_SRC_DAILY = {
kind: 'xlsx',
file: 'AWST_Pipe_Install_Tracker.xlsx',
pages: 'Sheet: Joints',
by: 'Don Hayes',
ago: '2 d ago',
agent: 'spreadsheet agent',
extractedAgo: '2 days ago',
excerpts: [
{
label: 'Pipe install progress — 191 total',
kind: 'table',
headers: ['Stage', 'Count', '%'],
rows: [
['Pipe delivered', 191, '100%'],
['Transferred to yard', 191, '100%'],
['Set in tunnel', 183, '95.8%'],
['Capped', 164, '85.9%'],
['Weld QC accepted', 144, '75.4%']]
}]
};
const OV_SRC_COMMERCIAL = {
kind: 'xlsx',
file: 'AWST_Commercial_Register.xlsx',
pages: 'Sheet: Open items',
by: 'Mike Reyes',
ago: '1 d ago',
agent: 'spreadsheet agent',
extractedAgo: '1 day ago',
excerpts: [
{
label: 'Open commercial items — Weekly 220',
kind: 'table',
headers: ['ID', 'Value', 'Status'],
rows: [
['PCO-008', '$5.1M', 'Disputed'],
['PCO-010', '—', 'Disputed'],
['PCO-052', '—', 'Disputed'],
['CO-028', '—', 'Executed'],
['CO-029', '—', 'Executed']]
}]
};
function SectionOverview({ editable = false, hideTitle = false }) {
// EB — local helper that conditionally wraps a block in EditableBlock.
const EB = ({ fields, render, sources = {}, className = '' }) => {
const init = Object.fromEntries(fields.map((f) => [f.id, f.default]));
if (!editable) return render(init);
return (
{(v) => render(v)}
);
};
const KPI_TILES = [
{ updateId: 'u-lti', defaults: { label: 'Lost-time incidents (March)', value: '0', sub: 'Goal · 0 · 0 YTD' }, tone: 'ok', source: OV_SRC_SAFETY_KPI },
{ updateId: 'u-hrs', defaults: { label: 'Hours worked (March)', value: '20,407', sub: '792,145 hrs project to date' }, source: OV_SRC_SAFETY_KPI },
{ updateId: 'u-nlti', defaults: { label: 'NLTI rate (March)', value: '0.00', sub: '2026 YTD · 14.3 · Goal 3.9' }, tone: 'warn', source: OV_SRC_SAFETY_KPI },
{ defaults: { label: 'Environmental incidents', value: '2', sub: 'March · 3 YTD · 48 PTD' }, source: OV_SRC_SAFETY_KPI }];
const WORKFRONTS = [
{ site: 'South site', t: 'SVC dewatering & excavation', s: 'Pump test complete · Well #11 added at −18 m · Wells #1 and #4 to be removed · Prep for Lift 1 excavation', tag: 'On track' },
{ site: 'Tunnel', t: 'Cellular grout backfill', s: '647 m³ placed this week · 4.5% complete · Slag mix in place of fly ash', tag: 'In progress' },
{ site: 'North site', t: 'NVC East Wall Lift 2 concrete', s: 'Formwork complete · Reinforcing & bulkheads set · Placement scheduled', tag: 'On track' },
{ site: 'Off-site', t: 'NWP fabrication — NVC pipe', s: 'Tunnel pipe 191/191 delivered · Shaft pipe 9/9 hydrotested · Tee MK 307 fit-up at Jewel', tag: 'In progress' },
{ site: 'South site', t: 'Stone column ground improvement', s: 'SC15, SC16, SC26 installed by Menard · CFA pre-drill by Henry', tag: 'In progress' },
{ site: 'Tunnel', t: 'Pipe welding QC', s: '144 of 191 joints QA-accepted · Stasuk QC / Acuren QA on site', tag: 'In progress' }];
return (
{/* === Section header === */}
} />
{/* === Headline KPI strip === */}
{KPI_TILES.map((k, i) =>
k.updateId ?
:
} />
)}
{/* === Status callout === */}
} />
{/* === Two-column: Tunnel pipe progress + Manpower === */}
{
const T = parseInt(v.total, 10) || 191;
const n = (s) => parseInt(s, 10) || 0;
return (
);
}} />
{
const rows = [1, 2, 3, 4, 5, 6].map((i) => ({
l: v[`r${i}l`],
v: parseInt(v[`r${i}v`], 10) || 0
}));
return (
{rows.map((r, i) =>
{r.l}
{r.v}
)}
);
}} />
{/* === Active workfronts — outer header non-editable, each card is its own block === */}
Active workfronts — this week
From Weekly Report No. 220
{WORKFRONTS.map((w, i) =>
{v.site}
{v.tag}
{v.t}
{v.s}
} />
)}
);
}
/* ===== Section: Safety ===== */
const SF_SRC_STATS = {
kind: 'xlsx',
file: 'Safety-Stats-Q4-2025.xlsx',
pages: 'Sheet: KPI',
by: 'Myles Murphy',
ago: '1 wk ago',
agent: 'spreadsheet agent',
extractedAgo: '4 days ago',
excerpts: [
{
label: 'KPI · 2022–2025 rolling',
kind: 'table',
headers: ['Year', 'LTI', 'TRIF', 'NLTI', 'Hours (k)'],
rows: [
['2022', 0, 0.00, 3.46, 50.3],
['2023', 0, 0.00, 6.35, 173.4],
['2024', 1, 0.79, 2.40, 251.8],
['2025', 0, 0.00, 8.92, 269.2]]
}]
};
const SF_SRC_INCIDENTS = {
kind: 'xlsx',
file: 'Safety-Stats-Q4-2025.xlsx',
pages: 'Sheet: Incident Log',
by: 'Myles Murphy',
ago: '1 wk ago',
agent: 'spreadsheet agent',
extractedAgo: '4 days ago',
excerpts: [
{
label: 'Incident counts — Sept 25 – Dec 25, 2025',
kind: 'table',
headers: ['Category', 'Count'],
rows: [
['Near Hit', 3],
['Report Only', 2],
['First Aid', 2],
['Medical Aid', 0],
['Lost Time', 0],
['Equip. Damage', 3],
['Prop. Damage', 5]]
}]
};
const SF_SRC_TRAINING = {
kind: 'xlsx',
file: 'Safety-Inspections-Q4-2025.xlsx',
pages: 'Sheet: Activity',
by: 'Reu De Los Santos',
ago: '3 d ago',
agent: 'spreadsheet agent',
extractedAgo: '3 days ago',
excerpts: [
{
label: 'Inspection / training counts',
kind: 'table',
headers: ['Activity', 'Count'],
rows: [
['Safety team inspections', 40],
['Crew inspections', 38],
['Orientations completed', 38],
['WSBC inspections — no orders', 2]]
}]
};
const SF_SRC_LEADERSHIP = {
kind: 'pdf',
file: '2025-12 Monthly Report-R0.pdf',
pages: 'p.4',
by: 'James Anderson',
ago: '2 wk ago',
agent: 'node agent',
extractedAgo: '12 days ago',
excerpts: [
{ label: 'Page 4 — Org', text: 'Safety Manager Myles Murphy has tendered his resignation. Reu De Los Santos has been promoted into the role effective January 2026.' }]
};
function SectionSafety({ editable = false }) {
const EB = ({ fields, render, sources = {}, className = '' }) => {
const init = Object.fromEntries(fields.map((f) => [f.id, f.default]));
if (!editable) return render(init);
return (
{(v) => render(v)}
);
};
const METRICS = [
{ key: 'lti', title: 'Lost Time Incidents', value: '0', delta: 'Goal · 0', tone: 'neutral',
labels: ['2022', '2023', '2024', 'Period'], values: [0, 0, 1, 0], source: SF_SRC_STATS },
{ key: 'trif', title: 'TRIF', value: '0.00', delta: 'Goal · 0.80', tone: 'neg',
labels: ['2023', '2024', '2025', 'Period'], values: [0, 0.79, 0, 0], source: SF_SRC_STATS },
{ key: 'nlti', title: 'NLTI', value: '8.92', delta: 'Goal · 3.90', tone: 'pos',
labels: ['2022', '2023', '2024', 'Period'], values: [3.46, 6.35, 2.40, 8.92], source: SF_SRC_STATS },
{ key: 'hrs', title: 'Hours worked (period)', value: '78,897', delta: '745,796 project to date', tone: 'pos',
labels: ['2022', '2023', '2024', '2025'], values: [50.3, 173.4, 251.8, 269.2], source: SF_SRC_STATS }];
const INCIDENT_LABELS = ['Near Hit', 'Report Only', 'First Aid', 'Non-Occ MA', 'Medical Aid', 'Lost Time', 'Equip. Damage', 'Prop. Damage'];
const INCIDENT_VALS = [3, 2, 2, 0, 0, 0, 3, 5];
return (
{v.title}
{v.lede}
} />
{v.n1}Total events (period)
{v.n2}Recordable / HP
{v.n3}Disciplinary actions
} />
{
const tiles = [
{ v: v.t1v, l: 'Safety team inspections' },
{ v: v.t2v, l: 'Crew inspections' },
{ v: v.t3v, l: 'Orientations completed' },
{ v: v.t4v, l: 'WSBC inspections — no orders' }];
return (
);
}} />
} />
);
}
/* ===== Section: Finance ===== */
const FN_SRC_KPI = {
kind: 'xlsx',
file: 'Cost-Tracker-Dec-2025.xlsx',
pages: 'Sheet: Summary',
by: 'Lorri Warf',
ago: '2 d ago',
agent: 'spreadsheet agent',
extractedAgo: '2 days ago',
excerpts: [
{
label: 'KPI strip — JTD',
kind: 'table',
headers: ['Metric', 'Value', 'Note'],
rows: [
['Payment received', '$240.4M', 'Before holdback $295.9M'],
['Earned revenue', '$178.4M', 'WIP $51.8M'],
['Cost to date', '$210.8M', 'Incl. $12M amort. equip.'],
['Net cash position', '$29.6M', 'Job 2104']]
}]
};
const FN_SRC_RBM = {
kind: 'xlsx',
file: 'EAC-Forecast-Dec-2025.xlsx',
pages: 'Sheet: RBM',
by: 'Lorri Warf',
ago: '2 d ago',
agent: 'spreadsheet agent',
extractedAgo: '2 days ago',
excerpts: [
{
label: 'Revenue · Budget · Margin ($M)',
kind: 'table',
headers: ['Bucket', 'Revenue', 'Cost', 'Margin'],
rows: [
['Original estimate', 287.8, 246.4, 41.4],
['Approved COs', 26.1, 22.1, 3.9],
['Current EAC', 313.9, 292.8, 21.1]]
}]
};
const FN_SRC_RISKS = {
kind: 'xlsx',
file: 'EAC-Forecast-Dec-2025.xlsx',
pages: 'Sheet: Risks',
by: 'Lorri Warf',
ago: '2 d ago',
agent: 'spreadsheet agent',
extractedAgo: '2 days ago',
excerpts: [
{
label: 'Current financial risks ($M)',
kind: 'table',
headers: ['Risk', 'Exposure'],
rows: [
['Pipe production', 0.65],
['VC concrete', 1.15],
['VC mechanical', 0.54],
['Valve remediation', 0.20],
['Archaeology T&M', 2.55],
['Tariffs', 0.00]]
}]
};
const FN_SRC_CHANGEMGMT = {
kind: 'xlsx',
file: 'Change-Management-Log.xlsx',
pages: 'Sheet: Changes',
by: 'James Anderson',
ago: '4 d ago',
agent: 'spreadsheet agent',
extractedAgo: '4 days ago',
excerpts: [
{
label: 'Change management — counts & values',
kind: 'table',
headers: ['Category', 'Count', 'Value'],
rows: [
['Approved Change Orders (CO)', 28, '$26.06M'],
['Field Orders (FO)', 29, '—'],
['Pending Change Orders (PCO)', 70, '$27.13M est.'],
['Approved time extensions', '155.5 CD', '—'],
['Disputed time extensions', '81 CD', 'SVC Secant Pile Depth']]
}]
};
function SectionFinance({ editable = false }) {
const EB = ({ fields, render, sources = {}, className = '' }) => {
const init = Object.fromEntries(fields.map((f) => [f.id, f.default]));
if (!editable) return render(init);
return (
{(v) => render(v)}
);
};
const FIN = [
{ key: 'pay',
headline: 'Payments received reached $240.4M to date — $295.9M before holdback',
accent: '$240.4M to date', accentColor: '#3F9A5E', color: '#3F9A5E', fill: 'rgba(63,154,94,0.14)',
labels: ['Q1', 'Q2', 'Q3', 'Q4', 'JTD'], values: [60, 110, 175, 220, 240.4],
yMax: 300, yTicks: [0, 100, 200, 300], anno: '$240M', legend: 'Cumulative payments received ($M)' },
{ key: 'rev',
headline: 'Earned revenue stands at $178.4M with $51.8M still in work-in-progress',
accent: '$51.8M still in work-in-progress', accentColor: '#3A6FB0', color: '#3A6FB0', fill: 'rgba(58,111,176,0.13)',
labels: ['Q1', 'Q2', 'Q3', 'Q4', 'JTD'], values: [55, 95, 135, 165, 178.4],
yMax: 200, yTicks: [0, 50, 100, 150, 200], anno: '$178M', legend: 'Cumulative earned revenue ($M)' }];
const RISKS_VALS = [0.65, 1.15, 0.54, 0.20, 2.55, 0.0];
const RISKS_LBL = ['Pipe prod.', 'VC concrete', 'VC mech.', 'Valve remed.', 'Arch. T&M', 'Tariffs'];
return (
{v.title}
{v.lede}
} />
{
const rows = [
['Approved Change Orders (CO)', v.r1c, v.r1v, v.r1s],
['Field Orders (FO)', v.r2c, v.r2v, v.r2s],
['Pending Change Orders (PCO)', v.r3c, v.r3v, v.r3s],
['Approved time extensions', v.r4c, v.r4v, v.r4s],
['Disputed time extensions', v.r5c, v.r5v, v.r5s]];
return (
Change Management
{rows.map((row, i) =>
{row[0]}
{row[1]}
{row[2]}
{row[3]}
)}
);
}} />
);
}
/* ===== Section: Photos ===== */
const PHOTOS = [
{ id: 'secant', img: (window.__resources && window.__resources.photoSecant) || 'assets/photo-secant-piles.png', t: 'Secant Pile Installation', s: 'South Valve Chamber · 45 of 67 piles complete' },
{ id: 'staging', img: (window.__resources && window.__resources.photoStaging) || 'assets/photo-staging-steel-pipe.png', t: 'Staging Steel Pipe', s: 'South site laydown · 84 tunnel pipes on site' },
{ id: 'welding', img: (window.__resources && window.__resources.photoWelding) || 'assets/photo-tunnel-welding.png', t: 'Jewel Welding Tunnel Pipe', s: 'Tunnel · 60 joints completed underway' },
{ id: 'grout', img: (window.__resources && window.__resources.photoGrout) || 'assets/photo-grout-plant.png', t: 'Cellular Grout Plant Assembly', s: 'South site · Mix design complete, plant assembly underway' },
{ id: 'excav', img: (window.__resources && window.__resources.photoExcav) || 'assets/photo-nvc-excavation.png', t: 'NVC Fine Grading Excavation', s: 'North Valve Chamber · Excavation and bracing complete' },
{ id: 'rebar', img: (window.__resources && window.__resources.photoRebar) || 'assets/photo-nvc-rebar.png', t: 'NVC Base Slab Reinforcing', s: 'North Valve Chamber · Rebar and edge forms installation' }];
function SectionPhotos({ onOpenPhoto, variant = 'overview', onCreateRecord }) {
const [view, setView] = useStateCol('grid'); // 'grid' (data table) | 'cards'
const [query, setQuery] = useStateCol('');
if (variant === 'overview') {
return (
Latest project photos
Sep – Dec 2025
Selected images from this reporting period across both sites and offsite fabrication.
{PHOTOS.map((p) =>
)}
);
}
// Standalone Photos tab — data-grid by default with toggle to cards
const filtered = PHOTOS.filter((p) => p.t.toLowerCase().includes(query.toLowerCase()));
return (
{view === 'grid' ?
:
{filtered.map((p) =>
)}
}
);
}
/* ===== Photos Data Grid ===== */
function PhotosDataGrid({ items, onOpenPhoto }) {
return ({ ...p, kind: 'photo' }))} onOpenPhoto={onOpenPhoto} />;
}
/* ===== Generic Items Data Grid ===== */
const RECORD_ITEMS = [
{ id: 'overview', kind: 'record', t: 'Cedar Narrows Water Supply Tunnel — Project Overview', tag: 'Need review', updated: '2 d', comments: 12 },
{ id: 'safety', kind: 'record', t: 'Safety — Period Sept – Dec 2025', tag: 'Approved', updated: '3 d', comments: 5 },
{ id: 'finance', kind: 'record', t: 'Finance — As of December 31, 2025', tag: 'Need review', updated: '4 d', comments: 9 }];
function ItemsDataGrid({ items, onOpenPhoto, onOpenRecord }) {
const handleClick = (it) => {
if (it.kind === 'photo' && onOpenPhoto) onOpenPhoto(it.id);else
if (it.kind === 'record' && onOpenRecord) onOpenRecord(it.id);
};
return (
| Title |
Owner |
|
|
Status |
|
|
|
{items.map((it) =>
handleClick(it)} className="dg-row">
{it.kind === 'photo' ?
:
it.kind === 'report' ?
:
}
{it.t}
|
|
|
|
{it.tag || 'Need review'} |
{it.updated || '5 d'} |
{it.comments != null ? it.comments : 8} |
|
)}
);
}
/* ===== Reports data fixtures =====
Filenames mirror the actual project conventions:
- Monthly: YYYY-MM Monthly Report-R{rev}.pdf
- Weekly: AWST_Weekly Report_{week#}_{DDMMMYYYY}.pdf
- Daily: AWST - Contractor Shift Report -{site} - TAGP - Rev{rr} - {sheet} - {YYYY} - {Month-DD}.pdf
*/
const MONTHLY_REPORTS = [
{ id: 'm-2026-03', t: '2026-03 Monthly Report-R0.pdf', period: 'March 2026', rev: 'R0', owner: 'J. Anderson', status: 'Need review', updated: '2 d', comments: 4 },
{ id: 'm-2026-02', t: '2026-02 Monthly Report-R1.pdf', period: 'February 2026', rev: 'R1', owner: 'J. Anderson', status: 'Approved', updated: '1 mo', comments: 7 },
{ id: 'm-2026-01', t: '2026-01 Monthly Report-R0.pdf', period: 'January 2026', rev: 'R0', owner: 'M. Reyes', status: 'Approved', updated: '2 mo', comments: 3 }];
const WEEKLY_REPORTS = [
{ id: 'w-220', t: 'AWST_Weekly Report_220_29Mar2026.pdf', period: 'Mar 23 – 29, 2026', week: 'WK 220', owner: 'M. Reyes', status: 'Need review', updated: '1 d', comments: 2 },
{ id: 'w-219', t: 'AWST_Weekly Report_219_22Mar2026.pdf', period: 'Mar 16 – 22, 2026', week: 'WK 219', owner: 'M. Reyes', status: 'Approved', updated: '8 d', comments: 1 },
{ id: 'w-218', t: 'AWST_Weekly Report_218_15Mar2026.pdf', period: 'Mar 9 – 15, 2026', week: 'WK 218', owner: 'D. Hayes', status: 'Approved', updated: '15 d', comments: 0 },
{ id: 'w-217', t: 'AWST_Weekly Report_217_08Mar2026.pdf', period: 'Mar 2 – 8, 2026', week: 'WK 217', owner: 'D. Hayes', status: 'Approved', updated: '22 d', comments: 3 },
{ id: 'w-216', t: 'AWST_Weekly Report_216_01Mar2026.pdf', period: 'Feb 23 – Mar 1, 2026', week: 'WK 216', owner: 'M. Reyes', status: 'Approved', updated: '29 d', comments: 1 },
{ id: 'w-215', t: 'AWST_Weekly Report_215_22Feb2026.pdf', period: 'Feb 16 – 22, 2026', week: 'WK 215', owner: 'L. Nguyen', status: 'Approved', updated: '36 d', comments: 0 },
{ id: 'w-214', t: 'AWST_Weekly Report_214_15Feb2026.pdf', period: 'Feb 9 – 15, 2026', week: 'WK 214', owner: 'L. Nguyen', status: 'Approved', updated: '43 d', comments: 4 }];
const DAILY_REPORTS = [
{ id: 'd-12010-s', t: 'AWST - Contractor Shift Report -South - TAGP - Rev00 - 12010 - 2026 - March-02.pdf', period: 'Mar 2, 2026', site: 'South', rev: 'Rev00', owner: 'D. Hayes', status: 'Approved', updated: '1 d', comments: 1 },
{ id: 'd-12010-n', t: 'AWST - Contractor Shift Report -North - TAGP - Rev00 - 12010 - 2026 - March-02.pdf', period: 'Mar 2, 2026', site: 'North', rev: 'Rev00', owner: 'M. Reyes', status: 'Approved', updated: '1 d', comments: 0 },
{ id: 'd-12009-s', t: 'AWST - Contractor Shift Report -South - TAGP - Rev00 - 12009 - 2026 - March-01.pdf', period: 'Mar 1, 2026', site: 'South', rev: 'Rev00', owner: 'D. Hayes', status: 'Approved', updated: '2 d', comments: 0 },
{ id: 'd-12009-n', t: 'AWST - Contractor Shift Report -North - TAGP - Rev00 - 12009 - 2026 - March-01.pdf', period: 'Mar 1, 2026', site: 'North', rev: 'Rev00', owner: 'M. Reyes', status: 'Need review', updated: '2 d', comments: 3 },
{ id: 'd-12008-s', t: 'AWST - Contractor Shift Report -South - TAGP - Rev01 - 12008 - 2026 - February-28.pdf', period: 'Feb 28, 2026', site: 'South', rev: 'Rev01', owner: 'D. Hayes', status: 'Approved', updated: '3 d', comments: 2 },
{ id: 'd-12008-n', t: 'AWST - Contractor Shift Report -North - TAGP - Rev00 - 12008 - 2026 - February-28.pdf', period: 'Feb 28, 2026', site: 'North', rev: 'Rev00', owner: 'M. Reyes', status: 'Approved', updated: '3 d', comments: 0 },
{ id: 'd-12007-s', t: 'AWST - Contractor Shift Report -South - TAGP - Rev00 - 12007 - 2026 - February-27.pdf', period: 'Feb 27, 2026', site: 'South', rev: 'Rev00', owner: 'L. Nguyen', status: 'Approved', updated: '4 d', comments: 0 },
{ id: 'd-12007-n', t: 'AWST - Contractor Shift Report -North - TAGP - Rev00 - 12007 - 2026 - February-27.pdf', period: 'Feb 27, 2026', site: 'North', rev: 'Rev00', owner: 'M. Reyes', status: 'Approved', updated: '4 d', comments: 1 },
{ id: 'd-12006-s', t: 'AWST - Contractor Shift Report -South - TAGP - Rev00 - 12006 - 2026 - February-26.pdf', period: 'Feb 26, 2026', site: 'South', rev: 'Rev00', owner: 'D. Hayes', status: 'Approved', updated: '5 d', comments: 0 },
{ id: 'd-12006-n', t: 'AWST - Contractor Shift Report -North - TAGP - Rev00 - 12006 - 2026 - February-26.pdf', period: 'Feb 26, 2026', site: 'North', rev: 'Rev00', owner: 'M. Reyes', status: 'Approved', updated: '5 d', comments: 2 }];
/* ===== Reports Data Grid ===== */
function ReportsDataGrid({ items, secondaryLabel = 'Period', secondaryKey = 'period', tertiaryLabel, tertiaryKey, totalCount }) {
return (
{tertiaryKey && }
| Name |
{secondaryLabel} |
{tertiaryKey && {tertiaryLabel} | }
Owner |
Status |
|
|
|
{items.map((it) =>
|
|
{it[secondaryKey]} |
{tertiaryKey && {it[tertiaryKey]} | }
{it.owner}
|
{it.status} |
{it.updated} |
{it.comments} |
|
)}
{totalCount &&
}
);
}
/* ===== Pagination ===== */
function DgPagination({ shown, total, page = 1 }) {
const pageSize = shown;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const visiblePages = [];
for (let i = 1; i <= Math.min(totalPages, 4); i++) visiblePages.push(i);
if (totalPages > 5) visiblePages.push('…');
if (totalPages > 4) visiblePages.push(totalPages);
return (
Showing 1–{shown} of {total}
{visiblePages.map((p, i) =>
p === '…' ?
… :
)}
);
}
/* ===== Reports section (generic for monthly/weekly/daily) ===== */
function SectionReports({ title, kicker, items, totalCount, secondaryLabel, secondaryKey, tertiaryLabel, tertiaryKey, onCreateRecord }) {
const [query, setQuery] = useStateCol('');
const filtered = items.filter((it) => it.t.toLowerCase().includes(query.toLowerCase()));
return (
{title}
{kicker &&
{kicker}
}
);
}
function SectionMonthly({ onCreateRecord }) {
return (
);
}
function SectionWeekly({ onCreateRecord }) {
return (
);
}
function SectionDaily({ onCreateRecord }) {
return (
);
}
/* ===== Section: All items ===== */
function SectionAllItems({ onOpenPhoto, onOpenRecord, onCreateRecord }) {
const [query, setQuery] = useStateCol('');
const PHOTO_ITEMS = PHOTOS.map((p) => ({ id: p.id, kind: 'photo', t: p.t, tag: 'Approved', updated: '4 d', comments: 0 }));
const MONTHLY_ITEMS = MONTHLY_REPORTS.map((r) => ({ id: r.id, kind: 'report', t: r.t, tag: r.status, updated: r.updated, comments: r.comments }));
const WEEKLY_ITEMS = WEEKLY_REPORTS.map((r) => ({ id: r.id, kind: 'report', t: r.t, tag: r.status, updated: r.updated, comments: r.comments }));
const DAILY_ITEMS = DAILY_REPORTS.map((r) => ({ id: r.id, kind: 'report', t: r.t, tag: r.status, updated: r.updated, comments: r.comments }));
const ALL = [...MONTHLY_ITEMS, ...WEEKLY_ITEMS, ...DAILY_ITEMS, ...PHOTO_ITEMS, ...RECORD_ITEMS];
const filtered = ALL.filter((it) => it.t.toLowerCase().includes(query.toLowerCase()));
return (
);
}
/* ===== Mobile sections tabs ===== */
function CollMobileTabs({ active, onPick }) {
return (
);
}
/* ===== Public: collection page ===== */
function CollectionPage({ onBack, onOpenRecord, onOpenPhoto, onOpenGraph, onCreateRecord, initialTab = 'overview', onTabChange }) {
const [active, setActive] = useStateCol(initialTab);
React.useEffect(() => {setActive(initialTab);}, [initialTab]);
const pickTab = (t) => {setActive(t);onTabChange && onTabChange(t);};
const openPhotoHere = (id) => onOpenPhoto && onOpenPhoto(id, active);
const openRecordHere = () => onOpenRecord && onOpenRecord();
return (
{active === 'overview' &&
}
{active === 'monthly' && }
{active === 'weekly' && }
{active === 'daily' && }
{active === 'photos' && }
{active === 'all' && }
{active === 'overview' &&
}
);
}
/* ===== Collection-level record menu (compact, 5 items) =====
Used when the record is shown inline as the collection's Overview.
Full set of actions (Details / Comments / Lineage / History) lives in
RecordContextMenu on the dedicated record page. */
function CollectionRecordContextMenu({ onOpen, align = 'right' }) {
const [menuOpen, setMenuOpen] = useStateCol(false);
const wrapRef = React.useRef(null);
React.useEffect(() => {
if (!menuOpen) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setMenuOpen(false);
};
const onKey = (e) => {if (e.key === 'Escape') setMenuOpen(false);};
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [menuOpen]);
const close = () => setMenuOpen(false);
const handleOpen = () => {close();if (onOpen) onOpen();};
return (
{menuOpen &&
}
);
}
/* ===== Reusable Record context menu (3-dots) ===== */
function RecordContextMenu({ onEdit, align = 'right' }) {
const [menuOpen, setMenuOpen] = useStateCol(false);
const sharedCtx = useOutput();
const isEmpty = sharedCtx?.isEmpty;
const [localPanel, setLocalPanel] = useStateCol(null);
const openPanel = sharedCtx ? sharedCtx.openPanel : localPanel;
const setOpenPanel = sharedCtx ? sharedCtx.setOpenPanel : setLocalPanel;
const mountPanelsLocally = !sharedCtx; // when shared, RecordPageBody mounts them
const wrapRef = React.useRef(null);
const su = useSourceUpdates(); // null on screens without provider
React.useEffect(() => {
if (!menuOpen) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setMenuOpen(false);
};
const onKey = (e) => {if (e.key === 'Escape') setMenuOpen(false);};
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [menuOpen]);
React.useEffect(() => {
if (!openPanel) return;
const onKey = (e) => {if (e.key === 'Escape') setOpenPanel(null);};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [openPanel]);
const close = () => setMenuOpen(false);
const open = (panel) => {setMenuOpen(false);setOpenPanel(panel);};
return (
{menuOpen &&
{!isEmpty && su && su.allCount > 0 &&
}
}
{mountPanelsLocally && openPanel === 'details' && setOpenPanel(null)} />}
{mountPanelsLocally && openPanel === 'comments' && setOpenPanel(null)} />}
{mountPanelsLocally && openPanel === 'lineage' && setOpenPanel(null)} />}
{mountPanelsLocally && openPanel === 'history' && setOpenPanel(null)} />}
);
}
/* ===== Output switcher chips (Dashboard / Report / Slide) ===== */
function OutputChips() {
const ctx = useOutput();
const output = ctx?.output || 'dashboard';
const setOutput = ctx?.setOutput || (() => {});
const setOpenPanel = ctx?.setOpenPanel || (() => {});
const isEmpty = ctx?.isEmpty;
const agentGenerated = ctx?.agentGenerated;
const extraOutputs = ctx?.extraOutputs || [];
const addOutput = ctx?.addOutput || (() => {});
const [menuOpen, setMenuOpen] = useStateCol(false);
const wrapRef = React.useRef(null);
React.useEffect(() => {
if (!menuOpen) return;
const onDoc = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setMenuOpen(false);
};
const onKey = (e) => {if (e.key === 'Escape') setMenuOpen(false);};
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [menuOpen]);
// Output chips:
// - During the initial empty (pre-generation) state: single "New output" chip.
// - After agent generation: just "Dashboard" + any user-added extras.
// - On the demo Cedar Narrows dashboard: all three default outputs.
let items;
if (isEmpty) {
items = [{ key: 'new-output', label: 'New output' }];
} else if (agentGenerated) {
items = [{ key: 'dashboard', label: 'Dashboard' }, ...extraOutputs];
} else {
items = [
{ key: 'dashboard', label: 'Dashboard' },
{ key: 'report', label: 'Report' },
{ key: 'slide', label: 'Slide' }];
}
// Only show the + when the record exists and isn't still being generated.
const showAddBtn = !isEmpty;
const openChipPanel = (panel) => () => {setMenuOpen(false);setOpenPanel(panel);};
return (
{items.map((it) => {
const isActive = isEmpty ? true : output === it.key;
return (
{isActive && menuOpen &&
}
);
})}
{showAddBtn && (
)}
);
}
/* ===== Record top bar (single-line, no sections sidebar) ===== */
function RecordTopBar({ onBack }) {
const ctx = useOutput();
const baseCommentsCount = window.RP_COMMENTS_COUNT || 0;
const agentGenerated = ctx?.agentGenerated;
// Freshly agent-generated records start with zero comments — they're a new
// artifact nobody has reviewed yet.
const commentsCount = agentGenerated ? 0 : baseCommentsCount;
const isEmpty = ctx?.isEmpty;
const newRecord = ctx?.newRecord;
const setNewRecordName = ctx?.setNewRecordName || (() => {});
const openComments = () => {
if (ctx) ctx.setOpenPanel({ panel: 'comments', scope: 'All outputs' });
};
return (
{!isEmpty &&
James Anderson · Marketing Team
·
4 min ago
}
{isEmpty &&
James Anderson · Marketing Team
·
Just now
}
{!isEmpty &&
}
{isEmpty &&
Draft}
);
}
/* =========================================================================
OUTPUT: REPORT — long-form article with hero + sections + inline photos
========================================================================= */
function OutputReport({ onOpenPhoto }) {
// Local EB helper — same pattern as SectionOverview etc.
const EB = ({ fields, render, sources = {}, className = '' }) => {
const init = Object.fromEntries(fields.map((f) => [f.id, f.default]));
return (
{(v) => render(v)}
);
};
const photoOptions = PHOTOS.map((p) => p.id);
const photoLabel = (id) => {
const p = PHOTOS.find((x) => x.id === id);
return p ? `${p.id} — ${p.t}` : id;
};
// For text inputs we display as id (so users can also paste any id).
const renderInlinePhoto = (photoId, caption) => {
const p = PHOTOS.find((x) => x.id === photoId);
if (!p) return null;
return (
{p.t}.{' '}{caption || p.s}
);
};
return (
{/* Hero */}
{
const hero = PHOTOS.find((x) => x.id === v.heroPhoto);
return (
{v.kicker}
{v.title}
{v.byline}
);
}} />
{/* Lede */}
{v.lede}
} />
{/* 1 — Status & milestones */}
{v.t}
} />
{v.p}
} />
{v.bullets.split('\n').filter(Boolean).map((line, i) => - {line}
)}
} />
renderInlinePhoto(v.photo, v.cap)} />
{/* 2 — Safety */}
{v.t}
} />
{v.p}
} />
{v.bullets.split('\n').filter(Boolean).map((line, i) => - {line}
)}
} />
renderInlinePhoto(v.photo, v.cap)} />
{/* 3 — Pipe installation progress */}
{v.t}
} />
{v.p}
} />
{v.bullets.split('\n').filter(Boolean).map((line, i) => - {line}
)}
} />
renderInlinePhoto(v.photo, v.cap)} />
{/* 4 — Financial / commercial */}
{v.t}
} />
{v.p}
} />
{v.bullets.split('\n').filter(Boolean).map((line, i) => - {line}
)}
} />
);
}
/* =========================================================================
OUTPUT: SLIDE — 16:9, full-width, headline metrics only
========================================================================= */
function OutputSlide() {
const EB = ({ fields, render, sources = {}, className = '' }) => {
const init = Object.fromEntries(fields.map((f) => [f.id, f.default]));
return (
{(v) => render(v)}
);
};
return (
{v.brand}} />
{v.date}} />
{v.kicker}
} />
{v.title}
} />
{v.label}
{v.value}
{v.sub}
} />
{v.label}
{v.value}
{v.sub}
} />
{v.label}
{v.value}
{v.sub}
} />
{v.label}
{v.value}
{v.sub}
} />
{v.foot}} />
);
}
/* ===== Empty state for a brand-new record =====
Shown the moment a user clicks "+" on a section toolbar (Monthly reports,
Weekly reports, Daily reports, Photos, All items). The record exists but
has no artifact yet — we use this surface to explain what a Record is and
to give two ways to start (AI agent or template library).
Renders the template-library get-started state. */
function RecordEmptyState({ onOpenAgentChat }) {
return ;
}
/* ===== Public: record page ===== */
function RecordPage({ onBack, onOpenPhoto, newRecord, setNewRecordName, onOpenAgentChat, generating, genStageIdx, genStages, agentGenerated, onTemplateGenerate, onTemplateCreateEmpty, templateArtifact }) {
return (
);
}
function RecordPageBody({ onBack, onOpenPhoto }) {
const { output, openPanel, setOpenPanel, isEmpty, onOpenAgentChat, generating, genStageIdx, genStages, agentGenerated, templateArtifact, extraOutputs, setOutputState } = useOutput();
// openPanel may be a string ('details' | 'comments' | ...) OR an object
// { panel: '...', scope: 'All outputs' } when the caller wants to override
// a panel's default filter (e.g. header comment-count button).
const panelKey = typeof openPanel === 'string' ? openPanel : openPanel?.panel;
const panelOpts = openPanel && typeof openPanel === 'object' ? openPanel : null;
const activeExtra = extraOutputs && extraOutputs.find((o) => o.key === output);
const isExtraActive = !!activeExtra;
// Template inserted via "Create empty": the record is still in its empty/new
// shell (editable title, Draft tag) but renders the template dashboard as its
// first artifact instead of the get-started state.
const showTemplateArtifact = isEmpty && !generating && templateArtifact;
const showEmptyState = isEmpty && !generating && !templateArtifact;
const isEmptyLayout = showEmptyState || isExtraActive; // centered get-started layout
const fixedWidth = !isEmpty && !isExtraActive; // real demo dashboard only
const mainKind = isEmpty
? (generating ? 'generating' : (showTemplateArtifact ? 'dashboard' : 'empty'))
: (isExtraActive ? 'empty' : output);
const screenLabel = isEmpty
? (generating ? 'Cedar Narrows Record — Generating dashboard'
: (templateArtifact ? 'Cedar Narrows Record — Template inserted' : 'Cedar Narrows Record — New (empty)'))
: (isExtraActive ? `Cedar Narrows Record — ${activeExtra.label} (empty)` : `Cedar Narrows Record — ${OUTPUT_LABELS[output] || 'Dashboard'}`);
return (
{!isEmpty && !isExtraActive &&
}
{isEmpty && generating &&
}
{showEmptyState &&
}
{showTemplateArtifact &&
}
{!isEmpty && isExtraActive &&
}
{!isEmpty && !isExtraActive && output === 'dashboard' &&
}
{!isEmpty && !isExtraActive && output === 'report' && }
{!isEmpty && !isExtraActive && output === 'slide' && }
{panelKey === 'details' &&
setOpenPanel(null)} />}
{panelKey === 'comments' && setOpenPanel(null)} initialScope={panelOpts?.scope} />}
{panelKey === 'lineage' && setOpenPanel(null)} />}
{panelKey === 'history' && setOpenPanel(null)} />}
);
}
// Reads the provider state to mount the side panel when requested
function RecordSourceUpdatesPanelMount() {
const ctx = useSourceUpdates();
if (!ctx || !ctx.panelOpen) return null;
return ;
}
/* ===== Public: photo page ===== */
function PhotoPage({ photoId, onBack }) {
const photo = PHOTOS.find((p) => p.id === photoId) || PHOTOS[0];
return (
);
}
/* ===== Public: hub-home dashboard =====
Reuses the record's Dashboard output (Overview + Safety + Finance + Photos)
verbatim, read-only (editable=false), with the duplicate title suppressed
because the hub-home header already carries it. */
function HubDashboard({ onOpenPhoto }) {
return (
);
}
window.HubDashboard = HubDashboard;
window.CollectionPage = CollectionPage;
window.RecordPage = RecordPage;
window.PhotoPage = PhotoPage;
// Dashboard primitives reused by other hub dashboards (e.g. Finance).
Object.assign(window, {
StatTile, ProgressBar, BarChart, GroupedBarChart,
RichTrendChart, AnalysisCard, HeadlineText,
});