Remove unused WorkPhaseStats and consolidate deal mapping into shared module

Delete WorkPhaseStats type and its four computation blocks (coordination,
design, install, lodgement) from transforms.ts and types.ts — the computed
values were never read by any component.

Extract mapDbRowToHubspotDeal, DealRow, and the coordinator/designer aliases
into a new dealQuery.ts module, eliminating the verbatim duplication between
the live tracker page and the deal detail page.

Replace the inline doc status computation in [dealId]/page.tsx with calls
to the existing fetchDocsByDealId and computeDocStatusMap from docStatus.ts,
so both paths now share a single implementation.
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-12 13:00:09 +00:00
parent 281f15c11e
commit e52ab73e9f
5 changed files with 90 additions and 374 deletions

View file

@ -1,104 +1,23 @@
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { redirect, notFound } from "next/navigation";
import { eq, inArray, and, desc } from "drizzle-orm";
import { eq, inArray, and, desc, sql } from "drizzle-orm";
import { db } from "@/app/db/db";
import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table";
import { alias } from "drizzle-orm/pg-core";
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 { dealMeasureApprovals } from "@/app/db/schema/approvals";
import { propertyRemovalRequests } from "@/app/db/schema/removal_requests";
import { user as userTable } from "@/app/db/schema/users";
import { sql } from "drizzle-orm";
import type {
HubspotDeal,
DocStatus,
MeasureDocProgress,
PortfolioCapabilityType,
EffectiveRemovalState,
} from "../types";
import {
EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES,
SURVEY_ALL_DOC_TYPES,
} from "../types";
import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements";
import type { DocStatus, PortfolioCapabilityType, EffectiveRemovalState } from "../types";
import { classifyDeals } from "../transforms";
import type { InferSelectModel } from "drizzle-orm";
import { fetchDocsByDealId, computeDocStatusMap } from "../docStatus";
import { coordinatorUser, designerUser, mapDbRowToHubspotDeal } from "../dealQuery";
import type { DealRow } from "../dealQuery";
import DealPage from "./DealPage";
import Link from "next/link";
const coordinatorUser = alias(hubspotUsers, "coordinator_user");
const designerUser = alias(hubspotUsers, "designer_user");
type DealRow = {
deal: InferSelectModel<typeof hubspotDealData>;
coordinator: string | null;
designer: string | null;
};
function mapDbRowToHubspotDeal(row: DealRow): HubspotDeal {
const d = row.deal;
return {
id: d.id,
dealId: d.dealId,
dealname: d.dealname,
dealstage: d.dealstage,
companyId: d.companyId,
projectCode: d.projectCode,
landlordPropertyId: d.landlordPropertyId,
uprn: d.uprn,
outcome: d.outcome,
outcomeNotes: d.outcomeNotes,
majorConditionIssueDescription: d.majorConditionIssueDescription,
majorConditionIssuePhotos: d.majorConditionIssuePhotos,
majorConditionIssuePhotosS3: d.majorConditionIssuePhotosS3,
coordinationStatus: d.coordinationStatus,
designStatus: d.designStatus,
pashubLink: d.pashubLink,
sharepointLink: d.sharepointLink,
dampMouldFlag: d.dampmouldGrowth,
dampMouldAndRepairComments: d.damnpMouldAndRepairComments,
preSapScore: d.preSap,
coordinator: row.coordinator,
ioeV1Date: d.mtpCompletionDate,
ioeV2Date: d.mtpReModelCompletionDate,
ioeV3Date: d.ioeV3CompletionDate,
proposedMeasures: d.proposedMeasures,
approvedPackage: d.approvedPackage,
designer: row.designer,
designDate: d.designCompletionDate,
actualMeasuresInstalled: d.actualMeasuresInstalled,
installer: d.installer,
installerHandover: d.installerHandover,
lodgementStatus: d.lodgementStatus,
measuresLodgementDate: d.measuresLodgementDate,
fullLodgementDate: d.lodgementDate,
confirmedSurveyDate: d.confirmedSurveyDate,
confirmedSurveyTime: d.confirmedSurveyTime,
surveyedDate: d.surveyedDate,
designType: d.dealType,
eiScore: d.eiScore,
eiScorePotential: d.eiScorePotential,
epcSapScore: d.epcSapScore,
epcSapScorePotential: d.epcSapScorePotential,
surveyType: d.surveyType,
measuresForPibiOrdered: d.measuresForPibiOrdered,
pibiOrderDate: d.pibiOrderDate,
pibiCompletedDate: d.pibiCompletedDate,
propertyHaltedDate: d.propertyHaltedDate,
propertyHaltedReason: d.propertyHaltedReason,
technicalApprovedMeasuresForInstall: d.technicalApprovedMeasuresForInstall,
domnaSurveyType: d.domnaSurveyType,
domnaSurveyDate: d.domnaSurveyDate,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
};
}
export default async function DealDetailPage(props: {
params: Promise<{ slug: string; dealId: string }>;
}) {
@ -240,99 +159,15 @@ export default async function DealDetailPage(props: {
}
}
// Doc status — same two-phase strategy as live tracker
const docFiles: Array<{ fileType: string; measureName: string | null }> = [];
const phase1Rows = await db
.select({
hubsotDealId: uploadedFiles.hubsotDealId,
fileType: uploadedFiles.fileType,
measureName: uploadedFiles.measureName,
})
.from(uploadedFiles)
.where(eq(uploadedFiles.hubsotDealId, dealId));
for (const row of phase1Rows) {
if (row.fileType !== null) {
docFiles.push({ fileType: row.fileType, measureName: row.measureName });
}
}
if (docFiles.length === 0 && deal.uprn) {
try {
const uprnBig = BigInt(deal.uprn);
const phase2Rows = await db
.select({
fileType: uploadedFiles.fileType,
measureName: uploadedFiles.measureName,
})
.from(uploadedFiles)
.where(eq(uploadedFiles.uprn, uprnBig));
for (const row of phase2Rows) {
if (row.fileType !== null) {
docFiles.push({
fileType: row.fileType,
measureName: row.measureName,
});
}
}
} catch {
// Invalid UPRN — skip phase 2
}
}
const measures =
approvedMeasures.length > 0
? approvedMeasures
: (deal.proposedMeasures ?? "")
.split(",")
.map((m: string) => m.trim())
.filter(Boolean);
const surveyDocs = docFiles.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.fileType));
const installDocs = docFiles.filter((d) => !SURVEY_ALL_DOC_TYPES.has(d.fileType));
const surveyTypeSet = new Set(surveyDocs.map((d) => d.fileType));
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) {
if (measures.length === 0) {
installStatus = "hasDocs";
} else {
installStatus = measureProgress.every((m) => m.isComplete)
? "all"
: measureProgress.some((m) => m.uploadedCount > 0)
? "partial"
: "none";
}
}
const docStatus: DocStatus = {
presentSurveyTypes: Array.from(surveyTypeSet),
hasSurveyDocs: surveyDocs.length > 0,
isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) =>
surveyTypeSet.has(t),
),
hasInstallDocs: installDocs.length > 0,
installStatus,
measureProgress,
const docsByDealId = await fetchDocsByDealId([hubspotDeal], [dealId]);
const docStatusMap = computeDocStatusMap([hubspotDeal], docsByDealId, { [dealId]: approvedMeasures });
const docStatus: DocStatus = docStatusMap[dealId] ?? {
presentSurveyTypes: [],
hasSurveyDocs: false,
isSurveyComplete: false,
hasInstallDocs: false,
installStatus: "none",
measureProgress: [],
};
return (

View file

@ -0,0 +1,73 @@
import { alias } from "drizzle-orm/pg-core";
import { hubspotUsers } from "@/app/db/schema/crm/hubspot_user_table";
import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table";
import type { HubspotDeal } from "./types";
import type { InferSelectModel } from "drizzle-orm";
export const coordinatorUser = alias(hubspotUsers, "coordinator_user");
export const designerUser = alias(hubspotUsers, "designer_user");
export type DealRow = {
deal: InferSelectModel<typeof hubspotDealData>;
coordinator: string | null;
designer: string | null;
};
export function mapDbRowToHubspotDeal(row: DealRow): HubspotDeal {
const d = row.deal;
return {
id: d.id,
dealId: d.dealId,
dealname: d.dealname,
dealstage: d.dealstage,
companyId: d.companyId,
projectCode: d.projectCode,
landlordPropertyId: d.landlordPropertyId,
uprn: d.uprn,
outcome: d.outcome,
outcomeNotes: d.outcomeNotes,
majorConditionIssueDescription: d.majorConditionIssueDescription,
majorConditionIssuePhotos: d.majorConditionIssuePhotos,
majorConditionIssuePhotosS3: d.majorConditionIssuePhotosS3,
coordinationStatus: d.coordinationStatus,
designStatus: d.designStatus,
pashubLink: d.pashubLink,
sharepointLink: d.sharepointLink,
dampMouldFlag: d.dampmouldGrowth,
dampMouldAndRepairComments: d.damnpMouldAndRepairComments,
preSapScore: d.preSap,
coordinator: row.coordinator,
ioeV1Date: d.mtpCompletionDate,
ioeV2Date: d.mtpReModelCompletionDate,
ioeV3Date: d.ioeV3CompletionDate,
proposedMeasures: d.proposedMeasures,
approvedPackage: d.approvedPackage,
designer: row.designer,
designDate: d.designCompletionDate,
actualMeasuresInstalled: d.actualMeasuresInstalled,
installer: d.installer,
installerHandover: d.installerHandover,
lodgementStatus: d.lodgementStatus,
measuresLodgementDate: d.measuresLodgementDate,
fullLodgementDate: d.lodgementDate,
confirmedSurveyDate: d.confirmedSurveyDate,
confirmedSurveyTime: d.confirmedSurveyTime,
surveyedDate: d.surveyedDate,
designType: d.dealType,
eiScore: d.eiScore,
eiScorePotential: d.eiScorePotential,
epcSapScore: d.epcSapScore,
epcSapScorePotential: d.epcSapScorePotential,
surveyType: d.surveyType,
measuresForPibiOrdered: d.measuresForPibiOrdered,
pibiOrderDate: d.pibiOrderDate,
pibiCompletedDate: d.pibiCompletedDate,
propertyHaltedDate: d.propertyHaltedDate,
propertyHaltedReason: d.propertyHaltedReason,
technicalApprovedMeasuresForInstall: d.technicalApprovedMeasuresForInstall,
domnaSurveyType: d.domnaSurveyType,
domnaSurveyDate: d.domnaSurveyDate,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
};
}

View file

@ -7,9 +7,9 @@ import { computeLiveTrackerData } from "./transforms";
import { fetchDocsByDealId, computeDocStatusMap } from "./docStatus";
import { db } from "@/app/db/db";
import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table";
import { alias } from "drizzle-orm/pg-core";
import { hubspotUsers } from "@/app/db/schema/crm/hubspot_user_table";
import { portfolioOrganisation } from "@/app/db/schema/portfolio_organisation";
import { coordinatorUser, designerUser, mapDbRowToHubspotDeal } from "./dealQuery";
import type { DealRow } from "./dealQuery";
import { organisation } from "@/app/db/schema/organisation";
import {
portfolioCapabilities,
@ -20,86 +20,15 @@ import { userDefinedDealMeasures } from "@/app/db/schema/user_defined_deal_measu
import { propertyRemovalRequests } from "@/app/db/schema/removal_requests";
import { user as userTable } from "@/app/db/schema/users";
import type {
HubspotDeal,
PortfolioCapabilityType,
ApprovalsByDeal,
InstructedMeasuresByDeal,
RemovalStatusByDeal,
EffectiveRemovalState,
} from "./types";
import type { InferSelectModel } from "drizzle-orm";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { Building2 } from "lucide-react";
const coordinatorUser = alias(hubspotUsers, "coordinator_user");
const designerUser = alias(hubspotUsers, "designer_user");
type DealRow = {
deal: InferSelectModel<typeof hubspotDealData>;
coordinator: string | null;
designer: string | null;
};
function mapDbRowToHubspotDeal(row: DealRow): HubspotDeal {
const d = row.deal;
return {
id: d.id,
dealId: d.dealId,
dealname: d.dealname,
dealstage: d.dealstage,
companyId: d.companyId,
projectCode: d.projectCode,
landlordPropertyId: d.landlordPropertyId,
uprn: d.uprn,
outcome: d.outcome,
outcomeNotes: d.outcomeNotes,
majorConditionIssueDescription: d.majorConditionIssueDescription,
majorConditionIssuePhotos: d.majorConditionIssuePhotos,
majorConditionIssuePhotosS3: d.majorConditionIssuePhotosS3,
coordinationStatus: d.coordinationStatus,
designStatus: d.designStatus,
pashubLink: d.pashubLink,
sharepointLink: d.sharepointLink,
dampMouldFlag: d.dampmouldGrowth,
dampMouldAndRepairComments: d.damnpMouldAndRepairComments,
preSapScore: d.preSap,
coordinator: row.coordinator,
ioeV1Date: d.mtpCompletionDate,
ioeV2Date: d.mtpReModelCompletionDate,
ioeV3Date: d.ioeV3CompletionDate,
proposedMeasures: d.proposedMeasures,
approvedPackage: d.approvedPackage,
designer: row.designer,
designDate: d.designCompletionDate,
actualMeasuresInstalled: d.actualMeasuresInstalled,
installer: d.installer,
installerHandover: d.installerHandover,
lodgementStatus: d.lodgementStatus,
measuresLodgementDate: d.measuresLodgementDate,
fullLodgementDate: d.lodgementDate,
confirmedSurveyDate: d.confirmedSurveyDate,
confirmedSurveyTime: d.confirmedSurveyTime,
surveyedDate: d.surveyedDate,
designType: d.dealType,
eiScore: d.eiScore,
eiScorePotential: d.eiScorePotential,
epcSapScore: d.epcSapScore,
epcSapScorePotential: d.epcSapScorePotential,
// New per-deal workflow fields
surveyType: d.surveyType,
measuresForPibiOrdered: d.measuresForPibiOrdered,
pibiOrderDate: d.pibiOrderDate,
pibiCompletedDate: d.pibiCompletedDate,
propertyHaltedDate: d.propertyHaltedDate,
propertyHaltedReason: d.propertyHaltedReason,
technicalApprovedMeasuresForInstall: d.technicalApprovedMeasuresForInstall,
domnaSurveyType: d.domnaSurveyType,
domnaSurveyDate: d.domnaSurveyDate,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
};
}
export default async function LiveReportingPage(props: {
params: Promise<{ slug: string }>;
}) {

View file

@ -11,7 +11,7 @@ import type {
ProjectData,
OutcomeSlice,
LiveTrackerProps,
WorkPhaseStats,
DampMouldRiskData,
FunnelStage,
} from "./types";
@ -224,106 +224,6 @@ export function computeProjectProgress(
const totalDeals = deals.length;
// Coordination phase:
// completed = Design in Progress + Installation in Progress + Installation Complete + At Lodgement + At Post Survey + Project Complete
// in progress = Coordination in Progress
const coordCompletedDeals = deals.filter((d) =>
[
"Design in Progress",
"Installation in Progress",
"Installation Complete",
"At Lodgement",
"At Post Survey",
"Project Complete",
].includes(d.displayStage)
);
const coordInProgressDeals = deals.filter(
(d) => d.displayStage === "Coordination in Progress"
);
const coordination: WorkPhaseStats = {
completedDeals: coordCompletedDeals,
inProgressDeals: coordInProgressDeals,
completedCount: coordCompletedDeals.length,
inProgressCount: coordInProgressDeals.length,
completedPercentage:
totalDeals > 0 ? (coordCompletedDeals.length / totalDeals) * 100 : 0,
inProgressPercentage:
totalDeals > 0 ? (coordInProgressDeals.length / totalDeals) * 100 : 0,
total: totalDeals,
};
// Design phase:
// completed = Installation in Progress + Installation Complete + At Lodgement + At Post Survey + Project Complete
// in progress = Design in Progress
const designCompletedDeals = deals.filter((d) =>
[
"Installation in Progress",
"Installation Complete",
"At Lodgement",
"At Post Survey",
"Project Complete",
].includes(d.displayStage)
);
const designInProgressDeals = deals.filter(
(d) => d.displayStage === "Design in Progress"
);
const design: WorkPhaseStats = {
completedDeals: designCompletedDeals,
inProgressDeals: designInProgressDeals,
completedCount: designCompletedDeals.length,
inProgressCount: designInProgressDeals.length,
completedPercentage:
totalDeals > 0 ? (designCompletedDeals.length / totalDeals) * 100 : 0,
inProgressPercentage:
totalDeals > 0 ? (designInProgressDeals.length / totalDeals) * 100 : 0,
total: totalDeals,
};
// Install phase:
// completed = At Lodgement + At Post Survey + Project Complete
// in progress = Installation Complete
const installCompletedDeals = deals.filter((d) =>
["At Lodgement", "At Post Survey", "Project Complete"].includes(d.displayStage)
);
const installInProgressDeals = deals.filter(
(d) => d.displayStage === "Installation Complete"
);
const install: WorkPhaseStats = {
completedDeals: installCompletedDeals,
inProgressDeals: installInProgressDeals,
completedCount: installCompletedDeals.length,
inProgressCount: installInProgressDeals.length,
completedPercentage:
totalDeals > 0 ? (installCompletedDeals.length / totalDeals) * 100 : 0,
inProgressPercentage:
totalDeals > 0 ? (installInProgressDeals.length / totalDeals) * 100 : 0,
total: totalDeals,
};
// Lodgement phase:
// completed = At Post Survey + Project Complete
// in progress = At Lodgement
const lodgementInProgressDeals = deals.filter(
(d) => d.displayStage === "At Lodgement"
);
const lodgement: WorkPhaseStats = {
completedDeals,
inProgressDeals: lodgementInProgressDeals,
completedCount,
inProgressCount: lodgementInProgressDeals.length,
completedPercentage:
totalDeals > 0 ? (completedCount / totalDeals) * 100 : 0,
inProgressPercentage:
totalDeals > 0
? (lodgementInProgressDeals.length / totalDeals) * 100
: 0,
total: totalDeals,
};
return {
stageProgress,
queriesDeals,
@ -332,10 +232,6 @@ export function computeProjectProgress(
completedPercentage,
nonQueryTotal,
totalDeals,
coordination,
design,
install,
lodgement,
dampMouldRisk: computeDampMouldRisk(deals),
funnelStages: computeFunnelStages(deals),
};

View file

@ -104,19 +104,6 @@ export type StageProgressItem = {
deals: ClassifiedDeal[];
};
// -----------------------------------------------------------------------
// Coordination/Design/Install/Lodgement summary card data
// -----------------------------------------------------------------------
export type WorkPhaseStats = {
completedDeals: ClassifiedDeal[];
inProgressDeals: ClassifiedDeal[];
completedCount: number;
inProgressCount: number;
completedPercentage: number; // out of ALL deals in project
inProgressPercentage: number;
total: number;
};
// -----------------------------------------------------------------------
// Damp & mould risk comparison (survey-stage vs coordination-stage flags)
// -----------------------------------------------------------------------
@ -151,10 +138,6 @@ export type ProjectProgressData = {
completedPercentage: number; // out of non-query total
nonQueryTotal: number;
totalDeals: number;
coordination: WorkPhaseStats;
design: WorkPhaseStats;
install: WorkPhaseStats;
lodgement: WorkPhaseStats;
dampMouldRisk: DampMouldRiskData;
funnelStages: FunnelStage[];
};