diff --git a/src/app/db/db.ts b/src/app/db/db.ts
index 52196123..992e73d7 100644
--- a/src/app/db/db.ts
+++ b/src/app/db/db.ts
@@ -12,6 +12,7 @@ import * as Relations from "@/app/db/schema/relations";
import * as Users from "@/app/db/schema/users";
import * as CrmSchema from "@/app/db/schema/crm/hubspot_deal_table";
import * as UploadedFilesSchema from "@/app/db/schema/uploaded_files";
+import * as PortfolioOrgSchema from "@/app/db/schema/portfolio_organisation";
export const pool = new Pool({
host: process.env.DB_HOST,
@@ -35,6 +36,7 @@ const schema = {
...Users,
...CrmSchema,
...UploadedFilesSchema,
+ ...PortfolioOrgSchema,
};
export const db = drizzle(pool, {
diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json
index 9fee2916..4b628e3e 100644
--- a/src/app/db/migrations/meta/_journal.json
+++ b/src/app/db/migrations/meta/_journal.json
@@ -1142,6 +1142,13 @@
"when": 1775041844023,
"tag": "0162_powerful_paladin",
"breakpoints": true
+ },
+ {
+ "idx": 163,
+ "version": "7",
+ "when": 1775309608582,
+ "tag": "0163_fat_mentallo",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx
index b4935313..169b9657 100644
--- a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx
@@ -35,6 +35,7 @@ import { PortfolioGoal as PortfolioGoalOptions } from "@/app/db/schema/portfolio
import { useSession } from "next-auth/react";
import PortfolioPlanTable from "@/app/components/portfolio/measures/PlanTable";
import { UsersPermissionsCard } from "./UsersPermissionsCard";
+import OrganisationLinkCard from "./OrganisationLinkCard";
// dropdown selection component for both goal and status
@@ -215,9 +216,11 @@ async function deletePortfolio({
export default function PortfolioSettings({
portfolioId,
portfolioSettingsData,
+ isDomnaUser = false,
}: {
portfolioId: string;
portfolioSettingsData: PortfolioSettingsType;
+ isDomnaUser?: boolean;
}) {
// This is a client component so we can access the session directly
const session = useSession();
@@ -475,6 +478,7 @@ export default function PortfolioSettings({
+ {isDomnaUser && }
diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx
index 67ae43fb..4732c675 100644
--- a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx
@@ -1,3 +1,5 @@
+import { getServerSession } from "next-auth";
+import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getPortfolioSettings } from "../../utils";
import PortfolioSettings from "./PortfolioSettings";
@@ -8,7 +10,13 @@ export default async function PortfolioSettingsPage(
) {
const params = await props.params;
const portfolioId = params.slug;
- const portfolioSettingsData = await getPortfolioSettings(portfolioId);
+
+ const [portfolioSettingsData, session] = await Promise.all([
+ getPortfolioSettings(portfolioId),
+ getServerSession(AuthOptions),
+ ]);
+
+ const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes");
return (
<>
@@ -16,6 +24,7 @@ export default async function PortfolioSettingsPage(
>
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx
index 5aea535d..73b3dec2 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx
@@ -95,6 +95,66 @@ function StatCard({
);
}
+// -----------------------------------------------------------------------
+// Per-stage column config for the drill-down table
+// -----------------------------------------------------------------------
+type StageTableConfig = {
+ cols: (keyof ClassifiedDeal)[];
+ labels: Partial>;
+};
+
+const STAGE_TABLE_CONFIG: Record = {
+ "Booking in Progress": {
+ cols: ["dealname", "landlordPropertyId", "confirmedSurveyDate", "ioeV1Date"],
+ labels: {
+ dealname: "Address",
+ landlordPropertyId: "Ref",
+ confirmedSurveyDate: "Confirmed Survey Date",
+ ioeV1Date: "Expected Commencement",
+ },
+ },
+ "Assessment in Progress": {
+ cols: ["dealname", "landlordPropertyId", "confirmedSurveyDate", "ioeV1Date", "outcome", "coordinator"],
+ labels: {
+ dealname: "Address",
+ landlordPropertyId: "Ref",
+ confirmedSurveyDate: "Confirmed Survey Date",
+ ioeV1Date: "Expected Commencement",
+ outcome: "Outcome",
+ coordinator: "Surveyor",
+ },
+ },
+ "Coordination in Progress": {
+ cols: ["dealname", "landlordPropertyId", "coordinator", "preSapScore", "coordinationStatus"],
+ labels: {
+ dealname: "Address",
+ landlordPropertyId: "Ref",
+ coordinator: "Coordinator",
+ preSapScore: "Pre-SAP Score",
+ coordinationStatus: "Coordination Status",
+ },
+ },
+ "Design in Progress": {
+ cols: ["dealname", "landlordPropertyId", "designer", "proposedMeasures", "designType"],
+ labels: {
+ dealname: "Address",
+ landlordPropertyId: "Ref",
+ designer: "Designer",
+ proposedMeasures: "Proposed Measures",
+ designType: "Design Type",
+ },
+ },
+ _default: {
+ cols: ["dealname", "landlordPropertyId", "displayStage", "installer"],
+ labels: {
+ dealname: "Address",
+ landlordPropertyId: "Ref",
+ displayStage: "Stage",
+ installer: "Installer",
+ },
+ },
+};
+
// -----------------------------------------------------------------------
// Pipeline Funnel — rich card rows
// -----------------------------------------------------------------------
@@ -116,7 +176,7 @@ function PipelineFunnel({
reason?: string,
) => void;
}) {
- const [mode, setMode] = useState<"current" | "cumulative">("cumulative");
+ const [mode, setMode] = useState<"current" | "cumulative">("current");
const ALWAYS_VISIBLE: DisplayStage[] = ["At Lodgement", "Project Complete"];
const visibleStages = funnelStages.filter(
@@ -180,34 +240,10 @@ function PipelineFunnel({
key={s.stage}
whileHover={{ scale: 1.01, y: -1 }}
transition={{ duration: 0.15 }}
- onClick={() =>
- onOpenTable(
- `Pipeline — ${s.stage}`,
- deals,
- [
- "dealname",
- "landlordPropertyId",
- "displayStage",
- "coordinator",
- "designer",
- "installer",
- ],
- {
- dealname: "Address",
- landlordPropertyId: "Ref",
- displayStage: "Stage",
- coordinator: "Coordinator",
- designer: "Designer",
- installer: "Installer",
- },
- undefined,
- `Pipeline — ${s.stage}`,
- mode === "cumulative"
- ? `Properties that have reached the "${s.stage}" stage or beyond.`
- : `Properties currently at the "${s.stage}" stage.`,
- undefined,
- )
- }
+ onClick={() => {
+ const { cols, labels } = STAGE_TABLE_CONFIG[s.stage] ?? STAGE_TABLE_CONFIG._default;
+ onOpenTable(`Pipeline — ${s.stage}`, deals, cols, labels);
+ }}
className={`w-full text-left rounded-xl border ${c.border} ${c.bg} p-4 shadow-sm hover:shadow-md transition-shadow`}
type="button"
>
@@ -339,8 +375,6 @@ export default function AnalyticsView({
{/* Row 1.5: Completion trends chart */}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx
index 9f92ec74..d869e97b 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx
@@ -2,9 +2,18 @@
import { useState } from "react";
import { AlertCircle } from "lucide-react";
-import { Card, Title, BarChart, Legend } from "@tremor/react";
-import { Button } from "@/app/shadcn_components/ui/button";
-import { Input } from "@/app/shadcn_components/ui/input";
+import { Card, Title } from "@tremor/react";
+import {
+ BarChart as RechartsBarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ LabelList,
+ ResponsiveContainer,
+ Legend as RechartsLegend,
+} from "recharts";
import {
Select,
SelectContent,
@@ -15,7 +24,6 @@ import type { ClassifiedDeal } from "./types";
interface CompletionTrendsChartProps {
deals: ClassifiedDeal[];
- isDomnaUser?: boolean;
projectCode?: string;
onOpenTable?: (
stage: string,
@@ -45,13 +53,13 @@ const METRICS = [
},
];
-// Brand colour palette (hex values work directly with Tremor v3 colors prop)
+// Brand colour palette
const C = {
- blue: "#5d6be0", // lighter periwinkle blue
- midblue: "#3943b7", // brandmidblue
- lightblue: "#8b96e9", // soft pale blue for 3rd series
- paleblue: "#b8bef4", // 4th series — replaces brown in bar charts
- brown: "#c4a47c", // accent only — trend/target lines
+ blue: "#5d6be0",
+ midblue: "#3943b7",
+ lightblue: "#8b96e9",
+ paleblue: "#b8bef4",
+ brown: "#c4a47c",
};
function ChartTooltip({
@@ -64,10 +72,13 @@ function ChartTooltip({
label?: string;
}) {
if (!active || !payload?.length) return null;
+ // Filter out the internal _total key
+ const visible = payload.filter((p) => p.name !== "_total");
+ if (!visible.length) return null;
return (
{label}
- {payload.map((item, i) => (
+ {visible.map((item, i) => (
= {};
for (const deal of deals) {
if (filter && !filter(deal)) continue;
- const date = deal[dateField as keyof ClassifiedDeal] as
- | string
- | Date
- | null;
+ const date = deal[dateField as keyof ClassifiedDeal] as string | Date | null;
if (!date) continue;
const d = new Date(date);
if (isNaN(d.getTime())) continue;
@@ -153,16 +159,14 @@ function aggregateByWeek(
}));
}
-// Coordination: buckets V1 and V2 completions separately
function aggregateCoordinationByWeek(
deals: ClassifiedDeal[],
-): Array<{ week: string; "V1 (MTP)": number; "V2 (Re-model)": number }> {
+): Array<{ week: string; "V1 (MTP)": number; "V2 (Re-model)": number; _total: number }> {
const v1Counts: Record = {};
const v2Counts: Record = {};
for (const deal of deals) {
const status = (deal.coordinationStatus ?? "").toUpperCase();
-
if (status.includes("(V1) IOE/MTP COMPLETE") && deal.ioeV1Date) {
const d = new Date(deal.ioeV1Date);
if (!isNaN(d.getTime())) {
@@ -170,7 +174,6 @@ function aggregateCoordinationByWeek(
v1Counts[key] = (v1Counts[key] || 0) + 1;
}
}
-
if (status.includes("(V2) IOE/MTP COMPLETE") && deal.ioeV2Date) {
const d = new Date(deal.ioeV2Date);
if (!isNaN(d.getTime())) {
@@ -184,17 +187,16 @@ function aggregateCoordinationByWeek(
Array.from(new Set([...Object.keys(v1Counts), ...Object.keys(v2Counts)])),
);
- return allKeys.map((isoKey) => ({
- week: formatMonday(isoKey),
- "V1 (MTP)": v1Counts[isoKey] ?? 0,
- "V2 (Re-model)": v2Counts[isoKey] ?? 0,
- }));
+ return allKeys.map((isoKey) => {
+ const v1 = v1Counts[isoKey] ?? 0;
+ const v2 = v2Counts[isoKey] ?? 0;
+ return { week: formatMonday(isoKey), "V1 (MTP)": v1, "V2 (Re-model)": v2, _total: v1 + v2 };
+ });
}
-// Assessments: Retrofit Assessment vs EPC, keyed by surveyedDate
function aggregateAssessmentsByWeek(
deals: ClassifiedDeal[],
-): Array<{ week: string; "Retrofit Assessment": number; EPC: number }> {
+): Array<{ week: string; "Retrofit Assessment": number; EPC: number; _total: number }> {
const retrofitCounts: Record = {};
const epcCounts: Record = {};
@@ -204,32 +206,27 @@ function aggregateAssessmentsByWeek(
const isEpc = o === "EPC Completed";
if (!isRetrofit && !isEpc) continue;
if (!deal.surveyedDate) continue;
-
const d = new Date(deal.surveyedDate);
if (isNaN(d.getTime())) continue;
const key = getMondayOfWeek(d);
-
if (isRetrofit) retrofitCounts[key] = (retrofitCounts[key] || 0) + 1;
if (isEpc) epcCounts[key] = (epcCounts[key] || 0) + 1;
}
const allKeys = fillWeekGaps(
- Array.from(
- new Set([...Object.keys(retrofitCounts), ...Object.keys(epcCounts)]),
- ),
+ Array.from(new Set([...Object.keys(retrofitCounts), ...Object.keys(epcCounts)])),
);
- return allKeys.map((isoKey) => ({
- week: formatMonday(isoKey),
- "Retrofit Assessment": retrofitCounts[isoKey] ?? 0,
- EPC: epcCounts[isoKey] ?? 0,
- }));
+ return allKeys.map((isoKey) => {
+ const r = retrofitCounts[isoKey] ?? 0;
+ const e = epcCounts[isoKey] ?? 0;
+ return { week: formatMonday(isoKey), "Retrofit Assessment": r, EPC: e, _total: r + e };
+ });
}
-// Lodgements: Stage 1 vs Lodged Measures
function aggregateLodgementsByWeek(
deals: ClassifiedDeal[],
-): Array<{ week: string; "Stage 1 Lodgement": number; "Lodged Measures": number }> {
+): Array<{ week: string; "Stage 1 Lodgement": number; "Lodged Measures": number; _total: number }> {
const stageCounts: Record = {};
const measuresCounts: Record = {};
@@ -251,19 +248,16 @@ function aggregateLodgementsByWeek(
}
const allKeys = fillWeekGaps(
- Array.from(
- new Set([...Object.keys(stageCounts), ...Object.keys(measuresCounts)]),
- ),
+ Array.from(new Set([...Object.keys(stageCounts), ...Object.keys(measuresCounts)])),
);
- return allKeys.map((isoKey) => ({
- week: formatMonday(isoKey),
- "Stage 1 Lodgement": stageCounts[isoKey] ?? 0,
- "Lodged Measures": measuresCounts[isoKey] ?? 0,
- }));
+ return allKeys.map((isoKey) => {
+ const s = stageCounts[isoKey] ?? 0;
+ const m = measuresCounts[isoKey] ?? 0;
+ return { week: formatMonday(isoKey), "Stage 1 Lodgement": s, "Lodged Measures": m, _total: s + m };
+ });
}
-// Designs: stacked by design type (Uploaded only)
function aggregateDesignsByWeek(
deals: ClassifiedDeal[],
): Array> {
@@ -275,10 +269,8 @@ function aggregateDesignsByWeek(
const d = new Date(deal.designDate);
if (isNaN(d.getTime())) continue;
const key = getMondayOfWeek(d);
-
const rawType = deal.designType ?? "Unknown";
const label = DESIGN_TYPE_LABELS[rawType] ?? rawType;
-
if (!counts[key]) counts[key] = {};
counts[key][label] = (counts[key][label] || 0) + 1;
}
@@ -286,25 +278,34 @@ function aggregateDesignsByWeek(
const allKeys = fillWeekGaps(Object.keys(counts));
return allKeys.map((isoKey) => {
const entry: Record = { week: formatMonday(isoKey) };
+ let total = 0;
for (const label of DESIGN_TYPE_ORDER) {
- entry[label] = counts[isoKey]?.[label] ?? 0;
+ const v = counts[isoKey]?.[label] ?? 0;
+ entry[label] = v;
+ total += v;
}
+ entry._total = total;
return entry;
});
}
+// Compute total completed count for metrics that support it
+function computeTotalCompleted(
+ metric: string,
+ chartData: Record[],
+ categories: string[],
+): number | null {
+ if (!["bookings", "assessments", "coordination", "design"].includes(metric)) return null;
+ return chartData.reduce((sum, row) => {
+ return sum + categories.reduce((s, cat) => s + ((row[cat] as number) || 0), 0);
+ }, 0);
+}
+
export default function CompletionTrendsChart({
deals,
- isDomnaUser,
- projectCode,
onOpenTable,
}: CompletionTrendsChartProps) {
const [metric, setMetric] = useState(METRICS[0].key);
- const [targets, setTargets] = useState<{ [week: string]: number }>({});
- const [targetInput, setTargetInput] = useState<{
- week: string;
- value: string;
- }>({ week: "", value: "" });
const selectedMetric = METRICS.find((m) => m.key === metric)!;
const isCoordination = metric === "coordination";
@@ -313,18 +314,15 @@ export default function CompletionTrendsChart({
const isDesign = metric === "design";
const isStacked = isCoordination || isAssessments || isLodgement || isDesign;
- // Assessments from external surveyors (no surveyedDate recorded)
+ // External assessments with no date
const undatedAssessments = isAssessments
? deals.filter((d) => {
const o = d.outcome ?? "";
- return (
- (o === "Surveyed" || o === "Surveyed - Pending Upload") &&
- !d.surveyedDate
- );
+ return (o === "Surveyed" || o === "Surveyed - Pending Upload") && !d.surveyedDate;
})
: [];
- // Compute chart data, categories, and colours in one place
+ // Build chart data
let chartData: Record[];
let categories: string[];
let colors: string[];
@@ -350,33 +348,32 @@ export default function CompletionTrendsChart({
chartData = singleData.map((d) => ({
week: d.week,
[selectedMetric.label]: d.value,
- ...(targets[d.week] !== undefined ? { Target: targets[d.week] } : {}),
}));
- categories = [selectedMetric.label, ...(isDomnaUser ? ["Target"] : [])];
- colors = [C.blue, C.brown];
+ categories = [selectedMetric.label];
+ colors = [C.blue];
}
- const handleAddTarget = () => {
- if (!targetInput.week || !targetInput.value) return;
- setTargets((prev) => ({
- ...prev,
- [targetInput.week]: Number(targetInput.value),
- }));
- setTargetInput({ week: "", value: "" });
- };
+ const totalCompleted = computeTotalCompleted(metric, chartData, categories);
return (
-
-
-
+ {/* Header row */}
+
+
+
Trends Over Time
-
- Switch between metrics to see weekly trends.
-
+ {totalCompleted !== null && (
+
+ {totalCompleted}
+
+ {metric === "bookings" ? "booked to date" : "completed to date"}
+
+ ✦
+
+ )}
-
+
{METRICS.find((m) => m.key === metric)?.label}
@@ -392,57 +389,14 @@ export default function CompletionTrendsChart({
- {isDomnaUser && !isStacked && (
-
-
- Add/Edit Targets (visible to Domna users only):
-
-
- setTargetInput((ti) => ({ ...ti, week: e.target.value }))
- }
- className="w-32 text-xs"
- />
-
- setTargetInput((ti) => ({ ...ti, value: e.target.value }))
- }
- className="w-24 text-xs"
- />
-
- Add/Update
-
-
- )}
-
-
+ {/* Undated external assessments — shown above the chart */}
{isAssessments && undatedAssessments.length > 0 && (
-
+
{undatedAssessments.length} {" "}
- assessment{undatedAssessments.length !== 1 ? "s" : ""} from external surveyors have no date recorded
+ external assessment{undatedAssessments.length !== 1 ? "s" : ""} have no date recorded
{onOpenTable && (
@@ -466,11 +420,59 @@ export default function CompletionTrendsChart({
)}
)}
+
+ {/* Chart */}
+
+
+
+
+
+ } cursor={{ fill: "rgba(89,107,224,0.06)" }} />
+ {categories.map((cat, i) => (
+
+ {/* For stacked bars: show total on the top (last) bar only via _total.
+ For non-stacked: show each bar's own value. */}
+ {i === categories.length - 1 && (
+ (v === 0 ? "" : v)}
+ />
+ )}
+
+ ))}
+
+
+
+ {/* Legend for stacked charts */}
{isStacked && (
-
({
+ value: cat,
+ type: "square" as const,
+ color: colors[i],
+ }))}
/>
)}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx
index 04086eac..502bd71d 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx
@@ -15,6 +15,7 @@ import PropertyTable from "./PropertyTable";
import DocumentTable from "./DocumentTable";
import type { HubspotDeal } from "./types";
import PropertyDrawer from "./PropertyDrawer";
+import PropertyDetailDrawer from "./PropertyDetailDrawer";
import AnalyticsView from "./AnalyticsView";
import type {
LiveTrackerProps,
@@ -53,6 +54,9 @@ export default function LiveTracker({
dealname: null,
});
+ // ── Property detail drawer ───────────────────────────────────────────
+ const [detailDeal, setDetailDeal] = useState(null);
+
const handleOpenTable = (
stage: string,
filteredDeals: ClassifiedDeal[],
@@ -166,6 +170,7 @@ export default function LiveTracker({
@@ -302,6 +307,12 @@ export default function LiveTracker({
setDrawerState({ open: false, uprn: null, landlordPropertyId: null, dealname: null })
}
/>
+
+ {/* ── Property detail drawer ─────────────────────────────────────── */}
+
setDetailDeal(null)}
+ />
);
}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx
index 4a61021c..ac29be06 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx
@@ -38,7 +38,7 @@ import {
import { Search, SlidersHorizontal, ChevronLeft, ChevronRight, Download } from "lucide-react";
import { createPropertyTableColumns } from "./PropertyTableColumns";
import { STAGE_ORDER } from "./types";
-import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types";
+import type { ClassifiedDeal, DocStatusMap } from "./types";
// Human-readable labels for toggle dropdown
const COLUMN_LABELS: Record = {
@@ -52,7 +52,6 @@ const COLUMN_LABELS: Record = {
approvedPackage: "Approved Package",
actualMeasuresInstalled: "Installed Measures",
preSapScore: "Pre-SAP",
- postSapScore: "Post-SAP",
lodgementStatus: "Lodgement Status",
designDate: "Design Date",
fullLodgementDate: "Lodgement Date",
@@ -63,6 +62,7 @@ type DocFilter = "all" | "has_docs" | "incomplete" | "none";
interface PropertyTableProps {
data: ClassifiedDeal[];
onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void;
+ onOpenDetail?: (deal: ClassifiedDeal) => void;
showDocuments?: boolean;
docStatusMap?: DocStatusMap;
}
@@ -96,7 +96,7 @@ function escapeCell(value: unknown): string {
: str;
}
-export default function PropertyTable({ data, onOpenDrawer, showDocuments = false, docStatusMap = {} }: PropertyTableProps) {
+export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDocuments = false, docStatusMap = {} }: PropertyTableProps) {
const [globalFilter, setGlobalFilter] = useState("");
const [stageFilter, setStageFilter] = useState("all");
const [docFilter, setDocFilter] = useState("all");
@@ -112,7 +112,6 @@ export default function PropertyTable({ data, onOpenDrawer, showDocuments = fals
approvedPackage: false,
actualMeasuresInstalled: false,
preSapScore: false,
- postSapScore: false,
lodgementStatus: false,
designDate: false,
fullLodgementDate: false,
@@ -137,8 +136,8 @@ export default function PropertyTable({ data, onOpenDrawer, showDocuments = fals
}, [data, stageFilter, docFilter, docStatusMap]);
const columns = useMemo(
- () => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap),
- [onOpenDrawer, showDocuments, docStatusMap]
+ () => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap, onOpenDetail),
+ [onOpenDrawer, showDocuments, docStatusMap, onOpenDetail]
);
const table = useReactTable({
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
index 07d4e53b..95624183 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
@@ -1,7 +1,7 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
-import { ArrowUpDown, FileDown, CheckCircle2, AlertCircle, FileX } from "lucide-react";
+import { ArrowUpDown, CheckCircle2, AlertCircle, FileX } from "lucide-react";
import { STAGE_COLORS } from "./types";
import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types";
@@ -48,6 +48,7 @@ export function createPropertyTableColumns(
onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void,
showDocuments: boolean = false,
docStatusMap: DocStatusMap = {},
+ onOpenDetail?: (deal: ClassifiedDeal) => void,
): ColumnDef[] {
const columns: ColumnDef[] = [
// ── Address ──────────────────────────────────────────────────────────
@@ -57,9 +58,18 @@ export function createPropertyTableColumns(
header: ({ column }) => ,
cell: ({ row }) => (
-
- {row.original.dealname ?? "—"}
-
+ {onOpenDetail ? (
+
onOpenDetail(row.original)}
+ className="text-sm font-medium text-brandblue hover:text-brandmidblue hover:underline underline-offset-2 leading-tight text-left truncate w-full transition-colors"
+ >
+ {row.original.dealname ?? "—"}
+
+ ) : (
+
+ {row.original.dealname ?? "—"}
+
+ )}
),
enableHiding: false,
@@ -221,28 +231,6 @@ export function createPropertyTableColumns(
},
},
- // ── Post-SAP score ───────────────────────────────────────────────────
- {
- accessorKey: "postSapScore",
- id: "postSapScore",
- header: ({ column }) => ,
- cell: ({ row }) => {
- const score = row.original.postSapScore;
- if (!score) return — ;
- const n = Number(score);
- const colour =
- n >= 70
- ? "text-emerald-700 bg-emerald-50"
- : n >= 50
- ? "text-sky-700 bg-sky-50"
- : "text-amber-700 bg-amber-50";
- return (
-
- {score}
-
- );
- },
- },
// ── Lodgement status ─────────────────────────────────────────────────
{
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
index 87c93918..55da8230 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
@@ -7,14 +7,13 @@ import { computeLiveTrackerData } from "./transforms";
import { db } from "@/app/db/db";
import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table";
import { uploadedFiles } from "@/app/db/schema/uploaded_files";
+import { portfolioOrganisation } from "@/app/db/schema/portfolio_organisation";
+import { organisation } from "@/app/db/schema/organisation";
import type { HubspotDeal, DocStatusMap, DocStatus } from "./types";
import { EXPECTED_SURVEY_DOC_TYPES } from "./types";
import type { InferSelectModel } from "drizzle-orm";
-
-// ⚠️ ⚠️ ⚠️ HARDCODED COMPANY ID — temporary for testing only.
-// TODO: derive this from the portfolio slug once the portfolio↔company mapping exists.
-// Do NOT ship this to production without replacing it with the real lookup.
-const HARDCODED_COMPANY_ID = "86970043613";
+import { Card, CardContent } from "@/app/shadcn_components/ui/card";
+import { Building2 } from "lucide-react";
type DbDeal = InferSelectModel;
@@ -37,25 +36,25 @@ function mapDbRowToHubspotDeal(row: DbDeal): HubspotDeal {
designStatus: row.designStatus,
pashubLink: row.pashubLink,
sharepointLink: row.sharepointLink,
- dampMouldFlag: row.dampmouldGrowth, // DB: dampmouldGrowth
- preSapScore: row.preSap, // DB: preSap
+ dampMouldFlag: row.dampmouldGrowth,
+ preSapScore: row.preSap,
coordinator: row.coordinator,
- ioeV1Date: row.mtpCompletionDate, // DB: mtpCompletionDate
- ioeV2Date: row.mtpReModelCompletionDate, // DB: mtpReModelCompletionDate
- ioeV3Date: row.ioeV3CompletionDate, // DB: ioeV3CompletionDate
+ ioeV1Date: row.mtpCompletionDate,
+ ioeV2Date: row.mtpReModelCompletionDate,
+ ioeV3Date: row.ioeV3CompletionDate,
proposedMeasures: row.proposedMeasures,
approvedPackage: row.approvedPackage,
designer: row.designer,
- designDate: row.designCompletionDate, // DB: designCompletionDate
+ designDate: row.designCompletionDate,
actualMeasuresInstalled: row.actualMeasuresInstalled,
installer: row.installer,
installerHandover: row.installerHandover,
lodgementStatus: row.lodgementStatus,
measuresLodgementDate: row.measuresLodgementDate,
- fullLodgementDate: row.lodgementDate, // DB: lodgementDate
+ fullLodgementDate: row.lodgementDate,
confirmedSurveyDate: row.confirmedSurveyDate,
- surveyedDate: row.SurveyedDate, // DB: surveyed_date
- designType: row.dealType, // DB: design_type
+ surveyedDate: row.SurveyedDate,
+ designType: row.dealType,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
@@ -64,26 +63,64 @@ function mapDbRowToHubspotDeal(row: DbDeal): HubspotDeal {
export default async function LiveReportingPage(props: {
params: Promise<{ slug: string }>;
}) {
- const { slug: _portfolioId } = await props.params;
+ const { slug: portfolioId } = await props.params;
const user = await getServerSession(AuthOptions);
if (!user?.user) {
- console.error("User not found");
redirect("/");
}
- // ⚠️ Using HARDCODED_COMPANY_ID — see constant above before deploying
+ // Look up the linked organisation for this portfolio
+ const link = await db
+ .select({ hubspotCompanyId: organisation.hubspotCompanyId })
+ .from(portfolioOrganisation)
+ .innerJoin(organisation, eq(portfolioOrganisation.organisationId, organisation.id))
+ .where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId)))
+ .limit(1);
+
+ const pageHeader = (
+
+
+
+ {`Check in on your projects' progress with real-time data updates.`}
+
+
+
+ );
+
+ if (!link.length || !link[0].hubspotCompanyId) {
+ return (
+
+ {pageHeader}
+
+
+
+
+
+
+
No organisation linked
+
+ A Domna administrator needs to connect this portfolio to an organisation in{" "}
+ Portfolio Settings before live tracking data can be displayed.
+
+
+
+
+
+ );
+ }
+
+ const companyId = link[0].hubspotCompanyId;
+
const rawDeals = await db
.select()
.from(hubspotDealData)
- .where(eq(hubspotDealData.companyId, HARDCODED_COMPANY_ID));
-
- console.log("Fetched deals from DB:", rawDeals.length);
+ .where(eq(hubspotDealData.companyId, companyId));
const deals = rawDeals.map(mapDbRowToHubspotDeal);
const trackerData = computeLiveTrackerData(deals);
- // ── Fetch survey document status for all properties ─────────────────
+ // Fetch survey document status for all properties
const uprnList = deals
.map((d) => d.uprn)
.filter((u): u is string => !!u)
@@ -120,16 +157,7 @@ export default async function LiveReportingPage(props: {
return (
-
-
-
- {`Check in on your projects' progress with real-time data updates.`}
-
-
-
-
+ {pageHeader}
);