summaryrefslogtreecommitdiff
path: root/components/ship-vendor-document/edit-revision-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-14 11:54:47 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-14 11:54:47 +0000
commit969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 (patch)
tree551d335e850e6163792ded0e7a75fa41d96d612a /components/ship-vendor-document/edit-revision-dialog.tsx
parentdd20ba9785cdbd3d61f6b014d003d3bd9646ad13 (diff)
(대표님) 정규벤더등록, 벤더문서관리, 벤더데이터입력, 첨부파일관리
Diffstat (limited to 'components/ship-vendor-document/edit-revision-dialog.tsx')
-rw-r--r--components/ship-vendor-document/edit-revision-dialog.tsx726
1 files changed, 726 insertions, 0 deletions
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 (
+ <div className="space-y-3 p-4 bg-gray-50 rounded-lg border">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <Badge variant="outline" className="text-base font-mono">
+ {revision.revision}
+ </Badge>
+ <Badge className={`text-xs ${getStatusColor(revision.revisionStatus)}`}>
+ {revision.revisionStatus}
+ </Badge>
+ {!canEdit && (
+ <Badge variant="secondary" className="text-xs">
+ Partially Processed
+ </Badge>
+ )}
+ </div>
+
+ <div className="flex items-center gap-2 text-sm text-gray-600">
+ <FileText className="h-4 w-4" />
+ <span>{revision.attachments?.length || 0} file(s)</span>
+ {processedCount > 0 && (
+ <span className="text-blue-600">
+ ({processedCount} processed)
+ </span>
+ )}
+ </div>
+ </div>
+
+ <div className="grid grid-cols-2 gap-4 text-sm">
+ <div>
+ <span className="text-gray-500">Uploader:</span>
+ <span className="ml-2 font-medium">{revision.uploaderName || '-'}</span>
+ </div>
+ <div>
+ <span className="text-gray-500">Upload Date:</span>
+ <span className="ml-2">
+ {revision.uploadedAt
+ ? new Date(revision.uploadedAt).toLocaleDateString()
+ : '-'
+ }
+ </span>
+ </div>
+ </div>
+
+ {revision.comment && (
+ <div className="pt-2 border-t">
+ <span className="text-gray-500 text-sm">Current Comment:</span>
+ <p className="mt-1 text-sm bg-white p-2 rounded border">
+ {revision.comment}
+ </p>
+ </div>
+ )}
+
+ {!canEdit && (
+ <div className="flex items-center gap-2 p-2 bg-yellow-50 border border-yellow-200 rounded text-sm">
+ <AlertTriangle className="h-4 w-4 text-yellow-600" />
+ <span className="text-yellow-800">
+ Some files have been processed. Editing is limited.
+ </span>
+ </div>
+ )}
+ </div>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * 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<typeof editRevisionSchema>
+
+ const form = useForm<EditRevisionSchema>({
+ 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 (
+ <Dialog open={open} onOpenChange={handleDialogClose}>
+ <DialogContent className="max-w-2xl h-[85vh] flex flex-col overflow-hidden">
+ {/* 고정 헤더 */}
+ <DialogHeader className="flex-shrink-0 pb-4 border-b">
+ <DialogTitle className="flex items-center gap-2">
+ <Edit className="h-5 w-5" />
+ Edit Revision
+ </DialogTitle>
+ <DialogDescription className="text-sm">
+ Modify revision details and metadata
+ </DialogDescription>
+ </DialogHeader>
+
+ {!showDeleteConfirm ? (
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 overflow-hidden">
+ {/* 스크롤 가능한 중간 영역 */}
+ <div className="flex-1 overflow-y-auto px-1 py-4 space-y-6">
+
+ {/* 리비전 정보 표시 */}
+ <RevisionInfoDisplay revision={revision} />
+
+ {/* ✅ 리비전 필드 추가 */}
+ <FormField
+ control={form.control}
+ name="revision"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">Revision</FormLabel>
+ <FormControl>
+ <Input
+ placeholder={revisionGuide}
+ disabled={!canEdit}
+ {...field}
+ />
+ </FormControl>
+ <div className="text-xs text-muted-foreground mt-1">
+ {revisionGuide}
+ </div>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 용도 선택 */}
+ <FormField
+ control={form.control}
+ name="usage"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">Usage</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value} disabled={!canEdit}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Select usage" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {usageOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 용도 타입 선택 (B3만) */}
+ {showUsageType && watchedUsage && (
+ <FormField
+ control={form.control}
+ name="usageType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">Usage Type</FormLabel>
+ <Select
+ onValueChange={field.onChange}
+ value={field.value || ""}
+ disabled={!canEdit || watchedUsage === "Comments"}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Select usage type" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {usageTypeOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ {watchedUsage === "Comments" && (
+ <div className="text-xs text-muted-foreground mt-1">
+ Automatically set to "Comments" for this usage
+ </div>
+ )}
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 코멘트 */}
+ <FormField
+ control={form.control}
+ name="comment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Comment</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="Enter description or changes for this revision (optional)"
+ className="resize-none"
+ rows={4}
+ disabled={!canEdit}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 첨부파일 목록 */}
+ {revision.attachments && revision.attachments.length > 0 && (
+ <div className="space-y-2">
+ <FormLabel>Attachments ({revision.attachments.length})</FormLabel>
+ <div className="space-y-2 max-h-32 overflow-y-auto border rounded-lg p-3">
+ {revision.attachments.map((file, index) => (
+ <div
+ key={file.id}
+ className="flex items-center justify-between p-2 bg-gray-50 rounded text-sm"
+ >
+ <div className="flex items-center gap-2 flex-1 min-w-0">
+ <FileText className="h-4 w-4 text-gray-500 flex-shrink-0" />
+ <span className="truncate" title={file.fileName}>
+ {file.fileName}
+ </span>
+ </div>
+ {file.dolceFilePath && file.dolceFilePath.trim() !== '' && (
+ <Badge variant="secondary" className="text-xs ml-2">
+ Processed
+ </Badge>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* 성공/로딩 상태 */}
+ {isLoading && (
+ <div className="flex items-center gap-2 p-3 bg-blue-50 border border-blue-200 rounded">
+ <Loader2 className="h-4 w-4 animate-spin text-blue-600" />
+ <span className="text-blue-800 text-sm">Updating revision...</span>
+ </div>
+ )}
+ </div>
+
+ {/* 고정 버튼 영역 */}
+ <DialogFooter className="flex-shrink-0 border-t pt-4">
+ <div className="flex items-center justify-between w-full">
+ {/* 삭제 버튼 */}
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setShowDeleteConfirm(true)}
+ disabled={isLoading || !canEdit}
+ className="text-red-600 border-red-200 hover:bg-red-50"
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ Delete
+ </Button>
+
+ {/* 저장/취소 버튼 */}
+ <div className="flex gap-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleDialogClose}
+ disabled={isLoading}
+ >
+ Cancel
+ </Button>
+ <Button
+ type="submit"
+ disabled={isLoading || !hasChanges || !canEdit}
+ className="min-w-[120px]"
+ >
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Updating...
+ </>
+ ) : (
+ <>
+ <CheckCircle className="mr-2 h-4 w-4" />
+ Update
+ </>
+ )}
+ </Button>
+ </div>
+ </div>
+ </DialogFooter>
+ </form>
+ </Form>
+ ) : (
+ // 삭제 확인 화면
+ <div className="flex flex-col flex-1 overflow-hidden">
+ <div className="flex-1 overflow-y-auto px-1 py-4">
+ <div className="space-y-4">
+ <div className="flex items-center gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
+ <AlertTriangle className="h-6 w-6 text-red-600 flex-shrink-0" />
+ <div>
+ <h4 className="font-medium text-red-900">Delete Revision</h4>
+ <p className="text-sm text-red-700 mt-1">
+ Are you sure you want to delete revision <strong>{revision.revision}</strong>?
+ </p>
+ </div>
+ </div>
+
+ <div className="space-y-3 text-sm text-gray-600">
+ <p>This action will permanently delete:</p>
+ <ul className="list-disc list-inside space-y-1 ml-4">
+ <li>Revision metadata and settings</li>
+ <li>All {revision.attachments?.length || 0} attached file(s)</li>
+ <li>Upload history and comments</li>
+ </ul>
+ <p className="font-medium text-red-600">
+ This action cannot be undone.
+ </p>
+ </div>
+
+ {/* 성공/로딩 상태 */}
+ {isDeleting && (
+ <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded">
+ <Loader2 className="h-4 w-4 animate-spin text-red-600" />
+ <span className="text-red-800 text-sm">Deleting revision...</span>
+ </div>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter className="flex-shrink-0 border-t pt-4">
+ <div className="flex gap-2 w-full justify-end">
+ <Button
+ variant="outline"
+ onClick={() => setShowDeleteConfirm(false)}
+ disabled={isDeleting}
+ >
+ Cancel
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={handleDelete}
+ disabled={isDeleting}
+ className="min-w-[120px]"
+ >
+ {isDeleting ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Deleting...
+ </>
+ ) : (
+ <>
+ <Trash2 className="mr-2 h-4 w-4" />
+ Delete Revision
+ </>
+ )}
+ </Button>
+ </div>
+ </DialogFooter>
+ </div>
+ )}
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file