"use client"; import * as React from "react"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { Check, ChevronsUpDown, Loader, LockIcon } from "lucide-react"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription, } from "@/components/ui/form"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; import { Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, } from "@/components/ui/command"; import { DataTableColumnJSON } from "./form-data-table-columns"; import { updateFormDataInDB } from "@/lib/forms-plant/services"; import { cn } from "@/lib/utils"; import { useParams } from "next/navigation"; import { useTranslation } from "@/i18n/client"; /** ============================================================= * πŸ”„ UpdateTagSheet with grouped fields by `head` property * ----------------------------------------------------------- * - Consecutive columns that share the same `head` value will be * rendered under a section title (the head itself). * - Columns without a head still appear normally. * * NOTE: Only rendering logic is touched – all validation, * read‑only checks, and mutation logic stay the same. * ============================================================*/ interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef { open: boolean; onOpenChange: (open: boolean) => void; columns: DataTableColumnJSON[]; rowData: Record | null; formCode: string; projectCode: string; packageCode: string; editableFieldsMap?: Map; // μƒˆλ‘œ μΆ”κ°€ /** μ—…λ°μ΄νŠΈ 성곡 μ‹œ 호좜될 콜백 */ onUpdateSuccess?: (updatedValues: Record) => void; } export function UpdateTagSheet({ open, onOpenChange, columns, rowData, formCode, projectCode, packageCode, editableFieldsMap = new Map(), onUpdateSuccess, ...props }: UpdateTagSheetProps) { // ─────────────────────────────────────────────────────────────── // hooks & helpers // ─────────────────────────────────────────────────────────────── const [isPending, startTransition] = React.useTransition(); const router = useRouter(); const params = useParams(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "engineering"); /* ---------------------------------------------------------------- * 1️⃣ Editable‑field helpers (unchanged) * --------------------------------------------------------------*/ const editableFields = React.useMemo(() => { if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { return [] as string[]; } return editableFieldsMap.get(rowData.TAG_NO) || []; }, [rowData?.TAG_NO, editableFieldsMap]); const isFieldEditable = React.useCallback((column: DataTableColumnJSON) => { if (column.shi === "OUT" || column.shi === null) return false; // SHI‑only if (column.key === "TAG_NO" || column.key === "TAG_DESC") return false; if (column.key === "status") return false; return editableFields.includes(column.key); // return true }, [editableFields]); const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => !isFieldEditable(column), [isFieldEditable]); const getReadOnlyReason = React.useCallback((column: DataTableColumnJSON) => { if (column.shi === "OUT" || column.shi === null) return t("updateTagSheet.readOnlyReasons.shiOnly"); if (column.key !== "TAG_NO" && column.key !== "TAG_DESC") { if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { return t("updateTagSheet.readOnlyReasons.noEditableFields"); } if (!editableFields.includes(column.key)) { return t("updateTagSheet.readOnlyReasons.notEditableForTag"); } } return t("updateTagSheet.readOnlyReasons.readOnly"); }, [rowData?.TAG_NO, editableFieldsMap, editableFields, t]); /* ---------------------------------------------------------------- * 2️⃣ Zod dynamic schema & form state (unchanged) * --------------------------------------------------------------*/ const dynamicSchema = React.useMemo(() => { const shape: Record = {}; for (const col of columns) { if (col.type === "NUMBER") { shape[col.key] = z .union([z.coerce.number(), z.nan()]) .transform((val) => (isNaN(val as number) ? undefined : val)) .optional(); } else { shape[col.key] = z.string().optional(); } } return z.object(shape); }, [columns]); const form = useForm({ resolver: zodResolver(dynamicSchema), defaultValues: React.useMemo(() => { if (!rowData) return {}; return columns.reduce>((acc, col) => { acc[col.key] = rowData[col.key] ?? ""; return acc; }, {}); }, [rowData, columns]), }); React.useEffect(() => { if (!rowData) { form.reset({}); return; } const defaults: Record = {}; columns.forEach((col) => { defaults[col.key] = rowData[col.key] ?? ""; }); form.reset(defaults); }, [rowData, columns, form]); /* ---------------------------------------------------------------- * 3️⃣ Grouping logic – figure out consecutive columns that share * the same `head` value. This mirrors `groupColumnsByHead` that * you already use for the table view. * --------------------------------------------------------------*/ const groupedColumns = React.useMemo(() => { // Ensure original ordering by `seq` where present const sorted = [...columns].sort((a, b) => { const seqA = a.seq ?? 999999; const seqB = b.seq ?? 999999; return seqA - seqB; }); const groups: { head: string | null; cols: DataTableColumnJSON[] }[] = []; let i = 0; while (i < sorted.length) { const curr = sorted[i]; const head = curr.head?.trim() || null; if (!head) { groups.push({ head: null, cols: [curr] }); i += 1; continue; } // Collect consecutive columns with the same head const cols: DataTableColumnJSON[] = [curr]; let j = i + 1; while (j < sorted.length && sorted[j].head?.trim() === head) { cols.push(sorted[j]); j += 1; } groups.push({ head, cols }); i = j; } return groups; }, [columns]); /* ---------------------------------------------------------------- * 4️⃣ Submission handler (unchanged) * --------------------------------------------------------------*/ async function onSubmit(values: Record) { startTransition(async () => { try { // Restore read‑only fields to their original value before saving const finalValues: Record = { ...values }; columns.forEach((col) => { if (isFieldReadOnly(col)) { finalValues[col.key] = rowData?.[col.key] ?? ""; } }); const { success, message } = await updateFormDataInDB( formCode, projectCode, packageCode, finalValues, ); if (!success) { toast.error(message); return; } toast.success(t("updateTagSheet.messages.updateSuccess")); const updatedData = { ...rowData, ...finalValues, TAG_NO: rowData?.TAG_NO }; onUpdateSuccess?.(updatedData); router.refresh(); onOpenChange(false); } catch (error) { console.error("Error updating form data:", error); toast.error(t("updateTagSheet.messages.updateError")); } }); } /* ---------------------------------------------------------------- * 5️⃣ UI * --------------------------------------------------------------*/ const editableFieldCount = React.useMemo(() => columns.filter(isFieldEditable).length, [columns, isFieldEditable]); return ( {t("updateTagSheet.title")} – {rowData?.TAG_NO || t("updateTagSheet.unknownTag")} {t("updateTagSheet.description")} {t("updateTagSheet.readOnlyIndicator")}
{t("updateTagSheet.editableFieldsCount", { editableCount: editableFieldCount, totalCount: columns.length })}
{/* ────────────────────────────────────────────── */}
{/* Scroll wrapper */}
{/* ------------------------------------------------------------------ * Render groups * ----------------------------------------------------------------*/} {groupedColumns.map(({ head, cols }) => (
{head && (

{head}

)} {/* Fields inside the group */} {cols.map((col) => { const isReadOnly = isFieldReadOnly(col); const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : ""; return ( { // β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Number β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” if (col.type === "NUMBER") { return ( {col.displayLabel || col.label} {isReadOnly && ( )} { const num = parseFloat(e.target.value); field.onChange(isNaN(num) ? "" : num); }} value={field.value ?? ""} className={cn( isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300", )} /> {isReadOnly && ( {readOnlyReason} )} ); } // β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” List β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” if (col.type === "LIST") { return ( {col.label} {isReadOnly && ( )} {t("updateTagSheet.noOptionFound")} {col.options?.map((opt) => ( field.onChange(opt)}> {opt} ))} {isReadOnly && ( {readOnlyReason} )} ); } // β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” String / default β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” return ( {col.label} {isReadOnly && ( )} {isReadOnly && ( {readOnlyReason} )} ); }} /> ); })}
))}
{/* Footer */}
); }