diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx index 6479501..b41971b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -4,10 +4,10 @@ import { useState } from "react"; import { motion } from "framer-motion"; import { Home, AlertTriangle, ToggleLeft, ToggleRight } from "lucide-react"; import { Card, CardContent } from "@/app/shadcn_components/ui/card"; -import SurveyedResultsPieChart from "./SurveyedResultsPieChart"; import DampMouldRiskPanel from "./DampMouldRiskPanel"; import CompletionTrendsChart from "./CompletionTrendsChart"; import SurveyIssuesPanel from "./SurveyIssuesPanel"; +import ExcludedFromPipelinePanel from "./ExcludedFromPipelinePanel"; import GroupFilter, { type GroupNode } from "./GroupFilter"; import { STAGE_COLORS, STAGE_ORDER } from "./types"; import type { @@ -429,6 +429,12 @@ export default function AnalyticsView({ deals={currentProject.allDeals} onOpenTable={onOpenTable} /> + + {/* Row 7: Excluded from Pipeline (Removed from Bookings / Removed from Program) */} + ); } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx index c5b314d..a20476a 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -47,6 +47,43 @@ function DampMouldBadgeCell({ value }: { value: unknown }) { ); } +function RemovalFlagChip({ label, tooltip }: { label: string; tooltip: string }) { + return ( + + {label} + + ); +} + +function DealNameCell({ deal }: { deal: ClassifiedDeal }) { + const name = deal.dealname; + const isDoNotBook = deal.bookingStatus === "Do Not Book"; + const isRemovedFromProgram = deal.batch === "Removed from Program"; + + return ( +
+ + {name ?? } + + {isDoNotBook && ( + + )} + {isRemovedFromProgram && ( + + )} +
+ ); +} + function DampMouldCommentCell({ value }: { value: unknown }) { const comment = typeof value === "string" ? value.trim() : ""; @@ -183,6 +220,9 @@ export default function DrillDownTable({ ), cell: ({ row }) => { const value = row.original[key as keyof ClassifiedDeal]; + if (key === "dealname") { + return ; + } if (key === "majorConditionIssuePhotosS3") { return ; } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExcludedFromPipelinePanel.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExcludedFromPipelinePanel.tsx new file mode 100644 index 0000000..5ca62fc --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ExcludedFromPipelinePanel.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { motion } from "framer-motion"; +import { CircleOff } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import type { ClassifiedDeal } from "./types"; + +const REMOVED_FROM_BOOKINGS_COLUMNS: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "coordinator", + "confirmedSurveyDate", + "propertyHaltedDate", + "propertyHaltedReason", +]; +const REMOVED_FROM_BOOKINGS_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + coordinator: "Coordinator", + confirmedSurveyDate: "Confirmed Survey Date", + propertyHaltedDate: "Halted Date", + propertyHaltedReason: "Halted Reason", +}; + +const REMOVED_FROM_PROGRAM_COLUMNS: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "batchDescription", + "coordinator", + "propertyHaltedDate", + "propertyHaltedReason", +]; +const REMOVED_FROM_PROGRAM_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + batchDescription: "Group", + coordinator: "Coordinator", + propertyHaltedDate: "Halted Date", + propertyHaltedReason: "Halted Reason", +}; + +interface ExcludedFromPipelinePanelProps { + deals: ClassifiedDeal[]; + onOpenTable: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, + ) => void; +} + +export default function ExcludedFromPipelinePanel({ + deals, + onOpenTable, +}: ExcludedFromPipelinePanelProps) { + const removedFromBookings = deals.filter( + (d) => d.displayStage === "Removed from Bookings", + ); + const removedFromProgram = deals.filter( + (d) => d.displayStage === "Removed from Program", + ); + + if (removedFromBookings.length === 0 && removedFromProgram.length === 0) return null; + + return ( + + +
+ +

+ Excluded from Pipeline +

+
+

+ Terminal states — properties intentionally not progressing through the pipeline. +

+ +
+ {removedFromBookings.length > 0 && ( + + onOpenTable( + "Removed from Bookings", + removedFromBookings, + REMOVED_FROM_BOOKINGS_COLUMNS, + REMOVED_FROM_BOOKINGS_LABELS, + ) + } + className="group text-left rounded-xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-4 hover:border-slate-300 hover:shadow-md transition-all duration-200" + > +

+ Removed from Bookings +

+

+ {removedFromBookings.length} +

+

+ Removed from the bookings list before a survey took place +

+
+ )} + {removedFromProgram.length > 0 && ( + + onOpenTable( + "Removed from Program", + removedFromProgram, + REMOVED_FROM_PROGRAM_COLUMNS, + REMOVED_FROM_PROGRAM_LABELS, + ) + } + className="group text-left rounded-xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-4 hover:border-slate-300 hover:shadow-md transition-all duration-200" + > +

+ Removed from Program +

+

+ {removedFromProgram.length} +

+

+ Removed from the project entirely +

+
+ )} +
+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index dd8b3bf..b8a67a3 100644 Binary files a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx and b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx differ diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx index dd90dc4..b9acc29 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx @@ -251,6 +251,8 @@ export default function PropertyTable({ data, onOpenDrawer, portfolioId = "", sh ))} Queries + Removed from Bookings + Removed from Program diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx deleted file mode 100644 index f1e46f3..0000000 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyedResultsPieChart.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; - -import { DonutChart, Card, Title } from "@tremor/react"; -import { useState } from "react"; -import type { OutcomeSlice, ClassifiedDeal } from "./types"; - -interface SurveyedPieChartProps { - slices: OutcomeSlice[]; - dealsByOutcome: Record; - onOpenTable?: (outcome: string, filteredDeals: ClassifiedDeal[]) => void; -} - -export default function SurveyedResultsPieChart({ - slices, - dealsByOutcome, - onOpenTable, -}: SurveyedPieChartProps) { - const colors = [ - "indigo-600", - "indigo-400", - "blue-300", - "amber-400", - "amber-200", - "slate-400", - "gray-300", - "gray-100", - "gray-200", - ]; - - const [hovered, setHovered] = useState(null); - - const handleClick = (slice: OutcomeSlice) => { - if (!slice) return; - const filteredDeals = dealsByOutcome[slice.name] ?? []; - onOpenTable?.(slice.name, filteredDeals); - }; - - // Don't show the chart if there's no data - if (slices.length === 0) { - return null; - } - - // Convert OutcomeSlice to chart data format - const chartData = slices.map((slice) => ({ - name: slice.name, - amount: slice.amount, - percentage: slice.percentage, - })); - - return ( - - {/* Header */} -
- - Survey Performance - -

- Click a segment or label to view filtered properties -

-
- - {/* Donut Chart (Centered) */} -
- `${n.toLocaleString()}`} - colors={colors} - onValueChange={handleClick} - showLabel={false} - className="w-64 h-64 cursor-pointer" - customTooltip={({ payload }) => { - const item = payload?.[0]?.payload; - if (!item) return null; - const { name, amount } = item; - return ( -
-
- - {name} - - - {amount.toLocaleString()} - -
-
- ); - }} - /> - {slices.length > 0 && ( -
- - {slices.reduce((a, b) => a + b.amount, 0)} - -
- )} -
- - {/* Legend (Clean Grid Layout) */} -
- {slices.map((slice, idx) => ( - - ))} -
-
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts index 0b2f2d7..26281cd 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts @@ -162,6 +162,7 @@ const mockDealRow = { majorConditionIssuePhotosS3: null, coordinationStatus: null, designStatus: null, + bookingStatus: null, pashubLink: null, sharepointLink: null, dampmouldGrowth: null, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts index 8f65919..a913087 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts @@ -31,6 +31,7 @@ export function mapDbRowToHubspotDeal(row: DealRow): HubspotDeal { majorConditionIssuePhotosS3: d.majorConditionIssuePhotosS3, coordinationStatus: d.coordinationStatus, designStatus: d.designStatus, + bookingStatus: d.bookingStatus, pashubLink: d.pashubLink, sharepointLink: d.sharepointLink, dampMouldFlag: d.dampmouldGrowth, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts index 38c1490..b848279 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts @@ -20,6 +20,7 @@ function makeDeal(overrides: Partial = {}): HubspotDeal { majorConditionIssuePhotosS3: null, coordinationStatus: null, designStatus: null, + bookingStatus: null, pashubLink: null, sharepointLink: null, dampMouldFlag: null, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/measureFilters.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/measureFilters.test.ts index 9398b9a..faaf452 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/measureFilters.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/measureFilters.test.ts @@ -19,6 +19,7 @@ function makeDeal(overrides: Partial = {}): ClassifiedDeal { majorConditionIssuePhotosS3: null, coordinationStatus: null, designStatus: null, + bookingStatus: null, pashubLink: null, sharepointLink: null, dampMouldFlag: null, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts index 9145af8..5d98e0b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts @@ -5,7 +5,6 @@ import { computeDampMouldRisk, computeFunnelStages, computeProjectProgress, - computeOutcomeSlices, computeLiveTrackerData, } from "./transforms"; import type { HubspotDeal, ClassifiedDeal } from "./types"; @@ -27,6 +26,7 @@ function makeDeal(overrides: Partial = {}): HubspotDeal { majorConditionIssuePhotosS3: null, coordinationStatus: null, designStatus: null, + bookingStatus: null, pashubLink: null, sharepointLink: null, dampMouldFlag: null, @@ -132,6 +132,62 @@ describe("resolveDisplayStage — RA ISSUE override on non-AFTER_ASSESSMENT stag }); }); +describe("resolveDisplayStage — Removed from Bookings", () => { + it("classifies as Removed from Bookings when bookingStatus is Do Not Book and no outcome", () => { + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223910", + bookingStatus: "Do Not Book", + outcome: null, + })) + ).toBe("Removed from Bookings"); + }); + + it("falls back to normal stage resolution when bookingStatus is Do Not Book but an outcome is set", () => { + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223910", + bookingStatus: "Do Not Book", + outcome: "Tenant Refusal", + })) + ).toBe("Scope & Planning"); + }); +}); + +describe("resolveDisplayStage — Removed from Program", () => { + it("classifies as Removed from Program when batch is 'Removed from Program' regardless of dealstage", () => { + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223913", + batch: "Removed from Program", + })) + ).toBe("Removed from Program"); + }); + + it("Removed from Program takes precedence over Removed from Bookings", () => { + expect( + resolveDisplayStage(makeDeal({ + batch: "Removed from Program", + bookingStatus: "Do Not Book", + outcome: null, + })) + ).toBe("Removed from Program"); + }); +}); + +describe("resolveDisplayStage — precedence between new stages and existing classification", () => { + it("Removed from Bookings takes precedence over a Queries dealstage", () => { + // "2663668937" is a Queries-mapped dealstage; without Do Not Book it'd resolve to "Queries" + expect( + resolveDisplayStage(makeDeal({ + dealstage: "2663668937", + bookingStatus: "Do Not Book", + outcome: null, + })) + ).toBe("Removed from Bookings"); + }); +}); + describe("resolveDisplayStage — AFTER_ASSESSMENT sub-classification", () => { const AFTER_ASSESSMENT_STAGE = "3948185842"; @@ -399,6 +455,17 @@ describe("computeFunnelStages", () => { const funnel = computeFunnelStages([]); expect(funnel.length).toBe(10); // STAGE_ORDER has 10 entries }); + + it("excludes Removed from Bookings and Removed from Program from counts and percentages", () => { + const deals = [ + makeClassified({ displayStage: "Scope & Planning" }), + makeClassified({ displayStage: "Removed from Bookings" }), + makeClassified({ displayStage: "Removed from Program" }), + ]; + const funnel = computeFunnelStages(deals); + const scopeEntry = funnel.find((f) => f.stage === "Scope & Planning")!; + expect(scopeEntry.currentPct).toBe(100); // 1 out of 1 in-pipeline deal + }); }); // ----------------------------------------------------------------------- @@ -454,40 +521,16 @@ describe("computeProjectProgress", () => { const result = computeProjectProgress([makeClassified({ displayStage: "Queries" })]); expect(result.stageProgress.find((s) => s.stage === "Queries")).toBeUndefined(); }); -}); -// ----------------------------------------------------------------------- -// computeOutcomeSlices -// ----------------------------------------------------------------------- - -describe("computeOutcomeSlices", () => { - it("counts only known SURVEYOR_OUTCOMES values", () => { + it("excludes Removed from Bookings and Removed from Program from nonQueryTotal", () => { const deals = [ - makeClassified({ outcome: "Surveyed" }), - makeClassified({ outcome: "Surveyed" }), - makeClassified({ outcome: "UNKNOWN_VALUE" }), - makeClassified({ outcome: null }), + makeClassified({ displayStage: "Scope & Planning" }), + makeClassified({ displayStage: "Removed from Bookings" }), + makeClassified({ displayStage: "Removed from Program" }), ]; - const slices = computeOutcomeSlices(deals); - expect(slices).toHaveLength(1); - expect(slices[0].name).toBe("Surveyed"); - expect(slices[0].amount).toBe(2); - }); - - it("computes percentage as a formatted string with one decimal place", () => { - const deals = [ - makeClassified({ outcome: "Surveyed" }), - makeClassified({ outcome: "Surveyed" }), - makeClassified({ outcome: "Other" }), - makeClassified({ outcome: "Other" }), - ]; - const slices = computeOutcomeSlices(deals); - const surveyed = slices.find((s) => s.name === "Surveyed")!; - expect(surveyed.percentage).toBe("50.0"); - }); - - it("returns empty array when no deals have matching outcomes", () => { - expect(computeOutcomeSlices([makeClassified({ outcome: null })])).toEqual([]); + const result = computeProjectProgress(deals); + expect(result.totalDeals).toBe(3); + expect(result.nonQueryTotal).toBe(1); }); }); 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 5d78839..6171f20 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -9,7 +9,6 @@ import type { DisplayStage, ProjectProgressData, ProjectData, - OutcomeSlice, LiveTrackerProps, DampMouldRiskData, @@ -18,10 +17,17 @@ import type { import { STAGE_ORDER, - SURVEYOR_OUTCOMES, MAJOR_CONDITION_STAGE_ID, } from "./types"; +// Terminal stages that exit the pipeline by design — excluded from funnel +// counts and stage-progress denominators. +const STAGES_EXCLUDED_FROM_PIPELINE: ReadonlySet = new Set([ + "Queries", + "Removed from Bookings", + "Removed from Program", +]); + // ----------------------------------------------------------------------- // Stage ID -> raw label mapping // ----------------------------------------------------------------------- @@ -106,6 +112,14 @@ function resolvePostDesignStage(deal: HubspotDeal): DisplayStage { // Maps dealstage ID + coordination/design/install status -> DisplayStage // ----------------------------------------------------------------------- export function resolveDisplayStage(deal: HubspotDeal): DisplayStage { + if (deal.batch === "Removed from Program") { + return "Removed from Program"; + } + + if (deal.bookingStatus === "Do Not Book" && !deal.outcome) { + return "Removed from Bookings"; + } + const raw = STAGE_ID_MAP[deal.dealstage ?? ""] ?? "AFTER_ASSESSMENT"; if (raw === "AFTER_ASSESSMENT") { @@ -170,7 +184,7 @@ export function computeDampMouldRisk(deals: ClassifiedDeal[]): DampMouldRiskData // Compute pipeline funnel — dual counts (current snapshot + cumulative) // ----------------------------------------------------------------------- export function computeFunnelStages(deals: ClassifiedDeal[]): FunnelStage[] { - const nonQueryDeals = deals.filter((d) => d.displayStage !== "Queries"); + const nonQueryDeals = deals.filter((d) => !STAGES_EXCLUDED_FROM_PIPELINE.has(d.displayStage)); const total = nonQueryDeals.length; return STAGE_ORDER.map((stage) => { @@ -201,7 +215,7 @@ export function computeProjectProgress( deals: ClassifiedDeal[] ): ProjectProgressData { const queriesDeals = deals.filter((d) => d.displayStage === "Queries"); - const nonQueryDeals = deals.filter((d) => d.displayStage !== "Queries"); + const nonQueryDeals = deals.filter((d) => !STAGES_EXCLUDED_FROM_PIPELINE.has(d.displayStage)); const nonQueryTotal = nonQueryDeals.length; // Stage counts/percentages (queries excluded from percentage calculation) @@ -210,7 +224,7 @@ export function computeProjectProgress( (stageBuckets[deal.displayStage] ??= []).push(deal); } - const stageProgress = STAGE_ORDER.filter((s) => s !== "Queries").map( + const stageProgress = STAGE_ORDER.filter((s) => !STAGES_EXCLUDED_FROM_PIPELINE.has(s)).map( (stage) => { const stageDeals = stageBuckets[stage] ?? []; return { @@ -243,34 +257,6 @@ export function computeProjectProgress( }; } -// ----------------------------------------------------------------------- -// Compute outcome pie slices for the surveyed pie chart -// ----------------------------------------------------------------------- -export function computeOutcomeSlices(deals: ClassifiedDeal[]): OutcomeSlice[] { - const counts: Partial> = {}; - - for (const deal of deals) { - if ( - deal.outcome && - (SURVEYOR_OUTCOMES as readonly string[]).includes(deal.outcome) - ) { - counts[deal.outcome] = (counts[deal.outcome] ?? 0) + 1; - } - } - - const total = Object.values(counts).reduce( - (a, b) => a + (b ?? 0), - 0 - ); - - return Object.entries(counts).map(([name, amount]) => ({ - name, - amount: amount ?? 0, - percentage: - total > 0 ? (((amount ?? 0) / total) * 100).toFixed(1) : "0.0", - })); -} - // ----------------------------------------------------------------------- // Top-level function called by page.tsx // Orchestrates all transformations: classify, group by project, compute stats @@ -293,12 +279,11 @@ export function computeLiveTrackerData( (grouped[key] ??= []).push(deal); } - // For each project group, compute progress data and outcome slices + // For each project group, compute progress data const projects: ProjectData[] = Object.entries(grouped).map( ([projectCode, deals]) => ({ projectCode, progress: computeProjectProgress(deals), - outcomePieSlices: computeOutcomeSlices(deals), allDeals: deals, }) ); @@ -308,7 +293,6 @@ export function computeLiveTrackerData( projects.unshift({ projectCode: "__ALL__", progress: computeProjectProgress(classified), - outcomePieSlices: computeOutcomeSlices(classified), allDeals: classified, }); } 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 ed65920..c97b5db 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -23,6 +23,7 @@ export type HubspotDeal = { majorConditionIssuePhotosS3: string | null; coordinationStatus: string | null; designStatus: string | null; + bookingStatus: string | null; // ── CRM-synced additions ────────────────────────────────────────────── pashubLink: string | null; @@ -88,6 +89,8 @@ export type DisplayStage = | "At Post Survey" | "Project Complete" | "Queries" + | "Removed from Bookings" + | "Removed from Program" | "Unknown Stage"; // ----------------------------------------------------------------------- @@ -145,22 +148,12 @@ export type ProjectProgressData = { funnelStages: FunnelStage[]; }; -// ----------------------------------------------------------------------- -// Surveyed outcome entry (for pie chart) -// ----------------------------------------------------------------------- -export type OutcomeSlice = { - name: string; // outcome label - amount: number; - percentage: string; // pre-formatted "12.3" -}; - // ----------------------------------------------------------------------- // What LiveTracker receives from page.tsx for one project // ----------------------------------------------------------------------- export type ProjectData = { projectCode: string; progress: ProjectProgressData; - outcomePieSlices: OutcomeSlice[]; // empty array = hide pie chart allDeals: ClassifiedDeal[]; // for table drill-downs within project }; @@ -411,6 +404,18 @@ export const STAGE_COLORS: Record< border: "border-red-200", dot: "bg-red-400", }, + "Removed from Bookings": { + bg: "bg-slate-50", + text: "text-slate-600", + border: "border-slate-200", + dot: "bg-slate-400", + }, + "Removed from Program": { + bg: "bg-slate-100", + text: "text-slate-700", + border: "border-slate-300", + dot: "bg-slate-500", + }, "Unknown Stage": { bg: "bg-gray-50", text: "text-gray-500",