From e52ab73e9fdc08b193cd6b4191d88fc3d661c0c4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 12 May 2026 13:00:09 +0000 Subject: [PATCH] Remove unused WorkPhaseStats and consolidate deal mapping into shared module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../your-projects/live/[dealId]/page.tsx | 193 ++---------------- .../your-projects/live/dealQuery.ts | 73 +++++++ .../(portfolio)/your-projects/live/page.tsx | 75 +------ .../your-projects/live/transforms.ts | 106 +--------- .../(portfolio)/your-projects/live/types.ts | 17 -- 5 files changed, 90 insertions(+), 374 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.tsx index 79a50c6..08d2561 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.tsx @@ -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; - 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 ( diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts new file mode 100644 index 0000000..ed215a0 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts @@ -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; + 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, + }; +} 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 840a2d3..87a9a7a 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -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; - 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 }>; }) { diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts index 9e45e67..0223e76 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -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), }; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts index da7d78e..e1be10c 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -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[]; };