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 48d8631..560aefe 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
@@ -11,12 +11,27 @@ import { hubspotUsers } from "@/app/db/schema/crm/hubspot_user_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 { portfolioCapabilities, portfolioUsers } from "@/app/db/schema/portfolio";
+import {
+ portfolioCapabilities,
+ portfolioUsers,
+} from "@/app/db/schema/portfolio";
import { dealMeasureApprovals } from "@/app/db/schema/approvals";
import { propertyRemovalRequests } from "@/app/db/schema/removal_requests";
import { user as userTable } from "@/app/db/schema/users";
-import type { HubspotDeal, DocStatusMap, DocStatus, MeasureDocProgress, PortfolioCapabilityType, ApprovalsByDeal, RemovalStatusByDeal, EffectiveRemovalState } from "./types";
-import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES, SURVEY_ALL_DOC_TYPES } from "./types";
+import type {
+ HubspotDeal,
+ DocStatusMap,
+ DocStatus,
+ MeasureDocProgress,
+ PortfolioCapabilityType,
+ ApprovalsByDeal,
+ RemovalStatusByDeal,
+ EffectiveRemovalState,
+} from "./types";
+import {
+ EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES,
+ SURVEY_ALL_DOC_TYPES,
+} from "./types";
import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements";
import type { InferSelectModel } from "drizzle-orm";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
@@ -105,12 +120,17 @@ export default async function LiveReportingPage(props: {
const link = await db
.select({ hubspotCompanyId: organisation.hubspotCompanyId })
.from(portfolioOrganisation)
- .innerJoin(organisation, eq(portfolioOrganisation.organisationId, organisation.id))
+ .innerJoin(
+ organisation,
+ eq(portfolioOrganisation.organisationId, organisation.id),
+ )
.where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId)));
const pageHeader = (
-
+
{`Check in on your projects' progress with real-time data updates.`}
@@ -118,11 +138,13 @@ export default async function LiveReportingPage(props: {
);
- const companyIds = link.map((l) => l.hubspotCompanyId).filter((id): id is string => !!id);
+ const companyIds = link
+ .map((l) => l.hubspotCompanyId)
+ .filter((id): id is string => !!id);
if (companyIds.length === 0) {
return (
-
+
{pageHeader}
@@ -130,10 +152,13 @@ export default async function LiveReportingPage(props: {
-
No organisation linked
+
+ No organisation linked
+
- A Domna administrator needs to connect this portfolio to an organisation in{" "}
- Portfolio Settings before live tracking data can be displayed.
+ A Domna administrator needs to connect this portfolio to an
+ organisation in Portfolio Settings before live
+ tracking data can be displayed.
@@ -145,12 +170,22 @@ export default async function LiveReportingPage(props: {
const rawDeals = await db
.select({
deal: hubspotDealData,
- coordinator: sql
`CASE WHEN ${hubspotDealData.coordinator} IS NULL THEN NULL ELSE COALESCE(${coordinatorUser.firstName} || ' ' || ${coordinatorUser.lastName}, 'Domna Coordinator') END`,
- designer: sql`CASE WHEN ${hubspotDealData.designer} IS NULL THEN NULL ELSE COALESCE(${designerUser.firstName} || ' ' || ${designerUser.lastName}, 'Domna Designer') END`,
+ coordinator: sql<
+ string | null
+ >`CASE WHEN ${hubspotDealData.coordinator} IS NULL THEN NULL ELSE COALESCE(${coordinatorUser.firstName} || ' ' || ${coordinatorUser.lastName}, 'Domna Coordinator') END`,
+ designer: sql<
+ string | null
+ >`CASE WHEN ${hubspotDealData.designer} IS NULL THEN NULL ELSE COALESCE(${designerUser.firstName} || ' ' || ${designerUser.lastName}, 'Domna Designer') END`,
})
.from(hubspotDealData)
- .leftJoin(coordinatorUser, eq(hubspotDealData.coordinator, coordinatorUser.hubspotOwnerId))
- .leftJoin(designerUser, eq(hubspotDealData.designer, designerUser.hubspotOwnerId))
+ .leftJoin(
+ coordinatorUser,
+ eq(hubspotDealData.coordinator, coordinatorUser.hubspotOwnerId),
+ )
+ .leftJoin(
+ designerUser,
+ eq(hubspotDealData.designer, designerUser.hubspotOwnerId),
+ )
.where(inArray(hubspotDealData.companyId, companyIds));
const deals = rawDeals.map(mapDbRowToHubspotDeal);
@@ -178,7 +213,10 @@ export default async function LiveReportingPage(props: {
);
userCapability = capRows
.map((r) => r.capability)
- .filter((c): c is "approver" | "contractor" => c === "approver" || c === "contractor");
+ .filter(
+ (c): c is "approver" | "contractor" =>
+ c === "approver" || c === "contractor",
+ );
}
}
@@ -247,7 +285,8 @@ export default async function LiveReportingPage(props: {
seenDeals.add(row.hubspotDealId);
let state: EffectiveRemovalState = "none";
if (row.status === "pending") {
- state = row.type === "re_addition" ? "pending_re_addition" : "pending_removal";
+ state =
+ row.type === "re_addition" ? "pending_re_addition" : "pending_removal";
} else if (row.type === "removal" && row.status === "approved") {
state = "removed";
} else if (row.type === "re_addition" && row.status === "declined") {
@@ -259,7 +298,10 @@ export default async function LiveReportingPage(props: {
// Fetch document status for all deals — two-phase strategy:
// Phase 1: query by dealId (reliable even when UPRN is missing from hubspot_deal_data)
// Phase 2: UPRN fallback only for deals that returned no results in phase 1
- const docsByDealId = new Map>();
+ const docsByDealId = new Map<
+ string,
+ Array<{ fileType: string; measureName: string | null }>
+ >();
if (dealIds.length > 0) {
const phase1Rows = await db
@@ -273,8 +315,11 @@ export default async function LiveReportingPage(props: {
for (const row of phase1Rows) {
if (!row.hubsotDealId || row.fileType === null) continue;
- if (!docsByDealId.has(row.hubsotDealId)) docsByDealId.set(row.hubsotDealId, []);
- docsByDealId.get(row.hubsotDealId)!.push({ fileType: row.fileType, measureName: row.measureName });
+ if (!docsByDealId.has(row.hubsotDealId))
+ docsByDealId.set(row.hubsotDealId, []);
+ docsByDealId
+ .get(row.hubsotDealId)!
+ .push({ fileType: row.fileType, measureName: row.measureName });
}
}
@@ -283,7 +328,13 @@ export default async function LiveReportingPage(props: {
const fallbackUprns = dealsWithoutDocs
.map((d) => d.uprn)
.filter((u): u is string => !!u)
- .map((u) => { try { return BigInt(u); } catch { return null; } })
+ .map((u) => {
+ try {
+ return BigInt(u);
+ } catch {
+ return null;
+ }
+ })
.filter((u): u is bigint => u !== null);
if (fallbackUprns.length > 0) {
@@ -301,7 +352,11 @@ export default async function LiveReportingPage(props: {
dealsWithoutDocs
.filter((d) => d.uprn)
.map((d) => {
- try { return [String(BigInt(d.uprn!)), d.dealId] as [string, string]; } catch { return null; }
+ try {
+ return [String(BigInt(d.uprn!)), d.dealId] as [string, string];
+ } catch {
+ return null;
+ }
})
.filter((e): e is [string, string] => e !== null),
);
@@ -311,7 +366,9 @@ export default async function LiveReportingPage(props: {
const dealId = uprnToDealId.get(String(row.uprn));
if (!dealId) continue;
if (!docsByDealId.has(dealId)) docsByDealId.set(dealId, []);
- docsByDealId.get(dealId)!.push({ fileType: row.fileType, measureName: row.measureName });
+ docsByDealId
+ .get(dealId)!
+ .push({ fileType: row.fileType, measureName: row.measureName });
}
}
@@ -319,9 +376,13 @@ export default async function LiveReportingPage(props: {
const measuresByDealId = new Map();
for (const deal of deals) {
const approved = approvalsByDeal[deal.dealId] ?? [];
- const measures = approved.length > 0
- ? approved
- : (deal.proposedMeasures ?? "").split(",").map((m: string) => m.trim()).filter(Boolean);
+ const measures =
+ approved.length > 0
+ ? approved
+ : (deal.proposedMeasures ?? "")
+ .split(",")
+ .map((m: string) => m.trim())
+ .filter(Boolean);
measuresByDealId.set(deal.dealId, measures);
}
@@ -330,26 +391,32 @@ export default async function LiveReportingPage(props: {
for (const [dealId, docs] of docsByDealId) {
const surveyDocs = docs.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.fileType));
- const installDocs = docs.filter((d) => !SURVEY_ALL_DOC_TYPES.has(d.fileType));
+ const installDocs = docs.filter(
+ (d) => !SURVEY_ALL_DOC_TYPES.has(d.fileType),
+ );
const surveyTypeSet = new Set(surveyDocs.map((d) => d.fileType));
const measures = measuresByDealId.get(dealId) ?? [];
// Compute per-measure document progress against the requirements matrix
- const measureProgress: MeasureDocProgress[] = measures.map((measureName) => {
- const required = getRequiredDocs(measureName);
- const docsForMeasure = installDocs.filter((d) => d.measureName === measureName);
- const uploadedTypeSet = new Set(docsForMeasure.map((d) => d.fileType));
- const uploaded = required.filter((r) => uploadedTypeSet.has(r));
- return {
- measureName,
- required,
- uploaded,
- isComplete: uploaded.length === required.length,
- uploadedCount: uploaded.length,
- requiredCount: required.length,
- };
- });
+ const measureProgress: MeasureDocProgress[] = measures.map(
+ (measureName) => {
+ const required = getRequiredDocs(measureName);
+ const docsForMeasure = installDocs.filter(
+ (d) => d.measureName === measureName,
+ );
+ const uploadedTypeSet = new Set(docsForMeasure.map((d) => d.fileType));
+ const uploaded = required.filter((r) => uploadedTypeSet.has(r));
+ return {
+ measureName,
+ required,
+ uploaded,
+ isComplete: uploaded.length === required.length,
+ uploadedCount: uploaded.length,
+ requiredCount: required.length,
+ };
+ },
+ );
let installStatus: DocStatus["installStatus"] = "none";
if (installDocs.length > 0) {
@@ -367,7 +434,9 @@ export default async function LiveReportingPage(props: {
docStatusMap[dealId] = {
presentSurveyTypes: Array.from(surveyTypeSet),
hasSurveyDocs: surveyDocs.length > 0,
- isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) => surveyTypeSet.has(t)),
+ isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) =>
+ surveyTypeSet.has(t),
+ ),
hasInstallDocs: installDocs.length > 0,
installStatus,
measureProgress,
@@ -375,7 +444,7 @@ export default async function LiveReportingPage(props: {
}
return (
-