/* ============================================================
Report panel + assessor cards + score + detail drawer
============================================================ */
const StatusIcon = ({ status, size }) => {
const s = PC_STATUS[status] || PC_STATUS.unknown;
return {s.glyph};
};
const StatusPill = ({ status }) => {
const s = PC_STATUS[status] || PC_STATUS.unknown;
return {s.glyph}{s.label};
};
const ConfBadge = ({ level }) => {
if (!level) return null;
const n = PC_CONF_LEVEL[level] || 0;
return (
{[0, 1, 2].map(i => )}
{level}
);
};
function verdictFor(score) {
if (score == null) return 'Awaiting assessment';
if (score >= 80) return 'Strong prospect';
if (score >= 65) return 'Promising';
if (score >= 50) return 'Marginal';
return 'Weak prospect';
}
const ScoreDial = ({ score, status }) => {
const r = 32, c = 2 * Math.PI * r;
const pct = score == null ? 0 : score / 100;
const col = { green: 'var(--st-green)', amber: 'var(--st-amber)', red: 'var(--st-red)', unknown: 'var(--st-unknown)' }[status] || 'var(--st-unknown)';
return (
{score == null ? '–' : score}
/ 100
);
};
const ValueRows = ({ values }) => {
const entries = Object.entries(values || {});
if (!entries.length) return null;
return (
{entries.map(([k, v]) => (
{k}{v}
))}
);
};
const AssessorCard = ({ result, onOpen, clickable }) => {
const meta = PC_ASSESSOR_META[result.assessor_key] || { name: result.assessor_key };
const isUnknown = result.status === 'unknown';
return (
onOpen(result) : undefined}>
{meta.name}
{result.headline}
{clickable && !isUnknown &&
{PCI.chevron}}
{!isUnknown &&
}
{isUnknown ? (
{PCI.clock}Not yet implemented — this check will fill in automatically once the assessor is online.
) : (
{result.source}
{result.source_retrieved_at && · retrieved {result.source_retrieved_at}}
)}
);
};
const ReportSection = ({ section, results, onOpen, clickable, headerAction }) => {
const inSection = section.keys
.map(k => results.find(r => r.assessor_key === k))
.filter(Boolean);
if (!inSection.length) return null;
const pending = inSection.filter(r => r.status === 'unknown').length;
return (
{section.label}
{pending ? `${pending} pending` : `${inSection.length} checked`}
{headerAction &&
{headerAction}}
{inSection.map(r =>
)}
);
};
/** Compact on/off pill — used by section headers (e.g. "Show on map"). */
const SectionToggle = ({ on, disabled, onChange, label }) => (
);
const EmptyPanel = () => (
No site selected
Draw a boundary or search an address to run a fast-screen assessment. Or pick a site from the pipeline.
);
const PC_LOADING_STEPS = [
'Matching boundary to LINZ parcels',
'Resolving land tenure & titles',
'Querying grid proximity',
'Modelling solar resource',
'Reading terrain & slope',
'Checking planning constraints',
];
/* Inline toast. Accepts a string (simple notification) or an object
{ msg, kind, actions: [{label, primary, onClick}] } for action toasts
(e.g. Save / Discard after a boundary edit). */
const Toast = ({ msg }) => {
if (!msg) return null;
const t = typeof msg === 'string' ? { msg } : msg;
return (
{t.msg}
{(t.actions || []).map((a, i) => (
))}
);
};
const ErrorPanel = ({ error, onClose }) => (
Assessment failed
{error || 'The backend could not complete the assessment.'}
Check that Start-Prospecting.ps1 launched the backend on port 8766, then try again.
);
const LoadingPanel = ({ step }) => (
Running assessment
Fast-screening the boundary against every assessor.
{PC_LOADING_STEPS.map((label, i) => (
{i < step ? {PCI.check}
: i === step ?
: }
{label}
))}
);
const ReportPanel = ({ site, onClose, onExport, onOpenAssessor, clickable, gridSectionToggle }) => {
const pending = site.assessors.filter(a => a.status === 'unknown').length;
const total = site.assessors.length;
return (
<>
Site report · {site.id}
{site.name}
{site.address}
{site.area_ha} ha · {site.shape_descriptor}
Fast-screen score
{verdictFor(site.overall_score)}
{pending} of {total} assessors pending — score reflects geometry & tenure only.
{PC_SECTIONS.map(sec => {
const headerAction =
sec.id === 'grid' && gridSectionToggle ? (
) : null;
return (
);
})}
>
);
};
const DetailDrawer = ({ assessor, open, onClose, width }) => {
const a = assessor;
const meta = a ? (PC_ASSESSOR_META[a.assessor_key] || { name: a.assessor_key }) : {};
return (
<>
>
);
};
Object.assign(window, {
StatusIcon, StatusPill, ConfBadge, ScoreDial, AssessorCard, ReportSection,
SectionToggle, ReportPanel, LoadingPanel, EmptyPanel, ErrorPanel, DetailDrawer,
Toast, verdictFor, PC_LOADING_STEPS,
});