From 969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 14 Aug 2025 11:54:47 +0000 Subject: (대표님) 정규벤더등록, 벤더문서관리, 벤더데이터입력, 첨부파일관리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ship-vendor-document/edit-revision-dialog.tsx | 726 +++++++++++++++++++++ 1 file changed, 726 insertions(+) create mode 100644 components/ship-vendor-document/edit-revision-dialog.tsx (limited to 'components/ship-vendor-document/edit-revision-dialog.tsx') diff --git a/components/ship-vendor-document/edit-revision-dialog.tsx b/components/ship-vendor-document/edit-revision-dialog.tsx new file mode 100644 index 00000000..313a27bc --- /dev/null +++ b/components/ship-vendor-document/edit-revision-dialog.tsx @@ -0,0 +1,726 @@ +"use client" + +import React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + Edit, + FileText, + Loader2, + AlertTriangle, + Trash2, + CheckCircle, + Clock +} from "lucide-react" +import { toast } from "sonner" +import { updateRevisionAction, deleteRevisionAction } from "@/lib/vendor-document-list/enhanced-document-service" // ✅ 서버 액션 import + +/* ------------------------------------------------------------------------------------------------- + * Schema & Types + * -----------------------------------------------------------------------------------------------*/ + +interface RevisionInfo { + id: number + issueStageId: number + revision: string + uploaderType: string + uploaderId: number | null + uploaderName: string | null + comment: string | null + usage: string | null + usageType: string | null + revisionStatus: string + submittedDate: string | null + approvedDate: string | null + uploadedAt: string | null + reviewStartDate: string | null + rejectedDate: string | null + reviewerId: number | null + reviewerName: string | null + reviewComments: string | null + createdAt: Date + updatedAt: Date + stageName?: string + attachments: AttachmentInfo[] +} + +interface AttachmentInfo { + id: number + revisionId: number + fileName: string + filePath: string + dolceFilePath: string | null + fileSize: number | null + fileType: string | null + createdAt: Date + updatedAt: Date +} + +// drawingKind에 따른 동적 스키마 생성 (수정용) +const createEditRevisionSchema = (drawingKind: string) => { + const baseSchema = { + usage: z.string().min(1, "Please select a usage"), + revision: z.string().min(1, "Please enter a revision").max(50, "Revision must be 50 characters or less"), // ✅ revision 필드 추가 + comment: z.string().optional(), + } + + // B3인 경우에만 usageType 필드 추가 + if (drawingKind === 'B3') { + return z.object({ + ...baseSchema, + usageType: z.string().min(1, "Please select a usage type"), + }) + } else { + return z.object({ + ...baseSchema, + usageType: z.string().optional(), + }) + } +} + +// drawingKind에 따른 용도 옵션 +const getUsageOptions = (drawingKind: string) => { + switch (drawingKind) { + case 'B3': + return [ + { value: "Approval", label: "Approval" }, + { value: "Working", label: "Working" }, + { value: "Comments", label: "Comments" }, + ] + case 'B4': + return [ + { value: "Pre", label: "Pre" }, + { value: "Working", label: "Working" }, + ] + case 'B5': + return [ + { value: "Pre", label: "Pre" }, + { value: "Working", label: "Working" }, + ] + default: + return [ + { value: "Pre", label: "Pre" }, + { value: "Working", label: "Working" }, + ] + } +} + +// B3 전용 용도 타입 옵션 +const getUsageTypeOptions = (usage: string) => { + switch (usage) { + case 'Approval': + return [ + { value: "Full", label: "Full" }, + { value: "Partial", label: "Partial" }, + ] + case 'Working': + return [ + { value: "Full", label: "Full" }, + { value: "Partial", label: "Partial" }, + ] + case 'Comments': + return [ + { value: "Comments", label: "Comments" }, + ] + default: + return [] + } +} + +// ✅ 리비전 가이드 생성 (NewRevisionDialog와 동일) +const getRevisionGuide = () => { + return "Enter in R01, R02, R03... format" +} + +interface EditRevisionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + revision: RevisionInfo | null + drawingKind?: string + onSuccess: (action: 'update' | 'delete', result?: any) => void +} + +/* ------------------------------------------------------------------------------------------------- + * Revision Info Display Component + * -----------------------------------------------------------------------------------------------*/ +function RevisionInfoDisplay({ revision }: { revision: RevisionInfo }) { + const canEdit = React.useMemo(() => { + if (!revision.attachments || revision.attachments.length === 0) { + return true + } + return revision.attachments.every(attachment => + !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' + ) + }, [revision.attachments]) + + const processedCount = revision.attachments?.filter(attachment => + attachment.dolceFilePath && attachment.dolceFilePath.trim() !== '' + ).length || 0 + + const getStatusColor = (status: string) => { + switch (status) { + case 'APPROVED': return 'bg-green-100 text-green-800' + case 'UPLOADED': return 'bg-blue-100 text-blue-800' + case 'REJECTED': return 'bg-red-100 text-red-800' + default: return 'bg-gray-100 text-gray-800' + } + } + + return ( +
+
+
+ + {revision.revision} + + + {revision.revisionStatus} + + {!canEdit && ( + + Partially Processed + + )} +
+ +
+ + {revision.attachments?.length || 0} file(s) + {processedCount > 0 && ( + + ({processedCount} processed) + + )} +
+
+ +
+
+ Uploader: + {revision.uploaderName || '-'} +
+
+ Upload Date: + + {revision.uploadedAt + ? new Date(revision.uploadedAt).toLocaleDateString() + : '-' + } + +
+
+ + {revision.comment && ( +
+ Current Comment: +

+ {revision.comment} +

+
+ )} + + {!canEdit && ( +
+ + + Some files have been processed. Editing is limited. + +
+ )} +
+ ) +} + +/* ------------------------------------------------------------------------------------------------- + * Main Dialog Component + * -----------------------------------------------------------------------------------------------*/ +export function EditRevisionDialog({ + open, + onOpenChange, + revision, + drawingKind = 'B4', + onSuccess +}: EditRevisionDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [isDeleting, setIsDeleting] = React.useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false) + + // drawingKind에 따른 동적 스키마 및 옵션 생성 + const editRevisionSchema = React.useMemo(() => createEditRevisionSchema(drawingKind), [drawingKind]) + const usageOptions = React.useMemo(() => getUsageOptions(drawingKind), [drawingKind]) + const showUsageType = drawingKind === 'B3' + + type EditRevisionSchema = z.infer + + const form = useForm({ + resolver: zodResolver(editRevisionSchema), + defaultValues: { + usage: "", + revision: "", // ✅ revision 기본값 추가 + comment: "", + usageType: showUsageType ? "" : undefined, + }, + }) + + const watchedUsage = form.watch("usage") + + // 용도 선택에 따른 용도 타입 옵션 업데이트 + const usageTypeOptions = React.useMemo(() => { + if (drawingKind === 'B3' && watchedUsage) { + return getUsageTypeOptions(watchedUsage) + } + return [] + }, [drawingKind, watchedUsage]) + + // ✅ 리비전 가이드 텍스트 + const revisionGuide = React.useMemo(() => { + return getRevisionGuide() + }, []) + + // revision이 변경될 때 폼 데이터 초기화 + React.useEffect(() => { + if (revision) { + form.reset({ + usage: revision.usage || "", + revision: revision.revision || "", // ✅ revision 값 설정 + comment: revision.comment || "", + usageType: showUsageType ? (revision.usageType || "") : undefined, + }) + } + }, [revision, showUsageType, form]) + + // 용도 변경 시 용도 타입 초기화 또는 자동 설정 (NewRevisionDialog와 동일한 로직) + React.useEffect(() => { + if (showUsageType && watchedUsage) { + if (watchedUsage === "Comments") { + form.setValue("usageType", "Comments") + } else { + // Comments가 아닌 경우, 초기 로드가 아니라면 초기화 + const currentValue = form.getValues("usageType") + if (revision && watchedUsage !== revision.usage) { + form.setValue("usageType", "") + } + } + } + }, [watchedUsage, showUsageType, form, revision]) + + // 수정 가능 여부 확인 + const canEdit = React.useMemo(() => { + if (!revision?.attachments || revision.attachments.length === 0) { + return true + } + return revision.attachments.every(attachment => + !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' + ) + }, [revision?.attachments]) + + // 폼 변경 체크 + const hasChanges = React.useMemo(() => { + if (!revision) return false + const currentValues = form.getValues() + return ( + currentValues.comment !== (revision.comment || '') || + currentValues.usage !== (revision.usage || '') || + currentValues.revision !== (revision.revision || '') || // ✅ revision 변경 체크 추가 + (showUsageType && currentValues.usageType !== (revision.usageType || '')) + ) + }, [revision, form, showUsageType]) + + const handleDialogClose = () => { + if (!isLoading && !isDeleting) { + setShowDeleteConfirm(false) + form.reset() + onOpenChange(false) + } + } + + const onSubmit = async (data: EditRevisionSchema) => { + if (!revision || !canEdit) { + toast.error("Cannot edit this revision") + return + } + + setIsLoading(true) + + try { + // ✅ 서버 액션 호출 - revision 필드 추가 + const result = await updateRevisionAction({ + revisionId: revision.id, + revision: data.revision.trim(), // ✅ revision 추가 + comment: data.comment?.trim() || null, + usage: data.usage.trim(), + usageType: showUsageType && 'usageType' in data ? data.usageType?.trim() || null : null, + }) + + if (!result.success) { + throw new Error(result.error || 'Failed to update revision') + } + + toast.success( + result.message || + `Revision ${data.revision} updated successfully` // ✅ 새 revision 값 사용 + ) + + setTimeout(() => { + handleDialogClose() + onSuccess('update', result) + }, 1000) + + } catch (error) { + console.error('❌ Update error:', error) + toast.error(error instanceof Error ? error.message : "An error occurred during update") + } finally { + setIsLoading(false) + } + } + + const handleDelete = async () => { + if (!revision || !canEdit) { + toast.error("Cannot delete this revision") + return + } + + setIsDeleting(true) + + try { + // ✅ 서버 액션 호출 + const result = await deleteRevisionAction({ + revisionId: revision.id, + }) + + if (!result.success) { + throw new Error(result.error || 'Failed to delete revision') + } + + toast.success( + result.message || + `Revision ${revision.revision} deleted successfully` + ) + + setTimeout(() => { + handleDialogClose() + onSuccess('delete', result) + }, 1000) + + } catch (error) { + console.error('❌ Delete error:', error) + toast.error(error instanceof Error ? error.message : "An error occurred during deletion") + } finally { + setIsDeleting(false) + } + } + + if (!revision) return null + + return ( + + + {/* 고정 헤더 */} + + + + Edit Revision + + + Modify revision details and metadata + + + + {!showDeleteConfirm ? ( +
+ + {/* 스크롤 가능한 중간 영역 */} +
+ + {/* 리비전 정보 표시 */} + + + {/* ✅ 리비전 필드 추가 */} + ( + + Revision + + + +
+ {revisionGuide} +
+ +
+ )} + /> + + {/* 용도 선택 */} + ( + + Usage + + + + )} + /> + + {/* 용도 타입 선택 (B3만) */} + {showUsageType && watchedUsage && ( + ( + + Usage Type + + {watchedUsage === "Comments" && ( +
+ Automatically set to "Comments" for this usage +
+ )} + +
+ )} + /> + )} + + {/* 코멘트 */} + ( + + Comment + +