mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
working on contractor upload modal
This commit is contained in:
parent
94027059e0
commit
3a4d102ea4
2 changed files with 306 additions and 55 deletions
|
|
@ -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",
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger className="h-8 text-xs w-full">
|
||||
<SelectValue placeholder="Select type…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FILE_TYPE_GROUPS.map((group) => {
|
||||
const items = FILE_TYPE_OPTIONS.filter((o) => o.group === group);
|
||||
if (!items.length) return null;
|
||||
return (
|
||||
<SelectGroup key={group}>
|
||||
<SelectLabel className="text-[10px] text-gray-400 uppercase tracking-wide">{group}</SelectLabel>
|
||||
{items.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value} className="text-xs">{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="rounded-lg border border-blue-100 bg-blue-50/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-left"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5 text-blue-500 shrink-0" />
|
||||
<span className="text-xs font-medium text-blue-700 flex-1">
|
||||
PAS 2030/2035 document requirements
|
||||
</span>
|
||||
{open
|
||||
? <ChevronDown className="h-3.5 w-3.5 text-blue-400 shrink-0" />
|
||||
: <ChevronRight className="h-3.5 w-3.5 text-blue-400 shrink-0" />
|
||||
}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-3 pb-3 space-y-3 border-t border-blue-100">
|
||||
{PAS_REQUIREMENTS.map((section) => (
|
||||
<div key={section.heading}>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wide text-blue-600 mt-2.5 mb-1">
|
||||
{section.heading}
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
{section.items.map((item) => (
|
||||
<li key={item} className="flex items-start gap-1.5 text-xs text-blue-800">
|
||||
<span className="mt-1 h-1 w-1 rounded-full bg-blue-400 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-[10px] text-blue-500 mt-2 italic">
|
||||
Insufficient mid-install photos will result in the Retrofit Coordinator sending back for re-submission and non-lodgement.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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<React.CSSProperties>({});
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="space-y-1">
|
||||
{/* Trigger */}
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={() => open ? close() : openDropdown()}
|
||||
className={`flex h-8 w-full items-center justify-between rounded-md border px-3 py-1 text-xs transition-colors
|
||||
${open ? "border-brandblue ring-1 ring-brandblue/30" : "border-input hover:border-gray-300"}
|
||||
bg-background`}
|
||||
>
|
||||
<span className={selected ? "text-gray-800" : "text-gray-400"}>
|
||||
{selected ? selected.label : "Select type…"}
|
||||
</span>
|
||||
<ChevronDown className={`h-3.5 w-3.5 text-gray-400 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{open && typeof document !== "undefined" && createPortal(
|
||||
<>
|
||||
{/* Transparent overlay — position:fixed escapes overflow:hidden, catches all outside clicks */}
|
||||
<div className="fixed inset-0" style={{ zIndex: 9998 }} onClick={close} />
|
||||
|
||||
{/* Dropdown — above the overlay */}
|
||||
<div style={dropdownStyle} className="rounded-md border border-gray-200 bg-white shadow-xl">
|
||||
<div className="border-b border-gray-100 p-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-56 overflow-y-auto py-1">
|
||||
{filtered ? (
|
||||
filtered.length === 0 ? (
|
||||
<p className="px-3 py-2 text-xs text-gray-400">No results</p>
|
||||
) : (
|
||||
filtered.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => select(o.value)}
|
||||
className={`flex w-full flex-col px-3 py-1.5 text-left text-xs hover:bg-brandlightblue/30 transition-colors
|
||||
${value === o.value ? "bg-brandlightblue/20 font-medium text-brandblue" : "text-gray-700"}`}
|
||||
>
|
||||
<span>{o.label}</span>
|
||||
<span className="text-[10px] text-gray-400">{o.group}</span>
|
||||
</button>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
FILE_TYPE_GROUPS.map((group) => {
|
||||
const items = FILE_TYPE_OPTIONS.filter((o) => o.group === group);
|
||||
if (!items.length) return null;
|
||||
return (
|
||||
<div key={group}>
|
||||
<p className="px-3 pt-2 pb-0.5 text-[10px] font-semibold uppercase tracking-wide text-gray-400">
|
||||
{group}
|
||||
</p>
|
||||
{items.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => select(o.value)}
|
||||
className={`flex w-full px-3 py-1.5 text-left text-xs hover:bg-brandlightblue/30 transition-colors
|
||||
${value === o.value ? "bg-brandlightblue/20 font-medium text-brandblue" : "text-gray-700"}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{showHint && selected?.hint && (
|
||||
<p className="text-[10px] text-blue-600 leading-snug px-0.5">{selected.hint}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-xl max-h-[90vh] flex flex-col overflow-hidden">
|
||||
<DialogContent className="sm:max-w-4xl max-h-[92vh] flex flex-col overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{phase === "loading" ? "Loading…" :
|
||||
|
|
@ -400,6 +630,9 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
|
|||
{/* ── Phase 1: Upload ── */}
|
||||
{phase === "upload" && (
|
||||
<>
|
||||
{/* PAS guidance */}
|
||||
<PasGuidancePanel />
|
||||
|
||||
{/* Existing unclassified banner */}
|
||||
{existingCount > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-2.5 rounded-lg bg-amber-50 border border-amber-200 text-xs">
|
||||
|
|
@ -463,15 +696,18 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
|
|||
{/* ── Phase 2: Classify ── */}
|
||||
{phase === "classify" && (
|
||||
<div className="space-y-3">
|
||||
{/* PAS guidance */}
|
||||
<PasGuidancePanel />
|
||||
|
||||
{/* Column headers */}
|
||||
<div className="grid grid-cols-[1fr_180px_128px] gap-2 px-1">
|
||||
<div className="grid grid-cols-[1fr_260px_180px] gap-2 px-1">
|
||||
<span className="text-[10px] font-semibold text-gray-400 uppercase tracking-wide">File</span>
|
||||
<span className="text-[10px] font-semibold text-gray-400 uppercase tracking-wide">Document Type <span className="text-red-400">*</span></span>
|
||||
<span className="text-[10px] font-semibold text-gray-400 uppercase tracking-wide">Measure</span>
|
||||
</div>
|
||||
|
||||
{classifiableEntries.map((entry) => (
|
||||
<div key={entry.id} className="grid grid-cols-[1fr_180px_128px] gap-2 items-center px-1">
|
||||
<div key={entry.id} className="grid grid-cols-[1fr_260px_180px] gap-2 items-center px-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<StatusIcon status={entry.status} isExisting={!!entry.existingS3Key} />
|
||||
<div className="min-w-0">
|
||||
|
|
@ -480,14 +716,14 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
|
|||
{entry.existingS3Key && <p className="text-[10px] text-amber-500">Previously uploaded</p>}
|
||||
</div>
|
||||
</div>
|
||||
<DocTypeSelect value={entry.docType} onChange={(v) => updateEntryField(entry.id, "docType", v)} />
|
||||
<DocTypeSelect value={entry.docType} onChange={(v) => updateEntryField(entry.id, "docType", v)} showHint />
|
||||
{measures.length > 0 ? (
|
||||
<Select value={entry.measureName} onValueChange={(v) => updateEntryField(entry.id, "measureName", v)}>
|
||||
<Select value={entry.measureName || "__none__"} onValueChange={(v) => updateEntryField(entry.id, "measureName", v === "__none__" ? "" : v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full">
|
||||
<SelectValue placeholder="—" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="" className="text-xs text-gray-400">— None —</SelectItem>
|
||||
<SelectItem value="__none__" className="text-xs text-gray-400">— None —</SelectItem>
|
||||
{measures.map((m) => (
|
||||
<SelectItem key={m} value={m} className="text-xs">{m}</SelectItem>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue