polishing project reporting ui
Some checks are pending
Next.js Build Check / build (push) Waiting to run

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-03 10:45:51 +00:00
parent 8ca1bf2799
commit 988bd2ce8f
8 changed files with 283 additions and 131 deletions

View file

@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { eq, or } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { db } from "@/app/db/db";
import { uploadedFiles } from "@/app/db/schema/uploaded_files";
@ -16,23 +16,24 @@ export async function GET(req: Request) {
}
try {
const conditions = [];
if (uprnParam) {
const uprnBigInt = BigInt(uprnParam);
conditions.push(eq(uploadedFiles.uprn, uprnBigInt));
}
if (landlordPropertyIdParam) {
conditions.push(
eq(uploadedFiles.landlordPropertyId, landlordPropertyIdParam),
);
}
// Prefer UPRN — it's more selective and avoids an OR full-table scan.
// Only fall back to landlordPropertyId when no UPRN is available.
const condition = uprnParam
? eq(uploadedFiles.uprn, BigInt(uprnParam))
: eq(uploadedFiles.landlordPropertyId, landlordPropertyIdParam!);
const rows = await db
.select()
.select({
id: uploadedFiles.id,
s3FileKey: uploadedFiles.s3FileKey,
s3FileBucket: uploadedFiles.s3FileBucket,
s3UploadTimestamp: uploadedFiles.s3UploadTimestamp,
fileType: uploadedFiles.fileType,
uprn: uploadedFiles.uprn,
landlordPropertyId: uploadedFiles.landlordPropertyId,
})
.from(uploadedFiles)
.where(conditions.length === 1 ? conditions[0] : or(...conditions));
.where(condition);
const documents = rows.map((row) => ({
id: String(row.id),

View file

@ -14,6 +14,7 @@ import type {
ClassifiedDeal,
TableModal,
FunnelStage,
DisplayStage,
} from "./types";
// -----------------------------------------------------------------------
@ -117,8 +118,9 @@ function PipelineFunnel({
}) {
const [mode, setMode] = useState<"current" | "cumulative">("cumulative");
const ALWAYS_VISIBLE: DisplayStage[] = ["At Lodgement", "Project Complete"];
const visibleStages = funnelStages.filter(
(s) => s.currentCount > 0 || s.cumulativeCount > 0,
(s) => s.currentCount > 0 || s.cumulativeCount > 0 || ALWAYS_VISIBLE.includes(s.stage),
);
const maxCount = Math.max(
@ -337,8 +339,9 @@ export default function AnalyticsView({
{/* Row 1.5: Completion trends chart */}
<CompletionTrendsChart
deals={currentProject.allDeals}
isDomnaUser={true} // TODO: Replace with real user check
isDomnaUser={true}
projectCode={currentProjectCode}
onOpenTable={onOpenTable}
/>
{/* Row 2: section header */}

View file

@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { AlertCircle } from "lucide-react";
import { Card, Title, BarChart, Legend } from "@tremor/react";
import { Button } from "@/app/shadcn_components/ui/button";
import { Input } from "@/app/shadcn_components/ui/input";
@ -16,6 +17,12 @@ interface CompletionTrendsChartProps {
deals: ClassifiedDeal[];
isDomnaUser?: boolean;
projectCode?: string;
onOpenTable?: (
stage: string,
deals: ClassifiedDeal[],
columns?: (keyof ClassifiedDeal)[],
columnLabels?: Partial<Record<keyof ClassifiedDeal, string>>,
) => void;
}
const METRICS = [
@ -290,6 +297,7 @@ export default function CompletionTrendsChart({
deals,
isDomnaUser,
projectCode,
onOpenTable,
}: CompletionTrendsChartProps) {
const [metric, setMetric] = useState(METRICS[0].key);
const [targets, setTargets] = useState<{ [week: string]: number }>({});
@ -305,6 +313,17 @@ export default function CompletionTrendsChart({
const isDesign = metric === "design";
const isStacked = isCoordination || isAssessments || isLodgement || isDesign;
// Assessments from external surveyors (no surveyedDate recorded)
const undatedAssessments = isAssessments
? deals.filter((d) => {
const o = d.outcome ?? "";
return (
(o === "Surveyed" || o === "Surveyed - Pending Upload") &&
!d.surveyedDate
);
})
: [];
// Compute chart data, categories, and colours in one place
let chartData: Record<string, string | number>[];
let categories: string[];
@ -417,6 +436,36 @@ export default function CompletionTrendsChart({
stack={isStacked}
customTooltip={ChartTooltip}
/>
{isAssessments && undatedAssessments.length > 0 && (
<div className="mt-4 flex items-center justify-between gap-3 p-3 rounded-lg border border-amber-200 bg-amber-50/60">
<div className="flex items-center gap-2 min-w-0">
<AlertCircle className="h-4 w-4 text-amber-500 shrink-0" />
<span className="text-sm text-amber-700">
<span className="font-semibold">{undatedAssessments.length}</span>{" "}
assessment{undatedAssessments.length !== 1 ? "s" : ""} from external surveyors have no date recorded
</span>
</div>
{onOpenTable && (
<button
onClick={() =>
onOpenTable(
"Undated External Assessments",
undatedAssessments,
["dealname", "landlordPropertyId", "coordinator"],
{
dealname: "Address",
landlordPropertyId: "Property Ref.",
coordinator: "Surveyor",
},
)
}
className="shrink-0 text-xs font-semibold text-amber-700 underline underline-offset-2 hover:text-amber-900 transition-colors"
>
View properties
</button>
)}
</div>
)}
{isStacked && (
<Legend
categories={categories}

View file

@ -131,8 +131,6 @@ export default function DampMouldRiskPanel({
coordinator: "Coordinator",
};
const bothFlaggedDeals = risk.surveyFlagDeals.filter((d) => !!d.dampMouldFlag);
const noRisk =
risk.surveyFlagCount === 0 &&
risk.coordinatorFlagCount === 0;
@ -166,14 +164,14 @@ export default function DampMouldRiskPanel({
</div>
) : (
<>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<RiskStatCard
label="Flagged at Survey"
subtitle="Identified by assessor"
count={risk.surveyFlagCount}
total={totalDeals}
icon={AlertTriangle}
color="amber"
color="red"
onClick={() =>
onOpenTable(
"Damp & Mould — Survey Stage Flags",
@ -189,7 +187,7 @@ export default function DampMouldRiskPanel({
count={risk.coordinatorFlagCount}
total={totalDeals}
icon={Droplets}
color="orange"
color="red"
onClick={() =>
onOpenTable(
"Damp & Mould — Coordination Stage Flags",
@ -199,22 +197,6 @@ export default function DampMouldRiskPanel({
)
}
/>
<RiskStatCard
label="Flagged at Both Stages"
subtitle="Highest risk — action required"
count={risk.bothFlaggedCount}
total={totalDeals}
icon={ShieldAlert}
color="red"
onClick={() =>
onOpenTable(
"Damp & Mould — Flagged at Both Stages",
bothFlaggedDeals,
coordColumns,
coordLabels
)
}
/>
</div>
{/* Missed risk callout */}

View file

@ -9,9 +9,10 @@ import {
TabsTrigger,
} from "@/app/shadcn_components/ui/tabs";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { BarChart2, Table2 } from "lucide-react";
import { BarChart2, Table2, FolderOpen } from "lucide-react";
import DrillDownTable from "./DrillDownTable";
import PropertyTable from "./PropertyTable";
import DocumentTable from "./DocumentTable";
import type { HubspotDeal } from "./types";
import PropertyDrawer from "./PropertyDrawer";
import AnalyticsView from "./AnalyticsView";
@ -30,7 +31,7 @@ export default function LiveTracker({
docStatusMap,
}: LiveTrackerProps) {
// ── Tab state ────────────────────────────────────────────────────────
const [activeTab, setActiveTab] = useState<"analytics" | "properties">(
const [activeTab, setActiveTab] = useState<"analytics" | "properties" | "documents">(
"analytics",
);
@ -89,7 +90,7 @@ export default function LiveTracker({
<div className="space-y-4 w-full">
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as "analytics" | "properties")}
onValueChange={(v) => setActiveTab(v as "analytics" | "properties" | "documents")}
>
{/* Tab bar */}
<TabsList className="h-10 p-1 bg-brandlightblue/10 border border-brandblue/10 rounded-xl mb-6">
@ -107,6 +108,13 @@ export default function LiveTracker({
<Table2 className="h-3.5 w-3.5" />
Properties
</TabsTrigger>
<TabsTrigger
value="documents"
className="flex items-center gap-2 rounded-lg text-sm font-medium px-4 data-[state=active]:bg-white data-[state=active]:text-brandblue data-[state=active]:shadow-sm transition-all"
>
<FolderOpen className="h-3.5 w-3.5" />
Document Management
</TabsTrigger>
</TabsList>
{/* Analytics tab */}
@ -158,7 +166,38 @@ export default function LiveTracker({
<PropertyTable
data={currentProject?.allDeals ?? []}
onOpenDrawer={handleOpenDrawer}
showDocuments={true}
docStatusMap={docStatusMap}
/>
</div>
</TabsContent>
{/* Document Management tab */}
<TabsContent value="documents" className="mt-0">
<div className="space-y-4">
{projects.length > 1 && (
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500 shrink-0">Project:</span>
<select
value={currentProjectCode}
onChange={(e) => setCurrentProjectCode(e.target.value)}
className="px-3 py-1.5 border border-brandblue/20 rounded-lg bg-white text-sm text-gray-800 font-medium focus:ring-2 focus:ring-brandblue focus:border-brandblue focus:outline-none transition-all appearance-none pr-8"
>
{projectCodes.map((code) =>
code === "__ALL__" ? (
<option key="__ALL__" value="__ALL__" style={{ fontWeight: 700 }}>
All Projects
</option>
) : (
<option key={code} value={code}>
{code}
</option>
),
)}
</select>
</div>
)}
<DocumentTable
data={currentProject?.allDeals ?? []}
onOpenDrawer={handleOpenDrawer}
docStatusMap={docStatusMap}
/>
</div>
@ -194,7 +233,7 @@ export default function LiveTracker({
<div className="grid grid-cols-2 gap-3">
{Object.entries(openTable.breakdown).map(
([category, items]) => {
const isCompleted = category.includes("Completed");
const isCompleted = category.includes("Complete");
const bgColor = isCompleted
? "bg-gradient-to-br from-brandblue/25 to-brandblue/15"
: "bg-gradient-to-br from-amber-100/40 to-amber-50/30";

View file

@ -1,11 +1,12 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
FileDown,
FileText,
FileX,
Loader2,
FolderOpen,
X,
@ -20,6 +21,7 @@ import {
DrawerDescription,
} from "@/app/shadcn_components/ui/drawer";
import type { PropertyDocument } from "./types";
import { EXPECTED_SURVEY_DOC_TYPES } from "./types";
// Human-readable labels for the main DB fileType enum values
const DOC_TYPE_LABELS: Record<string, string> = {
@ -61,10 +63,10 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) {
async function handleDownload() {
setSigning(true);
try {
const res = await fetch("/api/sign-s3-url", {
const res = await fetch("/api/sign-document-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: doc.s3FileKey }),
body: JSON.stringify({ key: doc.s3FileKey, bucket: doc.s3FileBucket }),
});
if (!res.ok) throw new Error("Failed to get signed URL");
const data = await res.json();
@ -85,33 +87,34 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) {
layout
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center justify-between gap-3 p-3 rounded-lg border border-gray-100 bg-white hover:border-brandblue/20 hover:shadow-sm transition-all duration-150"
className="flex items-center justify-between gap-4 px-4 py-3 rounded-lg border border-gray-100 bg-white hover:border-brandblue/20 hover:shadow-sm transition-all duration-150"
>
{/* Left: icon + label + date stacked */}
<div className="flex items-center gap-3 min-w-0">
{/* Doc type badge */}
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md border text-xs font-medium shrink-0 bg-sky-50 text-sky-700 border-sky-200">
<FileText className="h-3.5 w-3.5" />
{label}
</span>
<div className="shrink-0 w-8 h-8 rounded-lg bg-sky-50 border border-sky-200 flex items-center justify-center">
<FileText className="h-4 w-4 text-sky-600" />
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">{label}</p>
<p className="text-xs text-gray-400 mt-0.5">
{formatDate(doc.s3UploadTimestamp)}
</p>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="text-xs text-gray-400 hidden sm:block">
{formatDate(doc.s3UploadTimestamp)}
</span>
<button
onClick={handleDownload}
disabled={signing}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-brandblue text-white text-xs font-medium hover:bg-brandblue/90 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{signing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<FileDown className="h-3.5 w-3.5" />
)}
{signing ? "Preparing…" : "Download"}
</button>
</div>
{/* Right: download button */}
<button
onClick={handleDownload}
disabled={signing}
className="shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-brandblue text-white text-xs font-medium hover:bg-brandblue/90 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{signing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<FileDown className="h-3.5 w-3.5" />
)}
{signing ? "Preparing…" : "Download"}
</button>
</motion.div>
);
}
@ -136,16 +139,19 @@ export default function PropertyDrawer({
}: PropertyDrawerProps) {
const canQuery = !!(uprn || landlordPropertyId);
const {
data: documents = [],
isLoading,
data: fetchedDocuments = [],
isFetching,
isError,
} = useQuery({
queryKey: ["property-documents", uprn, landlordPropertyId],
queryFn: async () => {
const params = new URLSearchParams();
if (uprn) params.set("uprn", uprn);
else if (landlordPropertyId) params.set("landlordPropertyId", landlordPropertyId);
const res = await fetch(`/api/live-tracking/property-documents?${params}`);
else if (landlordPropertyId)
params.set("landlordPropertyId", landlordPropertyId);
const res = await fetch(
`/api/live-tracking/property-documents?${params}`,
);
if (!res.ok) throw new Error("Failed to load documents");
return res.json() as Promise<PropertyDocument[]>;
},
@ -153,8 +159,17 @@ export default function PropertyDrawer({
staleTime: 30_000,
});
// Keep the last successfully fetched result so the closing animation doesn't
// flash the empty state (the parent nulls out uprn/landlordPropertyId on close,
// which disables the query and resets fetchedDocuments to [] mid-animation).
const lastDocumentsRef = useRef<PropertyDocument[]>([]);
if (open && !isFetching && !isError) {
lastDocumentsRef.current = fetchedDocuments as PropertyDocument[];
}
const documents = open ? (fetchedDocuments as PropertyDocument[]) : lastDocumentsRef.current;
// Group docs by category for display
const grouped = (documents as PropertyDocument[]).reduce<
const grouped = documents.reduce<
Record<string, PropertyDocument[]>
>((acc, doc) => {
const category = getDocCategory(doc.docType);
@ -164,24 +179,29 @@ export default function PropertyDrawer({
const hasDocuments = documents.length > 0;
const presentTypes = new Set(documents.map((d) => d.docType));
const missingTypes = EXPECTED_SURVEY_DOC_TYPES.filter(
(t) => !presentTypes.has(t),
);
return (
<Drawer open={open} onOpenChange={(v) => !v && onClose()} direction="right">
<DrawerContent className="fixed right-0 top-0 bottom-0 h-full w-full max-w-[440px] rounded-l-2xl rounded-r-none mt-0 flex flex-col border-l border-t-0 border-b-0 border-r-0 border-brandblue/10 bg-white shadow-2xl">
<DrawerContent className="fixed right-0 top-0 bottom-0 h-full w-[40vw] min-w-80 rounded-l-2xl rounded-r-none mt-0 flex flex-col border-l border-t-0 border-b-0 border-r-0 border-brandblue/10 bg-white shadow-2xl overflow-hidden">
{/* Remove the default drag handle */}
<div className="hidden" />
<DrawerHeader className="px-6 pt-6 pb-4 border-b border-gray-100">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<DrawerTitle className="text-lg font-semibold text-brandblue leading-tight truncate">
<DrawerHeader className="shrink-0 px-6 pt-6 pb-4 border-b border-gray-100">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<DrawerTitle className="text-lg font-semibold text-brandblue leading-tight line-clamp-2">
{dealname ?? "Property Documents"}
</DrawerTitle>
{uprn ? (
<DrawerDescription className="text-xs text-gray-500 mt-0.5 font-mono">
<DrawerDescription className="text-xs text-gray-500 mt-0.5 font-mono truncate">
UPRN: {uprn}
</DrawerDescription>
) : landlordPropertyId ? (
<DrawerDescription className="text-xs text-gray-500 mt-0.5 font-mono">
<DrawerDescription className="text-xs text-gray-500 mt-0.5 font-mono truncate">
Ref: {landlordPropertyId}
</DrawerDescription>
) : null}
@ -196,7 +216,7 @@ export default function PropertyDrawer({
</DrawerClose>
</div>
{hasDocuments && !isLoading && (
{hasDocuments && !isFetching && (
<div className="mt-3 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-brandblue/10 border border-brandblue/20">
<FileDown className="h-3.5 w-3.5 text-brandblue" />
<span className="text-xs font-medium text-brandblue">
@ -209,7 +229,7 @@ export default function PropertyDrawer({
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-5">
{/* Loading state */}
{isLoading && (
{isFetching && (
<div className="space-y-3 pt-2">
{[1, 2, 3].map((i) => (
<div
@ -221,7 +241,7 @@ export default function PropertyDrawer({
)}
{/* Error state */}
{isError && !isLoading && (
{isError && !isFetching && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mb-3">
<ExternalLink className="h-5 w-5 text-red-400" />
@ -235,25 +255,43 @@ export default function PropertyDrawer({
</div>
)}
{/* Empty state */}
{!isLoading && !isError && !hasDocuments && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-12 h-12 rounded-full bg-gray-50 border border-gray-200 flex items-center justify-center mb-3">
<FolderOpen className="h-6 w-6 text-gray-400" />
{/* Empty state — shows all missing doc types */}
{!isFetching && !isError && !hasDocuments && (
<div className="space-y-4 pt-1">
<div className="flex flex-col items-center py-6 text-center">
<div className="w-12 h-12 rounded-full bg-amber-50 border border-amber-200 flex items-center justify-center mb-3">
<FolderOpen className="h-6 w-6 text-amber-400" />
</div>
<p className="text-sm font-medium text-gray-700">
No documents available
</p>
<p className="text-xs text-gray-400 mt-1">
All {EXPECTED_SURVEY_DOC_TYPES.length} survey documents are
outstanding.
</p>
</div>
<div className="space-y-1.5">
<h3 className="text-xs font-semibold uppercase tracking-wide text-amber-500 px-0.5">
Missing Documents ({missingTypes.length})
</h3>
{missingTypes.map((t) => (
<div
key={t}
className="flex items-center gap-2.5 p-3 rounded-lg border border-dashed border-amber-200 bg-amber-50/40"
>
<FileX className="h-3.5 w-3.5 text-amber-300 shrink-0" />
<span className="text-xs text-amber-600 font-medium">
{DOC_TYPE_LABELS[t] ?? t}
</span>
</div>
))}
</div>
<p className="text-sm font-medium text-gray-700">
No documents uploaded
</p>
<p className="text-xs text-gray-400 mt-1 max-w-[220px]">
Survey documents will appear here once uploaded for this
property.
</p>
</div>
)}
{/* Document groups */}
<AnimatePresence>
{!isLoading &&
{!isFetching &&
!isError &&
hasDocuments &&
Object.entries(grouped).map(([category, docs]) => (
@ -274,6 +312,35 @@ export default function PropertyDrawer({
</motion.div>
))}
</AnimatePresence>
{/* Missing documents section — shown when some but not all docs are present */}
{!isFetching &&
!isError &&
hasDocuments &&
missingTypes.length > 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="space-y-2"
>
<h3 className="text-xs font-semibold uppercase tracking-wide text-amber-500 px-0.5">
Missing Documents ({missingTypes.length})
</h3>
<div className="space-y-1.5">
{missingTypes.map((t) => (
<div
key={t}
className="flex items-center gap-2.5 p-3 rounded-lg border border-dashed border-amber-200 bg-amber-50/40"
>
<FileX className="h-3.5 w-3.5 text-amber-300 shrink-0" />
<span className="text-xs text-amber-600 font-medium">
{DOC_TYPE_LABELS[t] ?? t}
</span>
</div>
))}
</div>
</motion.div>
)}
</div>
{/* Footer */}

View file

@ -94,10 +94,11 @@ function resolveAfterAssessmentStage(
// Called when design is UPLOADED — resolves install / lodgement / completed
// -----------------------------------------------------------------------
function resolvePostDesignStage(deal: HubspotDeal): DisplayStage {
if (deal.fullLodgementDate) return "Completed";
if (deal.lodgementStatus) return "Lodgement";
if (deal.fullLodgementDate) return "Project Complete";
if (deal.measuresLodgementDate) return "At Post Survey";
if (deal.lodgementStatus) return "At Lodgement";
if (deal.actualMeasuresInstalled || deal.installerHandover) return "Installation Complete";
return "Awaiting Install";
return "Installation in Progress";
}
// -----------------------------------------------------------------------
@ -216,7 +217,7 @@ export function computeProjectProgress(
}
);
const completedDeals = stageBuckets["Completed"] ?? [];
const completedDeals = stageBuckets["Project Complete"] ?? [];
const completedCount = completedDeals.length;
const completedPercentage =
nonQueryTotal > 0 ? (completedCount / nonQueryTotal) * 100 : 0;
@ -224,15 +225,16 @@ export function computeProjectProgress(
const totalDeals = deals.length;
// Coordination phase:
// completed = Design in Progress + Awaiting Install + Installation Complete + Lodgement + Completed
// 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",
"Awaiting Install",
"Installation in Progress",
"Installation Complete",
"Lodgement",
"Completed",
"At Lodgement",
"At Post Survey",
"Project Complete",
].includes(d.displayStage)
);
const coordInProgressDeals = deals.filter(
@ -252,14 +254,15 @@ export function computeProjectProgress(
};
// Design phase:
// completed = Awaiting Install + Installation Complete + Lodgement + Completed
// completed = Installation in Progress + Installation Complete + At Lodgement + At Post Survey + Project Complete
// in progress = Design in Progress
const designCompletedDeals = deals.filter((d) =>
[
"Awaiting Install",
"Installation in Progress",
"Installation Complete",
"Lodgement",
"Completed",
"At Lodgement",
"At Post Survey",
"Project Complete",
].includes(d.displayStage)
);
const designInProgressDeals = deals.filter(
@ -279,10 +282,10 @@ export function computeProjectProgress(
};
// Install phase:
// completed = Lodgement + Completed
// completed = At Lodgement + At Post Survey + Project Complete
// in progress = Installation Complete
const installCompletedDeals = deals.filter((d) =>
["Lodgement", "Completed"].includes(d.displayStage)
["At Lodgement", "At Post Survey", "Project Complete"].includes(d.displayStage)
);
const installInProgressDeals = deals.filter(
(d) => d.displayStage === "Installation Complete"
@ -301,10 +304,10 @@ export function computeProjectProgress(
};
// Lodgement phase:
// completed = Completed
// in progress = Lodgement
// completed = At Post Survey + Project Complete
// in progress = At Lodgement
const lodgementInProgressDeals = deals.filter(
(d) => d.displayStage === "Lodgement"
(d) => d.displayStage === "At Lodgement"
);
const lodgement: WorkPhaseStats = {

View file

@ -62,10 +62,11 @@ export type DisplayStage =
| "Assessment in Progress"
| "Coordination in Progress"
| "Design in Progress"
| "Awaiting Install"
| "Installation in Progress"
| "Installation Complete"
| "Lodgement"
| "Completed"
| "At Lodgement"
| "At Post Survey"
| "Project Complete"
| "Queries"
| "Unknown Stage";
@ -249,10 +250,11 @@ export const STAGE_ORDER: DisplayStage[] = [
"Assessment in Progress",
"Coordination in Progress",
"Design in Progress",
"Awaiting Install",
"Installation in Progress",
"Installation Complete",
"Lodgement",
"Completed",
"At Lodgement",
"At Post Survey",
"Project Complete",
];
// -----------------------------------------------------------------------
@ -275,10 +277,10 @@ export const STAGE_COLORS: Record<
dot: "bg-sky-400",
},
"Assessment in Progress": {
bg: "bg-violet-50",
text: "text-violet-700",
border: "border-violet-200",
dot: "bg-violet-400",
bg: "bg-blue-100",
text: "text-blue-900",
border: "border-blue-400",
dot: "bg-blue-700",
},
"Coordination in Progress": {
bg: "bg-indigo-50",
@ -292,11 +294,11 @@ export const STAGE_COLORS: Record<
border: "border-blue-200",
dot: "bg-blue-400",
},
"Awaiting Install": {
bg: "bg-purple-50",
text: "text-purple-700",
border: "border-purple-200",
dot: "bg-purple-400",
"Installation in Progress": {
bg: "bg-indigo-50",
text: "text-indigo-600",
border: "border-indigo-200",
dot: "bg-indigo-300",
},
"Installation Complete": {
bg: "bg-teal-50",
@ -304,13 +306,19 @@ export const STAGE_COLORS: Record<
border: "border-teal-200",
dot: "bg-teal-400",
},
Lodgement: {
"At Lodgement": {
bg: "bg-cyan-50",
text: "text-cyan-700",
border: "border-cyan-200",
dot: "bg-cyan-400",
},
Completed: {
"At Post Survey": {
bg: "bg-violet-50",
text: "text-violet-700",
border: "border-violet-200",
dot: "bg-violet-400",
},
"Project Complete": {
bg: "bg-emerald-50",
text: "text-emerald-700",
border: "border-emerald-200",