increased width of layout to 8xl

This commit is contained in:
Khalim Conn-Kowlessar 2026-05-08 17:27:52 +00:00
parent 7bcac4f65a
commit 7b6763934c

View file

@ -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 = (
<div className="mb-6">
<header className="text-3xl font-semibold text-brandblue">Live Projects</header>
<header className="text-3xl font-semibold text-brandblue">
Live Projects
</header>
<p className="text-sm text-gray-500">
{`Check in on your projects' progress with real-time data updates.`}
</p>
@ -118,11 +138,13 @@ export default async function LiveReportingPage(props: {
</div>
);
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 (
<div className="max-w-7xl mx-auto px-6 pb-10 space-y-4">
<div className="max-w-8xl mx-auto px-6 pb-10 space-y-4">
{pageHeader}
<Card className="border border-brandblue/10 shadow-sm">
<CardContent className="flex flex-col items-center justify-center py-16 text-center gap-4">
@ -130,10 +152,13 @@ export default async function LiveReportingPage(props: {
<Building2 className="h-8 w-8 text-brandblue/50" />
</div>
<div>
<p className="text-base font-semibold text-gray-700">No organisation linked</p>
<p className="text-base font-semibold text-gray-700">
No organisation linked
</p>
<p className="text-sm text-gray-400 mt-1 max-w-sm">
A Domna administrator needs to connect this portfolio to an organisation in{" "}
<strong>Portfolio Settings</strong> before live tracking data can be displayed.
A Domna administrator needs to connect this portfolio to an
organisation in <strong>Portfolio Settings</strong> before live
tracking data can be displayed.
</p>
</div>
</CardContent>
@ -145,12 +170,22 @@ export default async function LiveReportingPage(props: {
const rawDeals = await db
.select({
deal: hubspotDealData,
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`,
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<string, Array<{ fileType: string; measureName: string | null }>>();
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<string, string[]>();
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 (
<div className="max-w-7xl mx-auto px-6 pb-10 space-y-4">
<div className="max-w-8xl mx-auto px-6 pb-10 space-y-4">
{pageHeader}
<LiveTracker
{...trackerData}