mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
increased width of layout to 8xl
This commit is contained in:
parent
7bcac4f65a
commit
7b6763934c
1 changed files with 111 additions and 42 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue