diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-04 09:36:14 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-04 09:36:14 +0000 |
| commit | 92eda21e45d902663052575aaa4c4f80bfa2faea (patch) | |
| tree | 8483702edf82932d4359a597a854fa8e1b48e94b /components/form-data/update-form-sheet.tsx | |
| parent | f0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (diff) | |
(대표님) 벤더 문서 변경사항, data-table 변경, sync 변경
Diffstat (limited to 'components/form-data/update-form-sheet.tsx')
| -rw-r--r-- | components/form-data/update-form-sheet.tsx | 310 |
1 files changed, 168 insertions, 142 deletions
diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx index abc9bbf3..5666a116 100644 --- a/components/form-data/update-form-sheet.tsx +++ b/components/form-data/update-form-sheet.tsx @@ -32,7 +32,7 @@ import { Popover, PopoverTrigger, PopoverContent, -} from "@/components/ui/popover" +} from "@/components/ui/popover"; import { Command, CommandInput, @@ -40,14 +40,24 @@ import { CommandGroup, CommandItem, CommandEmpty, -} from "@/components/ui/command" +} from "@/components/ui/command"; import { DataTableColumnJSON } from "./form-data-table-columns"; import { updateFormDataInDB } from "@/lib/forms/services"; import { cn } from "@/lib/utils"; -interface UpdateTagSheetProps - extends React.ComponentPropsWithoutRef<typeof Sheet> { +/** ============================================================= + * 🔄 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<typeof Sheet> { open: boolean; onOpenChange: (open: boolean) => void; columns: DataTableColumnJSON[]; @@ -66,74 +76,57 @@ export function UpdateTagSheet({ rowData, formCode, contractItemId, - editableFieldsMap = new Map(), // 기본값 설정 + editableFieldsMap = new Map(), onUpdateSuccess, ...props }: UpdateTagSheetProps) { + // ─────────────────────────────────────────────────────────────── + // hooks & helpers + // ─────────────────────────────────────────────────────────────── const [isPending, startTransition] = React.useTransition(); const router = useRouter(); - // 현재 TAG의 편집 가능한 필드 목록 가져오기 + /* ---------------------------------------------------------------- + * 1️⃣ Editable‑field helpers (unchanged) + * --------------------------------------------------------------*/ const editableFields = React.useMemo(() => { if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { - return []; + return [] as string[]; } return editableFieldsMap.get(rowData.TAG_NO) || []; }, [rowData?.TAG_NO, editableFieldsMap]); - // 필드가 편집 가능한지 판별하는 함수 const isFieldEditable = React.useCallback((column: DataTableColumnJSON) => { - // 1. SHI-only 필드는 편집 불가 - if (column.shi === true) { - return false; - } - - // 2. TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (필요에 따라 수정 가능) - if (column.key === "TAG_NO" || column.key === "TAG_DESC") { - return true; - } + if (column.shi === true) return false; // SHI‑only + if (column.key === "TAG_NO" || column.key === "TAG_DESC") return true; + return editableFields.includes(column.key); + }, [editableFields]); - //3. editableFieldsMap이 있으면 해당 리스트에 있는지 확인 - // if (rowData?.TAG_NO && editableFieldsMap.has(rowData.TAG_NO)) { - // return editableFields.includes(column.key); - // } + const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => !isFieldEditable(column), [isFieldEditable]); - // 4. editableFieldsMap 정보가 없으면 기본적으로 편집 불가 (안전한 기본값) - return true; - }, []); - - // 읽기 전용 필드인지 판별하는 함수 (편집 가능의 반대) - const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => { - return !isFieldEditable(column); - }, [isFieldEditable]); - - // 읽기 전용 사유를 반환하는 함수 const getReadOnlyReason = React.useCallback((column: DataTableColumnJSON) => { - if (column.shi === true) { - return "SHI-only field (managed by SHI system)"; - } - + if (column.shi) return "SHI‑only field (managed by SHI system)"; if (column.key !== "TAG_NO" && column.key !== "TAG_DESC") { if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { return "No editable fields information for this TAG"; } - if (!editableFields.includes(column.key)) { return "Not editable for this TAG class"; } } - - return "Read-only field"; + return "Read‑only field"; }, [rowData?.TAG_NO, editableFieldsMap, editableFields]); - // 1) zod 스키마 + /* ---------------------------------------------------------------- + * 2️⃣ Zod dynamic schema & form state (unchanged) + * --------------------------------------------------------------*/ const dynamicSchema = React.useMemo(() => { - const shape: Record<string, z.ZodType<any>> = {}; + const shape: Record<string, z.ZodTypeAny> = {}; for (const col of columns) { if (col.type === "NUMBER") { shape[col.key] = z .union([z.coerce.number(), z.nan()]) - .transform((val) => (isNaN(val) ? undefined : val)) + .transform((val) => (isNaN(val as number) ? undefined : val)) .optional(); } else { shape[col.key] = z.string().optional(); @@ -142,16 +135,14 @@ export function UpdateTagSheet({ return z.object(shape); }, [columns]); - // 2) form init const form = useForm({ resolver: zodResolver(dynamicSchema), defaultValues: React.useMemo(() => { if (!rowData) return {}; - const defaults: Record<string, any> = {}; - for (const col of columns) { - defaults[col.key] = rowData[col.key] ?? ""; - } - return defaults; + return columns.reduce<Record<string, any>>((acc, col) => { + acc[col.key] = rowData[col.key] ?? ""; + return acc; + }, {}); }, [rowData, columns]), }); @@ -161,28 +152,67 @@ export function UpdateTagSheet({ return; } const defaults: Record<string, any> = {}; - for (const col of columns) { + 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<string, any>) { startTransition(async () => { try { - // 제출 전에 읽기 전용 필드를 원본 값으로 복원 - const finalValues = { ...values }; - for (const col of columns) { + // Restore read‑only fields to their original value before saving + const finalValues: Record<string, any> = { ...values }; + columns.forEach((col) => { if (isFieldReadOnly(col)) { - // 읽기 전용 필드는 원본 값으로 복원 finalValues[col.key] = rowData?.[col.key] ?? ""; } - } + }); const { success, message } = await updateFormDataInDB( formCode, contractItemId, - finalValues + finalValues, ); if (!success) { @@ -190,25 +220,12 @@ export function UpdateTagSheet({ return; } - // Success handling toast.success("Updated successfully!"); - // Create a merged object of original rowData and new values - const updatedData = { - ...rowData, - ...finalValues, - TAG_NO: rowData?.TAG_NO, - }; - - // Call the success callback + const updatedData = { ...rowData, ...finalValues, TAG_NO: rowData?.TAG_NO }; onUpdateSuccess?.(updatedData); - - // Refresh the entire route to get fresh data router.refresh(); - - // Close the sheet onOpenChange(false); - } catch (error) { console.error("Error updating form data:", error); toast.error("An unexpected error occurred while updating"); @@ -216,44 +233,57 @@ export function UpdateTagSheet({ }); } - // 편집 가능한 필드 개수 계산 - const editableFieldCount = React.useMemo(() => { - return columns.filter(col => isFieldEditable(col)).length; - }, [columns, isFieldEditable]); + /* ---------------------------------------------------------------- + * 5️⃣ UI + * --------------------------------------------------------------*/ + const editableFieldCount = React.useMemo(() => columns.filter(isFieldEditable).length, [columns, isFieldEditable]); return ( <Sheet open={open} onOpenChange={onOpenChange} {...props}> <SheetContent className="sm:max-w-xl md:max-w-3xl lg:max-w-4xl xl:max-w-5xl flex flex-col"> <SheetHeader className="text-left"> - <SheetTitle>Update Row - {rowData?.TAG_NO || 'Unknown TAG'}</SheetTitle> + <SheetTitle> + Update Row – {rowData?.TAG_NO || "Unknown TAG"} + </SheetTitle> <SheetDescription> - Modify the fields below and save changes. Fields with <LockIcon className="inline h-3 w-3" /> are read-only. + Modify the fields below and save changes. Fields with + <LockIcon className="inline h-3 w-3 mx-1" /> are read‑only. <br /> <span className="text-sm text-green-600"> - {editableFieldCount} of {columns.length} fields are editable for this TAG. + {editableFieldCount} of {columns.length} fields are editable for + this TAG. </span> </SheetDescription> </SheetHeader> + {/* ────────────────────────────────────────────── */} <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - className="flex flex-col gap-4" - > + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4"> + {/* Scroll wrapper */} <div className="overflow-y-auto max-h-[80vh] flex-1 pr-4 -mr-4"> - <div className="flex flex-col gap-4 pt-2"> - {columns.map((col) => { - const isReadOnly = isFieldReadOnly(col); - const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : ""; - - return ( - <FormField - key={col.key} - control={form.control} - name={col.key} - render={({ field }) => { - switch (col.type) { - case "NUMBER": + {/* ------------------------------------------------------------------ + * Render groups + * ----------------------------------------------------------------*/} + {groupedColumns.map(({ head, cols }) => ( + <div key={head ?? cols[0].key} className="flex flex-col gap-4 pt-2"> + {head && ( + <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide pl-1"> + {head} + </h3> + )} + + {/* Fields inside the group */} + {cols.map((col) => { + const isReadOnly = isFieldReadOnly(col); + const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : ""; + return ( + <FormField + key={col.key} + control={form.control} + name={col.key} + render={({ field }) => { + // ——————————————— Number ———————————————— + if (col.type === "NUMBER") { return ( <FormItem> <FormLabel className="flex items-center"> @@ -272,7 +302,8 @@ export function UpdateTagSheet({ }} value={field.value ?? ""} className={cn( - isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300" + isReadOnly && + "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300", )} /> </FormControl> @@ -284,8 +315,10 @@ export function UpdateTagSheet({ <FormMessage /> </FormItem> ); + } - case "LIST": + // ——————————————— List ———————————————— + if (col.type === "LIST") { return ( <FormItem> <FormLabel className="flex items-center"> @@ -303,12 +336,11 @@ export function UpdateTagSheet({ className={cn( "w-full justify-between", !field.value && "text-muted-foreground", - isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300" + isReadOnly && + "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300", )} > - {field.value - ? col.options?.find((opt) => opt === field.value) - : "Select an option"} + {field.value ? col.options?.find((o) => o === field.value) : "Select an option"} <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> @@ -319,17 +351,11 @@ export function UpdateTagSheet({ <CommandList> <CommandGroup> {col.options?.map((opt) => ( - <CommandItem - key={opt} - value={opt} - onSelect={() => { - field.onChange(opt); - }} - > + <CommandItem key={opt} value={opt} onSelect={() => field.onChange(opt)}> <Check className={cn( "mr-2 h-4 w-4", - field.value === opt ? "opacity-100" : "opacity-0" + field.value === opt ? "opacity-100" : "opacity-0", )} /> {opt} @@ -348,52 +374,52 @@ export function UpdateTagSheet({ <FormMessage /> </FormItem> ); + } - case "STRING": - default: - return ( - <FormItem> - <FormLabel className="flex items-center"> - {col.label} - {isReadOnly && ( - <LockIcon className="ml-1 h-3 w-3 text-gray-400" /> - )} - </FormLabel> - <FormControl> - <Input - readOnly={isReadOnly} - {...field} - className={cn( - isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300" - )} - /> - </FormControl> + // ——————————————— String / default ———————————— + return ( + <FormItem> + <FormLabel className="flex items-center"> + {col.label} {isReadOnly && ( - <FormDescription className="text-xs text-gray-500"> - {readOnlyReason} - </FormDescription> + <LockIcon className="ml-1 h-3 w-3 text-gray-400" /> )} - <FormMessage /> - </FormItem> - ); - } - }} - /> - ); - })} - </div> + </FormLabel> + <FormControl> + <Input + readOnly={isReadOnly} + {...field} + className={cn( + isReadOnly && + "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300", + )} + /> + </FormControl> + {isReadOnly && ( + <FormDescription className="text-xs text-gray-500"> + {readOnlyReason} + </FormDescription> + )} + <FormMessage /> + </FormItem> + ); + }} + /> + ); + })} + </div> + ))} </div> + {/* Footer */} <SheetFooter className="gap-2 pt-2"> <SheetClose asChild> <Button type="button" variant="outline"> Cancel </Button> </SheetClose> - <Button type="submit" disabled={isPending}> - {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />} - Save + {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}Save </Button> </SheetFooter> </form> @@ -401,4 +427,4 @@ export function UpdateTagSheet({ </SheetContent> </Sheet> ); -}
\ No newline at end of file +} |
