From 3a4d102ea45107d6658e65d44255beb6ab7a546f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 17 Apr 2026 17:52:58 +0000 Subject: [PATCH] working on contractor upload modal --- src/app/db/schema/uploaded_files.ts | 29 +- .../live/ContractorUploadModal.tsx | 332 +++++++++++++++--- 2 files changed, 306 insertions(+), 55 deletions(-) diff --git a/src/app/db/schema/uploaded_files.ts b/src/app/db/schema/uploaded_files.ts index abf2fa1..cc5141c 100644 --- a/src/app/db/schema/uploaded_files.ts +++ b/src/app/db/schema/uploaded_files.ts @@ -16,22 +16,37 @@ export const fileType = pgEnum("file_type", [ "ecmk_rd_sap_site_note", "ecmk_survey_xml", // Contractor install documentation + // Photos "pre_photo", "mid_photo", "post_photo", + "loft_hatch_photo", + "dmev_photos", + "door_undercut_photos", + "trickle_vent_photos", + // Pre-installation "pre_installation_building_inspection", + "point_of_work_risk_assessment", + // Compliance & lodgement "claim_of_compliance", + "mcs_compliance_certificate", + "certificate_of_conformity", + "minor_works_electrical_certificate", + "trustmark_licence_numbers", + "operative_competency", + // Ventilation + "ventilation_assessment_checklist", + "anemometer_readings", + "commissioning_records", + "part_f_ventilation_document", + // Handover & warranties "handover_pack", "insurance_guarantee", - "installer_qualifications", - "mcs_compliance_certificate", - "minor_works_electrical_certificate", - "point_of_work_risk_assessment", - "installer_feedback", "workmanship_warranty", "g98_notification", - "certificate_of_conformity", - "ventilation_assessment_checklist", + // Qualifications & other + "installer_qualifications", + "installer_feedback", "contractor_other", ]); diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx index 0d6de22..eb6b66d 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { Dialog, DialogContent, @@ -19,7 +20,7 @@ import { SelectTrigger, SelectValue, } from "@/app/shadcn_components/ui/select"; -import { CheckCircle2, XCircle, Upload, Loader2, Clock } from "lucide-react"; +import { CheckCircle2, XCircle, Upload, Loader2, Clock, ChevronDown, ChevronRight, Info } from "lucide-react"; import { uploadFileToS3 } from "@/app/utils/s3"; import type { ClassifiedDeal } from "./types"; @@ -54,27 +55,97 @@ type Props = { // ── Constants ───────────────────────────────────────────────────────────── -const FILE_TYPE_OPTIONS: { value: string; label: string; group: string }[] = [ - { value: "pre_photo", label: "Pre Photo", group: "Install Photos" }, - { value: "mid_photo", label: "Mid Photo", group: "Install Photos" }, - { value: "post_photo", label: "Post Photo", group: "Install Photos" }, - { value: "pre_installation_building_inspection", label: "Pre-Installation Building Inspection (PIBI)", group: "Pre-Installation" }, - { value: "point_of_work_risk_assessment", label: "Point of Work Risk Assessment", group: "Pre-Installation" }, - { value: "claim_of_compliance", label: "Claim of Compliance (PAS 2030)", group: "Compliance" }, - { value: "mcs_compliance_certificate", label: "MCS Compliance Certificate", group: "Compliance" }, - { value: "certificate_of_conformity", label: "Certificate of Conformity", group: "Compliance" }, - { value: "minor_works_electrical_certificate", label: "Minor Works Electrical Certificate", group: "Compliance" }, - { value: "handover_pack", label: "Handover Documents / Pack", group: "Handover" }, - { value: "workmanship_warranty", label: "Workmanship Warranty", group: "Handover" }, - { value: "insurance_guarantee", label: "Insurance Backed Guarantee (IBG)", group: "Handover" }, - { value: "g98_notification", label: "G98 / G99 Notification", group: "Handover" }, - { value: "ventilation_assessment_checklist", label: "Ventilation Assessment Checklist", group: "Handover" }, - { value: "installer_qualifications", label: "Installer Qualifications", group: "Qualifications" }, - { value: "installer_feedback", label: "Installer Feedback", group: "Other" }, - { value: "contractor_other", label: "Other", group: "Other" }, +const FILE_TYPE_OPTIONS: { value: string; label: string; group: string; hint?: string }[] = [ + // Photos + { value: "pre_photo", label: "Pre-Install Photos", group: "Photos", hint: "Required for ALL measures. Capture existing condition before any work begins." }, + { value: "mid_photo", label: "Mid-Install Photos", group: "Photos", hint: "Required for ALL measures. Detailed photos showing all angles and areas during installation. Insufficient pictures will result in non-lodgement." }, + { value: "post_photo", label: "Post-Install Photos", group: "Photos", hint: "Required for ALL measures. Confirm completed installation." }, + { value: "loft_hatch_photo", label: "Loft Hatch & Draft Excluder Photos",group: "Photos", hint: "Required for loft insulation. Must show loft hatch insulation, draft excluders, and hook & eye closing. Also include photos of insulation depth with a ruler showing thickness." }, + { value: "dmev_photos", label: "DMEV Photos (Wetrooms)", group: "Photos", hint: "Clear photos of all Decentralised Mechanical Extract Ventilation units installed in wetrooms." }, + { value: "door_undercut_photos",label: "Door Undercut Photos", group: "Photos", hint: "Photos of all door undercuts to demonstrate compliant ventilation paths." }, + { value: "trickle_vent_photos", label: "Trickle Vent Photos", group: "Photos", hint: "Photos of all trickle vents located in windows." }, + // Pre-installation + { value: "pre_installation_building_inspection", label: "PIBI / Tech Survey", group: "Pre-Installation", hint: "Pre-Installation Building Inspection — required per property and per measure." }, + { value: "point_of_work_risk_assessment", label: "Point of Work Risk Assessment", group: "Pre-Installation" }, + // Compliance & lodgement + { value: "claim_of_compliance", label: "DOCC 2030 (Claim of Compliance)", group: "Compliance & Lodgement", hint: "Required per property and per measure for TrustMark lodgement under PAS 2030." }, + { value: "mcs_compliance_certificate", label: "MCS Compliance Certificate", group: "Compliance & Lodgement", hint: "Required for Solar PV and Air Source Heat Pump installations." }, + { value: "certificate_of_conformity", label: "Certificate of Conformity", group: "Compliance & Lodgement" }, + { value: "minor_works_electrical_certificate", label: "Minor Works Electrical Certificate", group: "Compliance & Lodgement" }, + { value: "trustmark_licence_numbers", label: "TrustMark Licence Numbers", group: "Compliance & Lodgement", hint: "All installer and subcontractor TrustMark licence numbers. Ensure all are accredited for the correct measures under PAS 2023." }, + { value: "operative_competency", label: "Operative Competency", group: "Compliance & Lodgement", hint: "PAS 2030 installer accreditation and qualifications of individual workers, suitable for the measure(s) installed. Verify all installers/subcontractors are accredited under PAS 2023." }, + // Ventilation + { value: "ventilation_assessment_checklist", label: "Ventilation Assessment Checklist", group: "Ventilation" }, + { value: "anemometer_readings", label: "Anemometer Readings", group: "Ventilation", hint: "Required for DMEV/ventilation measures to confirm airflow compliance." }, + { value: "commissioning_records", label: "Commissioning Records", group: "Ventilation", hint: "Tests, certifications and commissioning records for all systems installed." }, + { value: "part_f_ventilation_document", label: "Approved Document Part F", group: "Ventilation", hint: "Ventilation compliance document under Approved Document Part F." }, + // Handover & warranties + { value: "handover_pack", label: "Handover Pack", group: "Handover & Warranties" }, + { value: "workmanship_warranty", label: "Workmanship Warranty", group: "Handover & Warranties", hint: "Required per property and per measure for TrustMark lodgement." }, + { value: "insurance_guarantee", label: "Insurance Backed Guarantee (IBG)", group: "Handover & Warranties", hint: "Required per property and per measure for TrustMark lodgement." }, + { value: "g98_notification", label: "G98 / G99 Notification", group: "Handover & Warranties", hint: "Required for Solar PV and other grid-connected installations." }, + // Qualifications & other + { value: "installer_qualifications", label: "Installer Qualifications", group: "Qualifications & Other" }, + { value: "installer_feedback", label: "Installer Feedback", group: "Qualifications & Other" }, + { value: "contractor_other", label: "Other", group: "Qualifications & Other" }, ]; -const FILE_TYPE_GROUPS = ["Install Photos", "Pre-Installation", "Compliance", "Handover", "Qualifications", "Other"]; +const FILE_TYPE_GROUPS = [ + "Photos", + "Pre-Installation", + "Compliance & Lodgement", + "Ventilation", + "Handover & Warranties", + "Qualifications & Other", +]; + +// ── PAS 2030/2035 requirements summary (for guidance panel) ─────────────── + +const PAS_REQUIREMENTS = [ + { + heading: "Required for every property & measure", + items: [ + "PIBI / Tech Survey (pre-installation building inspection)", + "DOCC 2030 — Claim of Compliance (PAS 2030)", + "Insurance Backed Guarantee (IBG)", + "Workmanship Warranty", + "Pre, mid, and post-install photos (all measures)", + ], + }, + { + heading: "Additional for Solar PV & ASHP", + items: [ + "MCS Compliance Certificate", + "G98 / G99 Notification", + ], + }, + { + heading: "Loft insulation", + items: [ + "Loft hatch insulation, draft excluders, and hook & eye closing photos", + "Photos of insulation depth with a ruler showing thickness", + ], + }, + { + heading: "Ventilation measures", + items: [ + "Clear DMEV photos in all wetrooms", + "Anemometer readings", + "Commissioning records", + "Approved Document Part F ventilation document", + "Door undercut photos", + "Trickle vent photos", + ], + }, + { + heading: "Installer / lodgement pack", + items: [ + "TrustMark licence numbers for all installers & subcontractors (verify PAS 2023 accreditation)", + "Operative Competency (PAS 2030 accreditation + individual worker qualifications)", + "Minor Works Electrical Certificate (where applicable)", + ], + }, +]; // ── Helpers ─────────────────────────────────────────────────────────────── @@ -139,29 +210,188 @@ async function saveClassifications( if (!res.ok) throw new Error("Failed to save classifications"); } -// ── DocType select ───────────────────────────────────────────────────────── +// ── PAS guidance panel ──────────────────────────────────────────────────── -function DocTypeSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) { +function PasGuidancePanel() { + const [open, setOpen] = useState(false); return ( - +
+ + {open && ( +
+ {PAS_REQUIREMENTS.map((section) => ( +
+

+ {section.heading} +

+
    + {section.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +

+ Insufficient mid-install photos will result in the Retrofit Coordinator sending back for re-submission and non-lodgement. +

+
+ )} +
+ ); +} + +// ── Searchable DocType combobox ──────────────────────────────────────────── + +function DocTypeSelect({ value, onChange, showHint = false }: { value: string; onChange: (v: string) => void; showHint?: boolean }) { + const selected = FILE_TYPE_OPTIONS.find((o) => o.value === value); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [dropdownStyle, setDropdownStyle] = useState({}); + const triggerRef = useRef(null); + const inputRef = useRef(null); + + function openDropdown() { + if (!triggerRef.current) return; + const rect = triggerRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const dropdownHeight = 280; + const showAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight; + setDropdownStyle({ + position: "fixed", + left: rect.left, + width: rect.width, + zIndex: 9999, + ...(showAbove + ? { bottom: window.innerHeight - rect.top + 4 } + : { top: rect.bottom + 4 }), + }); + setOpen(true); + } + + function close() { + setOpen(false); + setQuery(""); + } + + function select(val: string) { + onChange(val); + close(); + } + + // Focus search input when opening + useEffect(() => { + if (open) setTimeout(() => inputRef.current?.focus(), 0); + }, [open]); + + const filtered = query.trim() + ? FILE_TYPE_OPTIONS.filter((o) => + o.label.toLowerCase().includes(query.toLowerCase()) || + o.group.toLowerCase().includes(query.toLowerCase()) + ) + : null; + + return ( +
+ {/* Trigger */} + + + {open && typeof document !== "undefined" && createPortal( + <> + {/* Transparent overlay — position:fixed escapes overflow:hidden, catches all outside clicks */} +
+ + {/* Dropdown — above the overlay */} +
+
+ setQuery(e.target.value)} + placeholder="Search…" + className="w-full rounded border border-gray-200 px-2 py-1 text-xs outline-none focus:border-brandblue" + /> +
+
+ {filtered ? ( + filtered.length === 0 ? ( +

No results

+ ) : ( + filtered.map((o) => ( + + )) + ) + ) : ( + FILE_TYPE_GROUPS.map((group) => { + const items = FILE_TYPE_OPTIONS.filter((o) => o.group === group); + if (!items.length) return null; + return ( +
+

+ {group} +

+ {items.map((o) => ( + + ))} +
+ ); + }) + )} +
+
+ , + document.body + )} + + {showHint && selected?.hint && ( +

{selected.hint}

+ )} +
); } @@ -343,7 +573,7 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr classifiableEntries.map((f) => ({ id: f.uploadedId!, fileType: f.docType, - measureName: f.measureName || undefined, + measureName: (f.measureName && f.measureName !== "__none__") ? f.measureName : undefined, })), ); onClose(); @@ -364,7 +594,7 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr return ( - + {phase === "loading" ? "Loading…" : @@ -400,6 +630,9 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr {/* ── Phase 1: Upload ── */} {phase === "upload" && ( <> + {/* PAS guidance */} + + {/* Existing unclassified banner */} {existingCount > 0 && (
@@ -463,15 +696,18 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr {/* ── Phase 2: Classify ── */} {phase === "classify" && (
+ {/* PAS guidance */} + + {/* Column headers */} -
+
File Document Type * Measure
{classifiableEntries.map((entry) => ( -
+
@@ -480,14 +716,14 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr {entry.existingS3Key &&

Previously uploaded

}
- updateEntryField(entry.id, "docType", v)} /> + updateEntryField(entry.id, "docType", v)} showHint /> {measures.length > 0 ? ( - updateEntryField(entry.id, "measureName", v === "__none__" ? "" : v)}> - — None — + — None — {measures.map((m) => ( {m} ))}