"use client" import * as React from "react" import { useParams, useRouter } from "next/navigation"; import { useForm, useFieldArray } from "react-hook-form" import { toast } from "sonner" import { Loader2, ChevronsUpDown, Check, Plus, Trash2, Copy } from "lucide-react" import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Form, FormField, FormItem, FormControl, FormLabel, FormMessage, } from "@/components/ui/form" import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover" import { Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, } from "@/components/ui/command" import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { createTagInForm } from "@/lib/tags/service" import { getFormTagTypeMappings, getTagTypeByDescription, getSubfieldsByTagTypeForForm } from "@/lib/forms/services" import { useTranslation } from "@/i18n/client"; // Form-specific tag mapping interface interface FormTagMapping { id: number; tagTypeLabel: string; classLabel: string; formCode: string; formName: string; remark?: string | null; } // Updated to support multiple rows interface MultiTagFormValues { class: string; tagType: string; rows: Array<{ [key: string]: string; tagNo: string; description: string; }>; } // SubFieldDef for clarity interface SubFieldDef { name: string; label: string; type: string; options?: { value: string; label: string }[]; expression?: string; delimiter?: string; } interface AddFormTagDialogProps { projectId: number; formCode: string; formName?: string; contractItemId: number; packageCode: string; open?: boolean; onOpenChange?: (open: boolean) => void; } export function AddFormTagDialog({ projectId, formCode, formName, contractItemId, packageCode, open: externalOpen, onOpenChange: externalOnOpenChange }: AddFormTagDialogProps) { const router = useRouter() const params = useParams(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "engineering"); // Use external control if provided, otherwise use internal state const [internalOpen, setInternalOpen] = React.useState(false); const isOpen = externalOpen !== undefined ? externalOpen : internalOpen; const setIsOpen = externalOnOpenChange || setInternalOpen; const [mappings, setMappings] = React.useState([]) const [selectedTagTypeLabel, setSelectedTagTypeLabel] = React.useState(null) const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState(null) const [subFields, setSubFields] = React.useState([]) const [isLoadingClasses, setIsLoadingClasses] = React.useState(false) const [isLoadingSubFields, setIsLoadingSubFields] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) // ID management for React keys const selectIdRef = React.useRef(0) const fieldIdsRef = React.useRef>({}) const classOptionIdsRef = React.useRef>({}) // --------------- // Load Form Tag Mappings // --------------- React.useEffect(() => { const loadMappings = async () => { if (!formCode || !projectId) return; setIsLoadingClasses(true); try { const result = await getFormTagTypeMappings(formCode, projectId); // Type safety casting const typedMappings: FormTagMapping[] = result.map(item => ({ id: item.id, tagTypeLabel: item.tagTypeLabel, classLabel: item.classLabel, formCode: item.formCode, formName: item.formName, remark: item.remark })); setMappings(typedMappings); } catch (err) { toast.error("폼 태그 매핑 로드에 실패했습니다."); } finally { setIsLoadingClasses(false); } }; loadMappings(); }, [formCode, projectId]); // Load mappings when dialog opens React.useEffect(() => { if (isOpen) { const loadMappings = async () => { if (!formCode || !projectId) return; setIsLoadingClasses(true); try { const result = await getFormTagTypeMappings(formCode, projectId); // Type safety casting const typedMappings: FormTagMapping[] = result.map(item => ({ id: item.id, tagTypeLabel: item.tagTypeLabel, classLabel: item.classLabel, formCode: item.formCode, formName: item.formName, remark: item.remark })); setMappings(typedMappings); } catch (err) { toast.error("폼 태그 매핑 로드에 실패했습니다."); } finally { setIsLoadingClasses(false); } }; loadMappings(); } }, [isOpen, formCode, projectId]); // --------------- // react-hook-form with fieldArray support for multiple rows // --------------- const form = useForm({ defaultValues: { tagType: "", class: "", rows: [{ tagNo: "", description: "" }] }, }); const { fields, append, remove } = useFieldArray({ control: form.control, name: "rows" }); // --------------- // Load subfields by TagType code // --------------- async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { setIsLoadingSubFields(true); try { const { subFields: apiSubFields } = await getSubfieldsByTagTypeForForm(tagTypeCode, projectId); const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, type: field.type, options: field.options || [], expression: field.expression ?? undefined, delimiter: field.delimiter ?? undefined, })); setSubFields(formattedSubFields); // Initialize the rows with these subfields const currentRows = form.getValues("rows"); const updatedRows = currentRows.map(row => { const newRow = { ...row }; formattedSubFields.forEach(field => { if (!newRow[field.name]) { newRow[field.name] = ""; } }); return newRow; }); form.setValue("rows", updatedRows); return true; } catch (err) { toast.error("서브필드를 불러오는데 실패했습니다."); setSubFields([]); return false; } finally { setIsLoadingSubFields(false); } } // --------------- // Handle class selection // --------------- async function handleSelectClass(classLabel: string) { form.setValue("class", classLabel); // Find the mapping for this class const mapping = mappings.find(m => m.classLabel === classLabel); if (mapping) { setSelectedTagTypeLabel(mapping.tagTypeLabel); form.setValue("tagType", mapping.tagTypeLabel); // Get the tagTypeCode for this tagTypeLabel try { const tagType = await getTagTypeByDescription(mapping.tagTypeLabel, projectId); if (tagType) { setSelectedTagTypeCode(tagType.code); await loadSubFieldsByTagTypeCode(tagType.code); } else { toast.error("선택한 태그 유형을 찾을 수 없습니다."); } } catch (error) { toast.error("태그 유형 정보를 불러오는데 실패했습니다."); } } } // --------------- // Build TagNo from subfields automatically for each row // --------------- React.useEffect(() => { if (subFields.length === 0) { return; } const subscription = form.watch((value) => { if (!value.rows || subFields.length === 0) { return; } const rows = [...value.rows]; rows.forEach((row, rowIndex) => { if (!row) return; let combined = ""; subFields.forEach((sf, idx) => { const fieldValue = row[sf.name] || ""; // delimiter를 앞에 붙이기 (첫 번째 필드가 아니고, 현재 필드에 값이 있고, delimiter가 있는 경우) if (idx > 0 && fieldValue && sf.delimiter) { combined += sf.delimiter; } combined += fieldValue; }); const currentTagNo = form.getValues(`rows.${rowIndex}.tagNo`); if (currentTagNo !== combined) { form.setValue(`rows.${rowIndex}.tagNo`, combined, { shouldDirty: true, shouldTouch: true, shouldValidate: true, }); } }); }); return () => subscription.unsubscribe(); }, [subFields, form]); // --------------- // Check if tag numbers are valid // --------------- const areAllTagNosValid = React.useMemo(() => { const rows = form.getValues("rows"); return rows.every(row => { const tagNo = row.tagNo; return tagNo && tagNo.trim() !== "" && !tagNo.includes("??"); }); }, [form.watch()]); // --------------- // Submit handler for multiple tags // --------------- async function onSubmit(data: MultiTagFormValues) { if (!contractItemId || !projectId) { toast.error("필요한 정보가 없습니다."); return; } setIsSubmitting(true); try { const successfulTags = []; const failedTags = []; // Process each row for (const row of data.rows) { // Create tag data const tagData = { tagType: data.tagType, class: data.class, tagNo: row.tagNo, description: row.description, ...Object.fromEntries( subFields.map(field => [field.name, row[field.name] || ""]) ), functionCode: row.functionCode || "", seqNumber: row.seqNumber || "", valveAcronym: row.valveAcronym || "", processUnit: row.processUnit || "", }; try { const res = await createTagInForm(tagData, contractItemId, formCode, packageCode); if (res && "error" in res) { failedTags.push({ tag: row.tagNo, error: res.error }); } else if (res && res.success) { successfulTags.push(row.tagNo); } else { // 예상치 못한 응답 처리 console.error("Unexpected response:", res); failedTags.push({ tag: row.tagNo, error: "Unexpected response format" }); } } catch (err) { failedTags.push({ tag: row.tagNo, error: "Unknown error" }); } } // Show results to the user if (successfulTags.length > 0) { toast.success(`${successfulTags.length}개의 태그가 성공적으로 생성되었습니다!`); } if (failedTags.length > 0) { console.log("Failed tags:", failedTags); // 전체 에러 메시지 표시 const errorMessage = failedTags .map(f => `${f.tag}: ${f.error}`) .join('\n'); toast.error(

{failedTags.length}개의 태그 생성 실패:

    {failedTags.map((f, idx) => (
  • • {f.tag}: {f.error}
  • ))}
); } // Refresh the page router.refresh(); // Reset the form and close dialog if all successful if (failedTags.length === 0) { form.reset(); setIsOpen(false); } } catch (err) { toast.error("태그 생성 처리에 실패했습니다."); } finally { setIsSubmitting(false); } } // --------------- // Add a new row // --------------- function addRow() { const newRow: { tagNo: string; description: string; [key: string]: string; } = { tagNo: "", description: "" }; // Add all subfields with empty values subFields.forEach(field => { newRow[field.name] = ""; }); append(newRow); // Force form validation after row is added setTimeout(() => form.trigger(), 0); } // --------------- // Duplicate row // --------------- function duplicateRow(index: number) { const rowToDuplicate = form.getValues(`rows.${index}`); const newRow: { tagNo: string; description: string; [key: string]: string; } = { ...rowToDuplicate }; // Clear the tagNo field as it will be auto-generated newRow.tagNo = ""; append(newRow); // Force form validation after row is duplicated setTimeout(() => form.trigger(), 0); } // --------------- // Render Class field // --------------- function renderClassField(field: any) { const [popoverOpen, setPopoverOpen] = React.useState(false) const buttonId = React.useMemo( () => `class-button-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, [] ) const popoverContentId = React.useMemo( () => `class-popover-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, [] ) const commandId = React.useMemo( () => `class-command-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, [] ) // Get unique class labels from mappings const classOptions = Array.from(new Set(mappings.map(m => m.classLabel))); return ( {t("labels.class")} {t("messages.noSearchResults")} {classOptions.map((className, optIndex) => { if (!classOptionIdsRef.current[className]) { classOptionIdsRef.current[className] = `class-${className}-${Date.now()}-${Math.random() .toString(36) .slice(2, 9)}` } const optionId = classOptionIdsRef.current[className] return ( { field.onChange(className) setPopoverOpen(false) handleSelectClass(className) }} value={className} className="truncate" title={className} > {className} ) })} ) } // --------------- // Render TagType field (readonly after class selection) // --------------- function renderTagTypeField(field: any) { const isReadOnly = !!selectedTagTypeLabel const inputId = React.useMemo( () => `tag-type-input-${isReadOnly ? "readonly" : "editable"}-${Date.now()}-${Math.random() .toString(36) .slice(2, 9)}`, [isReadOnly] ) return ( {t("labels.tagType")} {isReadOnly ? (
) : ( )}
) } // --------------- // Render the table of subfields // --------------- function renderTagTable() { if (isLoadingSubFields) { return (
{t("messages.loadingFields")}
) } if (subFields.length === 0 && selectedTagTypeCode) { return (
{t("messages.noFieldsForTagType")}
) } if (subFields.length === 0) { return (
{t("messages.selectClassFirst")}
) } return (
{/* 헤더 */}

{t("sections.tagItems")} ({fields.length}개)

{!areAllTagNosValid && ( {t("messages.invalidTagsExist")} )}
{/* 테이블 컨테이너 - 가로/세로 스크롤 모두 적용 */}
#
{t("labels.tagNo")}
{t("labels.description")}
{/* Subfields */} {subFields.map((field, fieldIndex) => (
{field.label}
{field.expression && (
{field.expression}
)}
))} {t("labels.actions")}
{fields.map((item, rowIndex) => ( {/* Row number */} {rowIndex + 1} {/* Tag No cell */} (
{field.value?.includes("??") && (
!
)}
)} />
{/* Description cell */} ( )} /> {/* Subfield cells */} {subFields.map((sf, sfIndex) => ( ( {sf.type === "select" ? ( ) : ( )} )} /> ))} {/* Actions cell */}

{t("tooltips.duplicateRow")}

{t("tooltips.deleteRow")}

))}
{/* 행 추가 버튼 */}
); } // --------------- // Reset IDs/states when dialog closes // --------------- React.useEffect(() => { if (!isOpen) { fieldIdsRef.current = {} classOptionIdsRef.current = {} selectIdRef.current = 0 } }, [isOpen]) return ( { if (!o) { form.reset({ tagType: "", class: "", rows: [{ tagNo: "", description: "" }] }); setSelectedTagTypeLabel(null); setSelectedTagTypeCode(null); setSubFields([]); } setIsOpen(o); }} > {/* Only show the trigger if external control is not being used */} {externalOnOpenChange === undefined && ( )} {t("dialogs.addFormTag")} - {formName || formCode} {t("dialogs.selectClassToLoadFields")}
{/* 클래스 및 태그 유형 선택 */}
renderClassField(field)} /> renderTagTypeField(field)} />
{/* 태그 테이블 */} {renderTagTable()} {/* 버튼 */}
) }