"use client" import * as React from "react" import { useRouter, useParams } from "next/navigation" import { useForm, useWatch, useFieldArray } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" 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 { useTranslation } from "@/i18n/client" import type { CreateTagSchema } from "@/lib/tags/validations" import { createTagSchema } from "@/lib/tags/validations" import { createTag, getSubfieldsByTagType, getClassOptions, type ClassOption, TagTypeOption, } from "@/lib/tags-plant/service" import { ScrollArea } from "@/components/ui/scroll-area" // Updated to support multiple rows and subclass interface MultiTagFormValues { class: string; tagType: string; subclass: string; // 새로 추가된 서브클래스 필드 rows: Array<{ [key: string]: string; tagNo: string; description: string; }>; } // SubFieldDef for clarity interface SubFieldDef { name: string label: string type: "select" | "text" options?: { value: string; label: string }[] expression?: string delimiter?: string } // 업데이트된 클래스 옵션 인터페이스 (서브클래스 정보 포함) interface UpdatedClassOption extends ClassOption { tagTypeCode: string tagTypeDescription?: string subclasses: { id: string; desc: string; }[] // 서브클래스 배열 추가 subclassRemark: Record // 서브클래스 리마크 추가 } interface AddTagDialogProps { projectCode: string packageCode: string } export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { const router = useRouter() const params = useParams() const lng = (params?.lng as string) || "ko" const { t } = useTranslation(lng, "engineering") const [open, setOpen] = React.useState(false) const [tagTypeList, setTagTypeList] = React.useState([]) const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState(null) const [selectedClassOption, setSelectedClassOption] = React.useState(null) const [selectedSubclass, setSelectedSubclass] = React.useState("") const [subFields, setSubFields] = React.useState([]) const [classOptions, setClassOptions] = React.useState([]) const [classSearchTerm, setClassSearchTerm] = React.useState("") const [isLoadingClasses, setIsLoadingClasses] = React.useState(false) const [isLoadingSubFields, setIsLoadingSubFields] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) // ID management const selectIdRef = React.useRef(0) const getUniqueSelectId = React.useCallback(() => `select-${selectIdRef.current++}`, []) const fieldIdsRef = React.useRef>({}) const classOptionIdsRef = React.useRef>({}) // --------------- // Load Class Options (서브클래스 정보 포함) // --------------- React.useEffect(() => { const loadClassOptions = async () => { setIsLoadingClasses(true) try { // getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정 const result = await getClassOptions(packageCode, projectCode) setClassOptions(result) } catch (err) { toast.error(t("toast.classOptionsLoadFailed")) } finally { setIsLoadingClasses(false) } } if (open) { loadClassOptions() } }, [open, projectCode, packageCode]) // --------------- // react-hook-form with fieldArray support for multiple rows // --------------- const form = useForm({ defaultValues: { tagType: "", class: "", subclass: "", // 서브클래스 필드 추가 rows: [{ tagNo: "", description: "" }] }, }) const { fields, append, remove } = useFieldArray({ control: form.control, name: "rows" }) // --------------- // 서브클래스별로 필터링된 서브필드 로드 // --------------- async function loadFilteredSubFieldsByTagTypeCode(tagTypeCode: string, subclassRemark: string, subclass: string) { setIsLoadingSubFields(true) try { // 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가) const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, subclassRemark, subclass) 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(t("toast.subfieldsLoadFailed")) setSubFields([]) return false } finally { setIsLoadingSubFields(false) } } // --------------- // Handle class selection // --------------- async function handleSelectClass(classOption: UpdatedClassOption) { form.setValue("class", classOption.label) form.setValue("subclass", "") // 서브클래스 초기화 setSelectedClassOption(classOption) setSelectedSubclass("") if (classOption.tagTypeCode) { setSelectedTagTypeCode(classOption.tagTypeCode) // If you have tagTypeList, you can find the label const tagType = tagTypeList.find(t => t.id === classOption.tagTypeCode) if (tagType) { form.setValue("tagType", tagType.label) } else if (classOption.tagTypeDescription) { form.setValue("tagType", classOption.tagTypeDescription) } // 서브클래스가 있으면 서브필드 로딩을 하지 않고 대기 if (classOption.subclasses && classOption.subclasses.length > 0) { setSubFields([]) // 서브클래스 선택을 기다림 } else { // 서브클래스가 없으면 바로 서브필드 로딩 await loadFilteredSubFieldsByTagTypeCode(classOption.tagTypeCode, "", "") } } } // --------------- // Handle subclass selection // --------------- async function handleSelectSubclass(subclassCode: string) { if (!selectedClassOption || !selectedTagTypeCode) return setSelectedSubclass(subclassCode) form.setValue("subclass", subclassCode) // 선택된 서브클래스의 리마크 값 가져오기 const subclassRemarkValue = selectedClassOption.subclassRemark[subclassCode] || "" // 리마크 값으로 필터링된 서브필드 로드 await loadFilteredSubFieldsByTagTypeCode(selectedTagTypeCode, subclassRemarkValue, subclassCode) } // --------------- // 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 (!projectCode) { toast.error(t("toast.noSelectedPackageId")); return; } setIsSubmitting(true); try { const successfulTags = []; const failedTags = []; // Process each row for (const row of data.rows) { // Create tag data from the row and shared class/tagType/subclass const tagData: CreateTagSchema = { tagType: data.tagType, class: data.class, subclass: data.subclass, // 서브클래스 정보 추가 tagNo: row.tagNo, description: row.description, ...Object.fromEntries( subFields.map(field => [field.name, row[field.name] || ""]) ), // Add any required default fields from the original form functionCode: row.functionCode || "", seqNumber: row.seqNumber || "", valveAcronym: row.valveAcronym || "", processUnit: row.processUnit || "", }; try { const res = await createTag(tagData, projectCode, packageCode); if ("error" in res) { console.log(res) failedTags.push({ tag: row.tagNo, error: res.error }); } else { successfulTags.push(row.tagNo); } } catch (err) { failedTags.push({ tag: row.tagNo, error: "Unknown error" }); } } // Show results to the user if (successfulTags.length > 0) { toast.success(`${successfulTags.length}${t("toast.tagsCreatedSuccess")}`); } if (failedTags.length > 0) { console.log("Failed tags:", failedTags); toast.error(`${failedTags.length}${t("toast.tagsCreateFailed")}`); } // Refresh the page router.refresh(); // Reset the form and close dialog if all successful if (failedTags.length === 0) { form.reset(); setOpen(false); } } catch (err) { toast.error(t("toast.tagProcessingFailed")); } 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); 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 }; newRow.tagNo = ""; append(newRow); 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)}`, [] ) return ( {t("labels.class")} { e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }}> {t("messages.noSearchResults")} {classOptions.map((opt, optIndex) => { if (!classOptionIdsRef.current[opt.code]) { classOptionIdsRef.current[opt.code] = `class-${opt.code}-${Date.now()}-${Math.random() .toString(36) .slice(2, 9)}` } const optionId = classOptionIdsRef.current[opt.code] return ( { field.onChange(opt.label) setPopoverOpen(false) handleSelectClass(opt) }} value={opt.label} className="truncate" title={opt.label} > {opt.label} ) })} ) } // --------------- // Render Subclass field (새로 추가) // --------------- function renderSubclassField(field: any) { const hasSubclasses = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 if (!hasSubclasses) { return null } return ( Item Class ) } // --------------- // Render TagType field (readonly after class selection) // --------------- function renderTagTypeField(field: any) { const isReadOnly = !!selectedTagTypeCode const inputId = React.useMemo( () => `tag-type-input-${isReadOnly ? "readonly" : "editable"}-${Date.now()}-${Math.random() .toString(36) .slice(2, 9)}`, [isReadOnly] ) const width = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 ? "w-1/3" : "w-2/3" return ( {t("labels.tagType")} {isReadOnly ? (
) : ( )}
) } // --------------- // Render the table of subfields // --------------- function renderTagTable() { if (isLoadingSubFields) { return (
{t("messages.loadingFields")}
) } if (subFields.length === 0 && selectedTagTypeCode) { const message = selectedClassOption?.subclasses && selectedClassOption.subclasses.length > 0 ? t("messages.selectSubclassFirst") : t("messages.noFieldsForTagType") return (
{message}
) } 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 (!open) { fieldIdsRef.current = {} classOptionIdsRef.current = {} selectIdRef.current = 0 } }, [open]) return ( { if (!o) { form.reset({ tagType: "", class: "", subclass: "", rows: [{ tagNo: "", description: "" }] }); setSelectedTagTypeCode(null); setSelectedClassOption(null); setSelectedSubclass(""); setSubFields([]); } setOpen(o); }} > {t("dialogs.addTag")} {t("dialogs.selectClassToLoadFields")}
{/* 클래스, 서브클래스, 태그 유형 선택 */}
renderClassField(field)} /> renderSubclassField(field)} /> renderTagTypeField(field)} />
{/* 태그 테이블 */} {renderTagTable()} {/* 버튼 */}
) }