260321:1700 Correct Coresspondence / Doing RFA

This commit is contained in:
admin
2026-03-21 17:00:41 +07:00
parent dcf55f4d08
commit 03d16cfd64
57 changed files with 1923 additions and 663 deletions
+506 -122
View File
@@ -1,14 +1,15 @@
"use client";
import { useForm, useFieldArray } from "react-hook-form";
import { useForm, type SubmitErrorHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { Plus, Trash2, Loader2 } from "lucide-react";
import { Loader2 } from "lucide-react";
import {
Select,
SelectContent,
@@ -18,18 +19,14 @@ import {
} from "@/components/ui/select";
import { useRouter } from "next/navigation";
import { useCreateRFA } from "@/hooks/use-rfa";
import { useDisciplines, useContracts } from "@/hooks/use-master-data";
import { useDrawings } from "@/hooks/use-drawing";
import { useDisciplines, useContracts, useOrganizations } from "@/hooks/use-master-data";
import { useCorrespondenceTypes, useRfaTypes } from "@/hooks/use-reference-data";
import { useProjects } from "@/hooks/use-projects";
import { CreateRfaDto } from "@/types/dto/rfa/rfa.dto";
import { useState, useEffect } from "react";
import { useState, useEffect, type FormEvent } from "react";
import { correspondenceService } from "@/lib/services/correspondence.service";
const rfaItemSchema = z.object({
itemNo: z.string().min(1, "Item No is required"),
description: z.string().min(3, "Description is required"),
quantity: z.number().min(0, "Quantity must be positive"),
unit: z.string().min(1, "Unit is required"),
});
const rfaSchema = z.object({
projectId: z.string().min(1, "Project is required"), // ADR-019: UUID
contractId: z.string().min(1, "Contract is required"),
@@ -41,25 +38,137 @@ const rfaSchema = z.object({
remarks: z.string().optional(),
toOrganizationId: z.string().min(1, "Please select To Organization"),
dueDate: z.string().optional(),
shopDrawingRevisionIds: z.array(z.number()).optional(),
items: z.array(rfaItemSchema).min(1, "At least one item is required"),
shopDrawingRevisionIds: z.array(z.string()).optional(),
asBuiltDrawingRevisionIds: z.array(z.string()).optional(),
});
type RFAFormData = z.infer<typeof rfaSchema>;
type ProjectOption = {
uuid?: string;
id?: number;
projectName?: string;
projectCode?: string;
};
type ContractOption = {
uuid?: string;
id?: number;
contractName?: string;
name?: string;
contractCode?: string;
};
type DisciplineOption = {
id: number;
disciplineCode: string;
codeNameEn?: string;
codeNameTh?: string;
};
type RfaTypeOption = {
id: number;
typeCode?: string;
typeName?: string;
typeNameEn?: string;
typeNameTh?: string;
};
type CorrespondenceTypeOption = {
id: number;
typeCode?: string;
typeName?: string;
};
type OrganizationOption = {
uuid?: string;
id?: number;
organizationCode?: string;
organizationName?: string;
};
type SelectableDrawingOption = {
uuid?: string;
drawingNumber?: string;
title?: string;
legacyDrawingNumber?: string;
currentRevisionUuid?: string;
currentRevision?: {
uuid?: string;
revisionLabel?: string;
revisionNumber?: number | string;
title?: string;
legacyDrawingNumber?: string;
};
};
const extractArrayData = <T,>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== "object" || !("data" in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
const dedupeByKey = <T,>(items: T[], getKey: (item: T) => string | number | undefined): T[] => {
const seen = new Set<string | number>();
return items.filter((item) => {
const key = getKey(item);
if (key === undefined || key === "" || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
};
const getOptionValue = (value?: string | number): string | undefined => {
if (value === undefined || value === null || value === "") {
return undefined;
}
return String(value);
};
export function RFAForm() {
const router = useRouter();
const createMutation = useCreateRFA();
// ADR-019: Dynamic project selection
const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
const projects = projectsData?.data || projectsData || [];
const projects = dedupeByKey(
extractArrayData<ProjectOption>(projectsData),
(project) => project.uuid ?? project.id
);
const { data: organizationsData, isLoading: isLoadingOrganizations } = useOrganizations({ isActive: true });
const organizations = dedupeByKey(
extractArrayData<OrganizationOption>(organizationsData),
(organization) => organization.uuid ?? organization.id
);
const { data: correspondenceTypesData } = useCorrespondenceTypes();
const correspondenceTypes = extractArrayData<CorrespondenceTypeOption>(correspondenceTypesData);
const rfaCorrespondenceType = correspondenceTypes.find(
(type) => type.typeCode?.toUpperCase() === "RFA"
);
const {
register,
control,
handleSubmit,
setValue,
setError,
clearErrors,
watch,
formState: { errors },
} = useForm<RFAFormData>({
@@ -76,26 +185,89 @@ export function RFAForm() {
toOrganizationId: "",
dueDate: "",
shopDrawingRevisionIds: [],
items: [{ itemNo: "1", description: "", quantity: 0, unit: "" }],
asBuiltDrawingRevisionIds: [],
},
});
const selectedProjectId = watch("projectId");
const { data: contracts, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
const contracts = dedupeByKey(
extractArrayData<ContractOption>(contractsData),
(contract) => contract.uuid ?? contract.id
);
const selectedContractId = watch("contractId");
const { data: disciplines, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);
const { data: disciplinesData, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);
const disciplines = dedupeByKey(extractArrayData<DisciplineOption>(disciplinesData), (discipline) => discipline.id);
const { data: rfaTypesData, isLoading: isLoadingRfaTypes } = useRfaTypes(selectedContractId);
const rfaTypes = dedupeByKey(extractArrayData<RfaTypeOption>(rfaTypesData), (rfaType) => rfaType.id);
const [shopDrawingSearch, setShopDrawingSearch] = useState("");
const [shopDrawingPage, setShopDrawingPage] = useState(1);
const { data: shopDrawingsData, isLoading: isLoadingShopDrawings } = useDrawings("SHOP", {
projectUuid: selectedProjectId || "",
search: shopDrawingSearch,
page: shopDrawingPage,
limit: 10,
});
const shopDrawings = dedupeByKey(
extractArrayData<SelectableDrawingOption>(shopDrawingsData),
(drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid
);
const [asBuiltDrawingSearch, setAsBuiltDrawingSearch] = useState("");
const [asBuiltDrawingPage, setAsBuiltDrawingPage] = useState(1);
const { data: asBuiltDrawingsData, isLoading: isLoadingAsBuiltDrawings } = useDrawings("AS_BUILT", {
projectUuid: selectedProjectId || "",
search: asBuiltDrawingSearch,
page: asBuiltDrawingPage,
limit: 10,
});
const asBuiltDrawings = dedupeByKey(
extractArrayData<SelectableDrawingOption>(asBuiltDrawingsData),
(drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid
);
const selectedDisciplineId = watch("disciplineId");
// Watch fields for preview
const rfaTypeId = watch("rfaTypeId");
const disciplineId = watch("disciplineId");
const toOrganizationId = watch("toOrganizationId");
const selectedShopDrawingRevisionIds = watch("shopDrawingRevisionIds") ?? [];
const selectedAsBuiltDrawingRevisionIds = watch("asBuiltDrawingRevisionIds") ?? [];
const selectedRfaType = rfaTypes.find((rfaType) => rfaType.id === rfaTypeId);
const selectedRfaTypeCode = selectedRfaType?.typeCode?.toUpperCase();
const requiresShopDrawings = selectedRfaTypeCode === "DDW" || selectedRfaTypeCode === "SDW";
const requiresAsBuiltDrawings = selectedRfaTypeCode === "ADW";
useEffect(() => {
// Reset page and search when project changes
setShopDrawingPage(1);
setShopDrawingSearch("");
setAsBuiltDrawingPage(1);
setAsBuiltDrawingSearch("");
if (requiresShopDrawings) {
setValue("asBuiltDrawingRevisionIds", []);
clearErrors("asBuiltDrawingRevisionIds");
return;
}
if (requiresAsBuiltDrawings) {
setValue("shopDrawingRevisionIds", []);
clearErrors("shopDrawingRevisionIds");
return;
}
setValue("shopDrawingRevisionIds", []);
setValue("asBuiltDrawingRevisionIds", []);
clearErrors("shopDrawingRevisionIds");
clearErrors("asBuiltDrawingRevisionIds");
}, [requiresShopDrawings, requiresAsBuiltDrawings, selectedProjectId, setValue, clearErrors]);
// -- Preview Logic --
const [preview, setPreview] = useState<{ number: string; isDefaultTemplate: boolean } | null>(null);
useEffect(() => {
if (!rfaTypeId || !disciplineId || !toOrganizationId) {
if (!selectedProjectId || !rfaCorrespondenceType?.id || !rfaTypeId || !disciplineId || !toOrganizationId) {
setPreview(null);
return;
}
@@ -104,11 +276,10 @@ export function RFAForm() {
try {
const res = await correspondenceService.previewNumber({
projectId: selectedProjectId,
typeId: rfaTypeId, // RfaTypeId acts as TypeId
typeId: rfaCorrespondenceType.id,
disciplineId,
// RFA uses 'TO' organization as recipient
recipients: [{ organizationId: toOrganizationId, type: 'TO' }],
dueDate: new Date().toISOString()
subject: watch("subject") || "Preview Subject"
});
setPreview(res);
} catch (err) {
@@ -118,17 +289,32 @@ export function RFAForm() {
const timer = setTimeout(fetchPreview, 500);
return () => clearTimeout(timer);
}, [rfaTypeId, disciplineId, toOrganizationId, selectedProjectId]);
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
}, [rfaTypeId, disciplineId, toOrganizationId, selectedProjectId, rfaCorrespondenceType?.id, watch]);
const onSubmit = (data: RFAFormData) => {
if (requiresShopDrawings && data.shopDrawingRevisionIds?.length === 0) {
setError("shopDrawingRevisionIds", {
type: "manual",
message: "Please select at least one Shop Drawing Revision",
});
return;
}
if (requiresAsBuiltDrawings && data.asBuiltDrawingRevisionIds?.length === 0) {
setError("asBuiltDrawingRevisionIds", {
type: "manual",
message: "Please select at least one As-Built Drawing Revision",
});
return;
}
clearErrors("shopDrawingRevisionIds");
clearErrors("asBuiltDrawingRevisionIds");
const payload: CreateRfaDto = {
...data,
// ADR-019: projectId is already a UUID string from the form
shopDrawingRevisionIds: requiresShopDrawings ? data.shopDrawingRevisionIds : undefined,
asBuiltDrawingRevisionIds: requiresAsBuiltDrawings ? data.asBuiltDrawingRevisionIds : undefined,
};
createMutation.mutate(payload, {
onSuccess: () => {
@@ -137,9 +323,14 @@ export function RFAForm() {
});
};
const onInvalidSubmit: SubmitErrorHandler<RFAFormData> = () => undefined;
const submitForm = handleSubmit(onSubmit, onInvalidSubmit);
const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
void submitForm(event).catch(() => undefined);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-4xl space-y-6">
{/* Preview Section */}
<form onSubmit={handleFormSubmit} className="max-w-4xl space-y-6">
{preview && (
<Card className="p-4 bg-muted border-l-4 border-l-primary">
<p className="text-sm text-muted-foreground mb-1">Document Number Preview</p>
@@ -154,7 +345,6 @@ export function RFAForm() {
</Card>
)}
{/* Basic Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
@@ -184,13 +374,17 @@ export function RFAForm() {
<Input id="description" {...register("description")} placeholder="Enter key description" />
</div>
{/* ADR-019: Project selector */}
<div>
<Label>Project *</Label>
<Select
value={selectedProjectId || undefined}
onValueChange={(val) => {
setValue("projectId", val);
setValue("contractId", ""); // Reset contract when project changes
setValue("contractId", "");
setValue("disciplineId", 0);
setValue("rfaTypeId", 0);
setValue("shopDrawingRevisionIds", []);
setValue("asBuiltDrawingRevisionIds", []);
}}
disabled={isLoadingProjects}
>
@@ -198,11 +392,19 @@ export function RFAForm() {
<SelectValue placeholder={isLoadingProjects ? "Loading..." : "Select Project"} />
</SelectTrigger>
<SelectContent>
{(Array.isArray(projects) ? projects : []).map((p: { uuid: string; projectName?: string; projectCode?: string }) => (
<SelectItem key={p.uuid} value={p.uuid}>
{projects.map((p) => {
const projectValue = getOptionValue(p.uuid ?? p.id);
if (!projectValue) {
return null;
}
return (
<SelectItem key={projectValue} value={projectValue}>
{p.projectName || p.projectCode}
</SelectItem>
))}
);
})}
</SelectContent>
</Select>
{errors.projectId && (
@@ -214,18 +416,33 @@ export function RFAForm() {
<div>
<Label>Contract *</Label>
<Select
onValueChange={(val) => setValue("contractId", val)}
value={selectedContractId || undefined}
onValueChange={(val) => {
setValue("contractId", val);
setValue("disciplineId", 0);
setValue("rfaTypeId", 0);
setValue("shopDrawingRevisionIds", []);
setValue("asBuiltDrawingRevisionIds", []);
}}
disabled={!selectedProjectId || isLoadingContracts}
>
<SelectTrigger>
<SelectValue placeholder={isLoadingContracts ? "Loading..." : "Select Contract"} />
</SelectTrigger>
<SelectContent>
{contracts?.map((c: { uuid: string; contractName?: string; name?: string; contractCode?: string }) => (
<SelectItem key={c.uuid} value={c.uuid}>
{contracts.map((c) => {
const contractValue = getOptionValue(c.uuid ?? c.id);
if (!contractValue) {
return null;
}
return (
<SelectItem key={contractValue} value={contractValue}>
{c.contractName || c.name || c.contractCode}
</SelectItem>
))}
);
})}
</SelectContent>
</Select>
{errors.contractId && (
@@ -236,6 +453,7 @@ export function RFAForm() {
<div>
<Label>Discipline *</Label>
<Select
value={selectedDisciplineId > 0 ? String(selectedDisciplineId) : undefined}
onValueChange={(val) => setValue("disciplineId", Number(val))}
disabled={!selectedContractId || isLoadingDisciplines}
>
@@ -243,12 +461,12 @@ export function RFAForm() {
<SelectValue placeholder={isLoadingDisciplines ? "Loading..." : "Select Discipline"} />
</SelectTrigger>
<SelectContent>
{disciplines?.map((d: { id: number; disciplineCode: string; codeNameEn?: string; codeNameTh?: string }) => (
{disciplines.map((d) => (
<SelectItem key={d.id} value={String(d.id)}>
{d.codeNameEn || d.codeNameTh || d.disciplineCode} ({d.disciplineCode})
{`${d.codeNameEn || d.codeNameTh || d.disciplineCode} (${d.disciplineCode})`}
</SelectItem>
))}
{!isLoadingDisciplines && !disciplines?.length && (
{!isLoadingDisciplines && disciplines.length === 0 && (
<SelectItem value="0" disabled>No disciplines found</SelectItem>
)}
</SelectContent>
@@ -258,96 +476,262 @@ export function RFAForm() {
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>RFA Type *</Label>
<Select
value={rfaTypeId > 0 ? String(rfaTypeId) : undefined}
onValueChange={(val) => {
setValue("rfaTypeId", Number(val));
setValue("shopDrawingRevisionIds", []);
setValue("asBuiltDrawingRevisionIds", []);
}}
disabled={!selectedContractId || isLoadingRfaTypes}
>
<SelectTrigger>
<SelectValue placeholder={isLoadingRfaTypes ? "Loading..." : "Select RFA Type"} />
</SelectTrigger>
<SelectContent>
{rfaTypes.map((rfaType) => (
<SelectItem key={rfaType.id} value={String(rfaType.id)}>
{`${rfaType.typeCode || "RFA"} - ${rfaType.typeName || rfaType.typeNameEn || rfaType.typeNameTh || "Unnamed Type"}`}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.rfaTypeId && (
<p className="text-sm text-destructive mt-1">{errors.rfaTypeId.message}</p>
)}
</div>
<div>
<Label>To Organization *</Label>
<Select
value={toOrganizationId || undefined}
onValueChange={(val) => setValue("toOrganizationId", val)}
disabled={isLoadingOrganizations}
>
<SelectTrigger>
<SelectValue placeholder={isLoadingOrganizations ? "Loading..." : "Select To Organization"} />
</SelectTrigger>
<SelectContent>
{organizations.map((organization) => {
const organizationValue = getOptionValue(organization.uuid ?? organization.id);
if (!organizationValue) {
return null;
}
return (
<SelectItem key={organizationValue} value={organizationValue}>
{`${organization.organizationCode || "ORG"} - ${organization.organizationName || "Unnamed Organization"}`}
</SelectItem>
);
})}
</SelectContent>
</Select>
{errors.toOrganizationId && (
<p className="text-sm text-destructive mt-1">{errors.toOrganizationId.message}</p>
)}
</div>
</div>
</div>
</Card>
{/* RFA Items */}
<Card className="p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">RFA Items</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
append({
itemNo: (fields.length + 1).toString(),
description: "",
quantity: 0,
unit: "",
})
}
>
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>
</div>
{(requiresShopDrawings || requiresAsBuiltDrawings) && (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">New Item</h3>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
{requiresShopDrawings
? "RFA Type นี้ต้องอ้างอิง Shop Drawing Revision อย่างน้อย 1 รายการ"
: "RFA Type นี้ต้องอ้างอิง As-Built Drawing Revision อย่างน้อย 1 รายการ"}
</p>
<div className="space-y-4">
{fields.map((field, index) => (
<Card key={field.id} className="p-4 bg-muted/20">
<div className="flex justify-between items-start mb-3">
<h4 className="font-medium text-sm">Item #{index + 1}</h4>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
{requiresShopDrawings && (
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Input
placeholder="ค้นหาตาม Drawing Number..."
value={shopDrawingSearch}
onChange={(e) => {
setShopDrawingSearch(e.target.value);
setShopDrawingPage(1);
}}
className="max-w-xs"
/>
</div>
{isLoadingShopDrawings && (
<p className="text-sm text-muted-foreground">Loading Shop Drawings...</p>
)}
{!isLoadingShopDrawings && shopDrawings.length === 0 && (
<p className="text-sm text-muted-foreground">No Shop Drawings found for the selected project.</p>
)}
<div className="grid grid-cols-1 gap-3">
{shopDrawings.map((drawing) => {
const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid;
if (!revisionUuid) {
return null;
}
return (
<label
key={revisionUuid}
className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors"
>
<Checkbox
checked={selectedShopDrawingRevisionIds.includes(revisionUuid)}
onCheckedChange={(checked) => {
const nextValues = checked === true
? [...selectedShopDrawingRevisionIds, revisionUuid]
: selectedShopDrawingRevisionIds.filter((value) => value !== revisionUuid);
setValue("shopDrawingRevisionIds", nextValues, { shouldDirty: true, shouldValidate: true });
clearErrors("shopDrawingRevisionIds");
}}
/>
<div className="space-y-1">
<p className="font-medium">{drawing.drawingNumber || "Unnamed Shop Drawing"}</p>
<p className="text-sm text-muted-foreground">{drawing.currentRevision?.title || drawing.title || "Untitled Revision"}</p>
<p className="text-xs text-muted-foreground">
Revision {drawing.currentRevision?.revisionLabel || drawing.currentRevision?.revisionNumber || "-"}
{drawing.currentRevision?.legacyDrawingNumber ? ` • Legacy ${drawing.currentRevision.legacyDrawingNumber}` : ""}
</p>
</div>
</label>
);
})}
</div>
{shopDrawingsData?.meta && shopDrawingsData.meta.totalPages > 1 && (
<div className="flex items-center justify-between mt-4">
<p className="text-xs text-muted-foreground">
Page {shopDrawingPage} of {shopDrawingsData.meta.totalPages}
</p>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
disabled={shopDrawingPage === 1 || isLoadingShopDrawings}
onClick={() => setShopDrawingPage((p) => Math.max(1, p - 1))}
>
Previous
</Button>
<Button
type="button"
variant="outline"
size="sm"
disabled={shopDrawingPage >= shopDrawingsData.meta.totalPages || isLoadingShopDrawings}
onClick={() => setShopDrawingPage((p) => p + 1)}
>
Next
</Button>
</div>
</div>
)}
{errors.shopDrawingRevisionIds && (
<p className="text-sm text-destructive mt-2">{errors.shopDrawingRevisionIds.message}</p>
)}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-12 gap-3">
<div className="md:col-span-2">
<Label className="text-xs">Item No.</Label>
<Input {...register(`items.${index}.itemNo`)} placeholder="1.1" />
{errors.items?.[index]?.itemNo && (
<p className="text-xs text-destructive mt-1">{errors.items[index]?.itemNo?.message}</p>
)}
</div>
<div className="md:col-span-6">
<Label className="text-xs">Description *</Label>
<Input {...register(`items.${index}.description`)} placeholder="Item description" />
{errors.items?.[index]?.description && (
<p className="text-xs text-destructive mt-1">{errors.items[index]?.description?.message}</p>
)}
</div>
<div className="md:col-span-2">
<Label className="text-xs">Quantity</Label>
{requiresAsBuiltDrawings && (
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Input
type="number"
{...register(`items.${index}.quantity`, {
valueAsNumber: true,
})}
placeholder="ค้นหาตาม Drawing Number..."
value={asBuiltDrawingSearch}
onChange={(e) => {
setAsBuiltDrawingSearch(e.target.value);
setAsBuiltDrawingPage(1);
}}
className="max-w-xs"
/>
{errors.items?.[index]?.quantity && (
<p className="text-xs text-destructive mt-1">{errors.items[index]?.quantity?.message}</p>
)}
</div>
<div className="md:col-span-2">
<Label className="text-xs">Unit</Label>
<Input {...register(`items.${index}.unit`)} placeholder="pcs, m3" />
{errors.items?.[index]?.unit && (
<p className="text-xs text-destructive mt-1">{errors.items[index]?.unit?.message}</p>
)}
{isLoadingAsBuiltDrawings && (
<p className="text-sm text-muted-foreground">Loading As-Built Drawings...</p>
)}
{!isLoadingAsBuiltDrawings && asBuiltDrawings.length === 0 && (
<p className="text-sm text-muted-foreground">No As-Built Drawings found for the selected project.</p>
)}
<div className="grid grid-cols-1 gap-3">
{asBuiltDrawings.map((drawing) => {
const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid;
if (!revisionUuid) {
return null;
}
return (
<label
key={revisionUuid}
className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors"
>
<Checkbox
checked={selectedAsBuiltDrawingRevisionIds.includes(revisionUuid)}
onCheckedChange={(checked) => {
const nextValues = checked === true
? [...selectedAsBuiltDrawingRevisionIds, revisionUuid]
: selectedAsBuiltDrawingRevisionIds.filter((value) => value !== revisionUuid);
setValue("asBuiltDrawingRevisionIds", nextValues, { shouldDirty: true, shouldValidate: true });
clearErrors("asBuiltDrawingRevisionIds");
}}
/>
<div className="space-y-1">
<p className="font-medium">{drawing.drawingNumber || "Unnamed As-Built Drawing"}</p>
<p className="text-sm text-muted-foreground">{drawing.currentRevision?.title || drawing.title || "Untitled Revision"}</p>
<p className="text-xs text-muted-foreground">
Revision {drawing.currentRevision?.revisionLabel || drawing.currentRevision?.revisionNumber || "-"}
{drawing.currentRevision?.legacyDrawingNumber ? ` • Legacy ${drawing.currentRevision.legacyDrawingNumber}` : ""}
</p>
</div>
</label>
);
})}
</div>
{asBuiltDrawingsData?.meta && asBuiltDrawingsData.meta.totalPages > 1 && (
<div className="flex items-center justify-between mt-4">
<p className="text-xs text-muted-foreground">
Page {asBuiltDrawingPage} of {asBuiltDrawingsData.meta.totalPages}
</p>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
disabled={asBuiltDrawingPage === 1 || isLoadingAsBuiltDrawings}
onClick={() => setAsBuiltDrawingPage((p) => Math.max(1, p - 1))}
>
Previous
</Button>
<Button
type="button"
variant="outline"
size="sm"
disabled={asBuiltDrawingPage >= asBuiltDrawingsData.meta.totalPages || isLoadingAsBuiltDrawings}
onClick={() => setAsBuiltDrawingPage((p) => p + 1)}
>
Next
</Button>
</div>
</div>
)}
{errors.asBuiltDrawingRevisionIds && (
<p className="text-sm text-destructive mt-2">{errors.asBuiltDrawingRevisionIds.message}</p>
)}
</div>
</Card>
))}
</div>
)}
</div>
</Card>
)}
{errors.items?.root && (
<p className="text-sm text-destructive mt-2">
{errors.items.root.message}
</p>
)}
</Card>
{/* Actions */}
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancel