From f7f5069a2209cfa39b65f492f32270a5f554bed0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 23 Oct 2025 10:10:21 +0000 Subject: (대표님) EDP 해양 관련 개발 사항들 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data-plant/update-form-sheet.tsx | 445 +++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 components/form-data-plant/update-form-sheet.tsx (limited to 'components/form-data-plant/update-form-sheet.tsx') diff --git a/components/form-data-plant/update-form-sheet.tsx b/components/form-data-plant/update-form-sheet.tsx new file mode 100644 index 00000000..bd75d8f3 --- /dev/null +++ b/components/form-data-plant/update-form-sheet.tsx @@ -0,0 +1,445 @@ +"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; + contractItemId: number; + editableFieldsMap?: Map; // 새로 추가 + /** 업데이트 성공 시 호출될 콜백 */ + onUpdateSuccess?: (updatedValues: Record) => void; +} + +export function UpdateTagSheet({ + open, + onOpenChange, + columns, + rowData, + formCode, + contractItemId, + 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, + contractItemId, + 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 */} + + + + + + +
+ +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3