From 18954df6565108a469fb1608ea3715dd9bb1b02d Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Sep 2025 09:12:09 +0000 Subject: (대표님) 구매 기본계약, gtc 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gtc-vendor/update-gtc-clause-sheet.tsx | 522 +++++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx (limited to 'lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx') diff --git a/lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx b/lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx new file mode 100644 index 00000000..3487ebbf --- /dev/null +++ b/lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx @@ -0,0 +1,522 @@ +// update-vendor-gtc-clause-sheet.tsx +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader, Info, AlertCircle } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Label } from "@/components/ui/label" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" + +import { type GtcClauseTreeView } from "@/db/schema/gtc" +import { updateVendorGtcClauseSchema, type UpdateVendorGtcClauseSchema } from "@/lib/gtc-contract/gtc-clauses/validations" +import { updateVendorGtcClause } from "@/lib/gtc-contract/gtc-clauses/service" +import { useSession } from "next-auth/react" +import { MarkdownImageEditor } from "./markdown-image-editor" +import { Checkbox } from "@/components/ui/checkbox" + +interface ClauseImage { + id: string + url: string + fileName: string + size: number + savedName?: string + mimeType?: string + width?: number + height?: number + hash?: string +} + +export interface UpdateVendorGtcClauseSheetProps + extends React.ComponentPropsWithRef { + gtcClause: GtcClauseTreeView | null + vendorInfo?: any // 벤더 조항 정보 + documentId: number + vendorId: number + vendorName?: string +} + +export function UpdateGtcClauseSheet ({ + gtcClause, + vendorInfo, + documentId, + vendorId, + vendorName, + ...props +}: UpdateVendorGtcClauseSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const { data: session } = useSession() + const [images, setImages] = React.useState([]) + + console.log(vendorInfo,"vendorInfo") + + const currentUserId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null + }, [session]) + + const form = useForm({ + resolver: zodResolver(updateVendorGtcClauseSchema), + defaultValues: { + modifiedItemNumber: "", + modifiedCategory: "", + modifiedSubtitle: "", + modifiedContent: "", + isNumberModified: false, + isCategoryModified: false, + isSubtitleModified: false, + isContentModified: false, + reviewStatus: "draft", + negotiationNote: "", + isExcluded: false, + }, + }) + + // 벤더 정보가 있으면 초기값 세팅 + React.useEffect(() => { + if (vendorInfo) { + form.reset({ + modifiedItemNumber: vendorInfo.modifiedItemNumber || "", + modifiedCategory: vendorInfo.modifiedCategory || "", + modifiedSubtitle: vendorInfo.modifiedSubtitle || "", + modifiedContent: vendorInfo.modifiedContent || "", + isNumberModified: vendorInfo.isNumberModified || false, + isCategoryModified: vendorInfo.isCategoryModified || false, + isSubtitleModified: vendorInfo.isSubtitleModified || false, + isContentModified: vendorInfo.isContentModified || false, + reviewStatus: vendorInfo.reviewStatus || "draft", + negotiationNote: vendorInfo.negotiationNote || "", + isExcluded: vendorInfo.isExcluded || false, + }) + setImages((vendorInfo.images as any[]) || []) + } + }, [vendorInfo, form]) + + async function onSubmit(input: UpdateVendorGtcClauseSchema) { + startUpdateTransition(async () => { + if (!gtcClause || !currentUserId) { + toast.error("조항 정보를 찾을 수 없습니다.") + return + } + + try { + const result = await updateVendorGtcClause({ + baseClauseId: gtcClause.id, + documentId, + vendorId, + ...input, + images, + updatedById: currentUserId, + }) + + if (result.error) { + toast.error(result.error) + return + } + + form.reset() + props.onOpenChange?.(false) + toast.success("벤더 협의 내용이 저장되었습니다!") + } catch (error) { + toast.error("벤더 협의 저장 중 오류가 발생했습니다.") + } + }) + } + + const getDepthBadge = (depth: number) => { + const levels = ["1단계", "2단계", "3단계", "4단계", "5단계+"] + return levels[depth] || levels[4] + } + + const handleContentImageChange = (content: string, newImages: ClauseImage[]) => { + form.setValue("modifiedContent", content) + setImages(newImages) + } + + return ( + + + + 벤더 GTC 조항 협의 + + {vendorName ? `${vendorName}과(와)의 조항 협의 내용을 입력하세요` : '벤더별 조항 수정사항을 입력하세요'} + + + +
+ {/* 기존 조항 정보 (읽기 전용) */} +
+
+

표준 조항 내용

+ + {getDepthBadge(gtcClause?.depth || 0)} + +
+ +
+
+ +
{gtcClause?.itemNumber}
+
+ + {gtcClause?.category && ( +
+ +
{gtcClause.category}
+
+ )} + +
+ +
{gtcClause?.subtitle}
+
+ + {gtcClause?.content && ( +
+ +
+
{gtcClause.content}
+
+
+ )} +
+
+ + + + {/* 벤더별 수정 폼 */} +
+ +
+ +

+ 수정할 항목에 체크하고 내용을 입력하세요. 체크하지 않은 항목은 표준 조항의 내용을 그대로 사용합니다. +

+
+ + {/* 협의 상태 */} + ( + + 협의 상태 + + + + )} + /> + + {/* 제외 여부 */} + ( + + + + +
+ + 이 조항을 벤더 계약에서 제외 + + + 체크하면 최종 계약서에서 이 조항이 제외됩니다 + +
+
+ )} + /> + + {/* 채번 수정 */} +
+ ( + + + + + + 채번 수정 + + + )} + /> + + {form.watch("isNumberModified") && ( + ( + + + + + + + )} + /> + )} +
+ + {/* 분류 수정 */} +
+ ( + + + + + + 분류 수정 + + + )} + /> + + {form.watch("isCategoryModified") && ( + ( + + + + + + + )} + /> + )} +
+ + {/* 소제목 수정 */} +
+ ( + + + + + + 소제목 수정 + + + )} + /> + + {form.watch("isSubtitleModified") && ( + ( + + + + + + + )} + /> + )} +
+ + {/* 내용 수정 */} +
+ ( + + + + + + 상세항목 수정 + + + )} + /> + + {form.watch("isContentModified") && ( + ( + + + + + + + )} + /> + )} +
+ + {/* 협의 이력 표시 섹션 */} +{vendorInfo?.negotiationHistory && vendorInfo.negotiationHistory.length > 0 && ( +
+

협의 이력

+
+ {vendorInfo.negotiationHistory.map((history, index) => ( +
+
+
+ {history.actorName || "시스템"} + {history.previousStatus && history.newStatus && ( + + ({history.previousStatus} → {history.newStatus}) + + )} +
+
+ {new Date(history.createdAt).toLocaleString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + })} +
+
+ {history.comment && ( +
+ {history.comment} +
+ )} +
+ ))} +
+
+)} + + {/* 협의 노트 */} + ( + + 협의 메모 + +