diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/ConditionView.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/ConditionView.tsx
index 6a3904db..3ce8b99d 100644
--- a/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/ConditionView.tsx
+++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/ConditionView.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { Drawer as Vaul } from "vaul";
import {
HomeModernIcon,
BuildingOffice2Icon,
@@ -13,8 +14,14 @@ import {
ChevronRightIcon,
Squares2X2Icon,
CalendarDaysIcon,
+ XMarkIcon,
} from "@heroicons/react/24/outline";
-import { elementLabel, renewalUrgency, type RenewalUrgency } from "@/lib/condition/labels";
+import {
+ elementLabel,
+ aspectLabel,
+ renewalUrgency,
+ type RenewalUrgency,
+} from "@/lib/condition/labels";
import {
CONDITION_CATEGORIES,
categoryKeyOf,
@@ -46,7 +53,6 @@ function earliestInstallYear(aspects: AspectInput[]): number | null {
const years = aspects.map((a) => yearOf(a.installDate)).filter((y): y is number => y !== null);
return years.length ? Math.min(...years) : null;
}
-// Headline value for a component: prefer material, else a rating/condition aspect.
function primaryValue(aspects: AspectInput[]): string | null {
const mat = aspects.find((a) => a.aspectType === "material" && a.value)?.value;
if (mat) return mat;
@@ -56,6 +62,10 @@ function primaryValue(aspects: AspectInput[]): string | null {
}
return aspects.find((a) => a.value)?.value ?? null;
}
+function categoryLabelOf(elementType: string): string {
+ const key = categoryKeyOf(elementType);
+ return CONDITION_CATEGORIES.find((c) => c.key === key)?.label ?? "Component";
+}
function ElementIcon({ elementType, className }: { elementType: string; className?: string }) {
const t = elementType;
@@ -120,7 +130,7 @@ function SectionLabel({ children, count }: { children: React.ReactNode; count?:
}
// ── Priority worklist ─────────────────────────────────────────────────────────
-function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
+function Worklist({ rows, currentYear, onSelect }: { rows: Row[]; currentYear: number; onSelect: (r: Row) => void }) {
const comps = rows.filter((r) => !r.isAssessment);
const assess = rows.filter((r) => r.isAssessment);
const byYear = (a: Row, b: Row) => (a.el.renewalYear ?? Infinity) - (b.el.renewalYear ?? Infinity);
@@ -128,6 +138,7 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
const soon = comps.filter((r) => r.urgency === "soon").sort(byYear);
const ok = comps.filter((r) => r.urgency === "ok").sort(byYear);
const attn = [...overdue, ...soon];
+ const key = (r: Row) => `${r.el.elementType}__${r.el.elementInstance}`;
return (
@@ -140,9 +151,10 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
const span = row.installed != null && row.el.renewalYear != null ? row.el.renewalYear - row.installed : 0;
const pct = span > 0 ? Math.max(0, Math.min(100, ((currentYear - row.installed!) / span) * 100)) : 0;
return (
-
onSelect(row)}
+ className={`group grid w-full grid-cols-[auto_1fr_auto_auto] items-center gap-4 rounded-2xl border bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:shadow-md ${u.border}`}
>
@@ -167,7 +179,8 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
{row.urgency === "overdue" ? "Replace" : "Plan"}
-
+
+
);
})}
@@ -179,16 +192,20 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
On the horizon
{ok.slice(0, 6).map((row) => (
-
onSelect(row)}
+ className="group flex items-center justify-between gap-2 rounded-xl border border-gray-100 bg-white px-4 py-3 text-left transition-colors hover:border-brandblue/30 hover:bg-gray-50"
>
{elementLabel(row.el.elementType)}
- {row.value &&
{row.value}
}
+ {row.value &&
{row.value}
}
-
{row.el.renewalYear}
-
+
+ {row.el.renewalYear}
+
+
+
))}
@@ -202,13 +219,17 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
{ok.map((row) => (
-
onSelect(row)}
+ className="group flex items-center justify-between border-t border-gray-50 px-5 py-2.5 text-left text-[13px] transition-colors first:border-t-0 hover:bg-gray-50 sm:[&:nth-child(2)]:border-t-0"
>
{elementLabel(row.el.elementType)}
- {row.el.renewalYear}
-
+
+ {row.el.renewalYear}
+
+
+
))}
@@ -224,13 +245,15 @@ function Worklist({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
{assess.map((row) => {
const flag = /category\s*[12]/i.test(row.value ?? "");
return (
- onSelect(row)}
+ className={`group inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors ${flag ? "border-amber-200 bg-amber-50 hover:border-amber-300" : "border-gray-100 bg-white hover:border-brandblue/30"}`}
>
{elementLabel(row.el.elementType)}
{row.value ?? "—"}
-
+
+
);
})}
@@ -245,7 +268,7 @@ const LO = 2015;
const HI = 2060;
const xOf = (y: number) => ((Math.min(Math.max(y, LO), HI) - LO) / (HI - LO)) * 100;
-function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
+function Roadmap({ rows, currentYear, onSelect }: { rows: Row[]; currentYear: number; onSelect: (r: Row) => void }) {
const dated = rows.filter((r) => !r.isAssessment && r.el.renewalYear !== null);
const assessCount = rows.length - dated.length;
const nowX = xOf(currentYear);
@@ -253,12 +276,11 @@ function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
const groups = CONDITION_CATEGORIES.map((c) => ({
category: c,
- rows: dated.filter((r) => categoryKeyOf(r.el.elementType) === c.key).sort((a, b) => (a.el.renewalYear! - b.el.renewalYear!)),
+ rows: dated.filter((r) => categoryKeyOf(r.el.elementType) === c.key).sort((a, b) => a.el.renewalYear! - b.el.renewalYear!),
})).filter((g) => g.rows.length > 0);
return (
- {/* Axis */}
@@ -284,31 +306,33 @@ function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
const x0 = row.installed != null ? xOf(row.installed) : Math.max(0, xOf(row.el.renewalYear!) - 3);
const x1 = xOf(row.el.renewalYear!);
return (
-
onSelect(row)}
+ className="group grid w-full grid-cols-[130px_1fr] items-center px-4 py-0.5 text-left transition-colors hover:bg-gray-50 sm:grid-cols-[160px_1fr]"
>
-
- {elementLabel(row.el.elementType)}
+
+
+ {elementLabel(row.el.elementType)}
-
+
{x1 < 86 && (
{row.el.renewalYear}
)}
-
+
);
})}
))}
{assessCount > 0 && (
-
+
{assessCount} health & safety assessment{assessCount === 1 ? "" : "s"} not shown — they carry a rating, not a renewal date.
)}
@@ -316,9 +340,106 @@ function Roadmap({ rows, currentYear }: { rows: Row[]; currentYear: number }) {
);
}
-// ── Shell: summary + view toggle ──────────────────────────────────────────────
+// ── Aspect drill-down drawer ──────────────────────────────────────────────────
+function AspectDrawer({ row, currentYear, onClose }: { row: Row; currentYear: number; onClose: () => void }) {
+ const el = row.el;
+ const u = U[row.urgency];
+ const span = row.installed != null && el.renewalYear != null ? el.renewalYear - row.installed : 0;
+ const pct = span > 0 ? Math.max(0, Math.min(100, ((currentYear - row.installed!) / span) * 100)) : 0;
+
+ return (
+
+
+
+
{categoryLabelOf(el.elementType)}
+
+
+
+
+
+ {elementLabel(el.elementType)}
+ {el.elementInstance > 1 && #{el.elementInstance}}
+
+
+
+ {el.source} · surveyed {el.date}
+
+
+
+ {!row.isAssessment && (
+
+
+
{statusLine(row, currentYear)}
+
{el.renewalYear ?? "—"}
+
+ {span > 0 && (
+
+
+
+ Installed {row.installed}
+ {span}-yr life
+ Renew {el.renewalYear}
+
+
+ )}
+
+ )}
+
+
+
+ Aspects · {el.aspects.length}
+
+
+
+
+ | Aspect |
+ Value |
+ Inst. |
+ Renew |
+
+
+
+ {el.aspects.map((a, i) => {
+ const au = urgencyOf(a.renewalYear, currentYear);
+ return (
+
+ | {aspectLabel(a.aspectType)} |
+
+ {a.value ?? —}
+ {a.comments && {a.comments} }
+ |
+
+ {yearOf(a.installDate) ?? —}
+ |
+
+ {a.renewalYear === null ? (
+ —
+ ) : (
+ {a.renewalYear}
+ )}
+ |
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+// ── Shell: summary + view toggle + drawer ─────────────────────────────────────
export function ConditionView({ elements, currentYear }: { elements: ResolvedElement[]; currentYear: number }) {
const [view, setView] = useState<"worklist" | "roadmap">("worklist");
+ const [selected, setSelected] = useState
(null);
const rows = elements.map((el) => toRow(el, currentYear));
const comps = rows.filter((r) => !r.isAssessment);
@@ -335,12 +456,12 @@ export function ConditionView({ elements, currentYear }: { elements: ResolvedEle
? { n: counts.soon, label: `component${counts.soon === 1 ? "" : "s"} due soon`, tone: U.soon.tone }
: { n: counts.ok, label: "in good condition", tone: U.ok.tone };
- const tab = (key: "worklist" | "roadmap", label: string, Icon: React.ElementType) => (
+ const tab = (k: "worklist" | "roadmap", label: string, Icon: React.ElementType) => (
- {/* View toggle */}
-
+ {/* Hint + view toggle */}
+
+
Select a component to view its aspects
{tab("worklist", "Priority", Squares2X2Icon)}
{tab("roadmap", "Timeline", CalendarDaysIcon)}
- {view === "worklist" ?
:
}
+ {view === "worklist" ? (
+
+ ) : (
+
+ )}
+
+
!open && setSelected(null)} direction="right">
+
+
+
+ {selected && setSelected(null)} />}
+
+
+
);
}