// create-vendor-gtc-clause-dialog.tsx "use client" import * as React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" 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 { Textarea } from "@/components/ui/textarea" import { Checkbox } from "@/components/ui/checkbox" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } 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, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Check, ChevronsUpDown, Loader, Plus, Info } from "lucide-react" import { cn } from "@/lib/utils" import { toast } from "sonner" import { createVendorGtcClauseSchema, type CreateVendorGtcClauseSchema } from "@/lib/gtc-contract/gtc-clauses/validations" import { createVendorGtcClause, getGtcClausesTree } from "@/lib/gtc-contract/gtc-clauses/service" import { type GtcClauseTreeView } from "@/db/schema/gtc" import { useSession } from "next-auth/react" import { MarkdownImageEditor } from "./markdown-image-editor" interface ClauseImage { id: string url: string fileName: string size: number } interface CreateVendorGtcClauseDialogProps { documentId: number document: any vendorDocumentId: number vendorId: number vendorName?: string baseClauseId?: number // Optional - if creating a new vendor clause from scratch baseClause?: GtcClauseTreeView | null // Original clause to modify, if available parentClause?: GtcClauseTreeView | null onSuccess?: () => void open?: boolean onOpenChange?: (open: boolean) => void showTrigger?: boolean } export function CreateVendorGtcClauseDialog({ documentId, document, vendorDocumentId, vendorId, vendorName, baseClauseId, baseClause, parentClause = null, onSuccess, open: controlledOpen, onOpenChange: controlledOnOpenChange, showTrigger = true }: CreateVendorGtcClauseDialogProps) { const [internalOpen, setInternalOpen] = React.useState(false) // controlled vs uncontrolled 모드 const isControlled = controlledOpen !== undefined const open = isControlled ? controlledOpen : internalOpen const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen const [parentClauses, setParentClauses] = React.useState([]) const [isCreatePending, startCreateTransition] = React.useTransition() const { data: session } = useSession() const [images, setImages] = React.useState([]) const currentUserId = React.useMemo(() => { return session?.user?.id ? Number(session.user.id) : null }, [session]) React.useEffect(() => { if (open) { loadParentClauses() } }, [open, documentId]) const loadParentClauses = async () => { try { const tree = await getGtcClausesTree(documentId) setParentClauses(flattenTree(tree)) } catch (error) { console.error("Error loading parent clauses:", error) } } const form = useForm({ resolver: zodResolver(createVendorGtcClauseSchema), defaultValues: { documentId, vendorDocumentId, baseClauseId: baseClauseId || null, parentId: parentClause?.id || null, modifiedItemNumber: baseClause?.itemNumber || "", modifiedCategory: baseClause?.category || "", modifiedSubtitle: baseClause?.subtitle || "", modifiedContent: baseClause?.content || "", isNumberModified: false, isCategoryModified: false, isSubtitleModified: false, isContentModified: false, sortOrder: 0, reviewStatus: "draft", negotiationNote: "", isExcluded: false, editReason: "", }, }) const handleContentImageChange = (content: string, newImages: ClauseImage[]) => { form.setValue("modifiedContent", content) setImages(newImages) } async function onSubmit(data: CreateVendorGtcClauseSchema) { startCreateTransition(async () => { if (!currentUserId) { toast.error("로그인이 필요합니다") return } try { // 사용자 정보 추가 const result = await createVendorGtcClause({ ...data, images: images, createdById: currentUserId, vendorId, // 벤더 ID 추가 actorName: session?.user?.name || null, actorEmail: session?.user?.email || null, }) if (result.error) { toast.error(`에러: ${result.error}`) return } form.reset() setImages([]) setOpen(false) toast.success("벤더 GTC 조항이 생성되었습니다.") onSuccess?.() } catch (error) { toast.error("벤더 조항 생성 중 오류가 발생했습니다.") } }) } function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { form.reset() setImages([]) } setOpen(nextOpen) } const selectedParent = parentClauses.find(c => c.id === form.watch("parentId")) return ( {showTrigger && ( )} {baseClause ? `${baseClause.subtitle} 조항 협의 생성` : parentClause ? "하위 벤더 조항 생성" : "새 벤더 조항 생성"} {vendorName ? `${vendorName}과(와)의 GTC 조항 협의 내용을 입력하세요.` : '벤더별 GTC 조항 정보를 입력하세요.'} {/* 기존 조항 정보 (있는 경우) */} {baseClause && (
원본 조항 정보
채번: {baseClause.itemNumber}
{baseClause.category &&
분류: {baseClause.category}
}
소제목: {baseClause.subtitle}
{baseClause.content &&
내용: {baseClause.content.substring(0, 80)}...
}
)}
{/* 협의 상태 */} ( 협의 상태 )} /> {/* 제외 여부 */} (
이 조항을 벤더 계약에서 제외 체크하면 최종 계약서에서 이 조항이 제외됩니다
)} /> {/* 부모 조항 선택 (벤더 조항에만 필요한 경우) */} {!baseClause && ( { const [popoverOpen, setPopoverOpen] = React.useState(false) return ( 부모 조항 (선택사항) 조항을 찾을 수 없습니다. { field.onChange(null) setPopoverOpen(false) }} > 최상위 조항 {parentClauses.map((clause) => ( { field.onChange(clause.id) setPopoverOpen(false) }} >
{`${clause.itemNumber} - ${clause.subtitle}`}
))}
) }} /> )} {/* 채번 수정 */}
( 채번 수정 )} /> {form.watch("isNumberModified") && ( ( )} /> )}
{/* 분류 수정 */}
( 분류 수정 )} /> {form.watch("isCategoryModified") && ( ( )} /> )}
{/* 소제목 수정 */}
( 소제목 수정 )} /> {form.watch("isSubtitleModified") && ( ( )} /> )}
{/* 내용 수정 */}
( 상세항목 수정 )} /> {form.watch("isContentModified") && ( ( )} /> )}
{/* 협의 노트 */} ( 협의 메모