diff options
Diffstat (limited to 'lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx')
| -rw-r--r-- | lib/basic-contract/gtc-vendor/update-gtc-clause-sheet.tsx | 522 |
1 files changed, 522 insertions, 0 deletions
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<typeof Sheet> { + 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<ClauseImage[]>([]) + + console.log(vendorInfo,"vendorInfo") + + const currentUserId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null + }, [session]) + + const form = useForm<UpdateVendorGtcClauseSchema>({ + 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 ( + <Sheet {...props}> + <SheetContent className="flex flex-col sm:max-w-2xl h-full"> + <SheetHeader className="text-left flex-shrink-0"> + <SheetTitle>벤더 GTC 조항 협의</SheetTitle> + <SheetDescription> + {vendorName ? `${vendorName}과(와)의 조항 협의 내용을 입력하세요` : '벤더별 조항 수정사항을 입력하세요'} + </SheetDescription> + </SheetHeader> + + <div className="flex-1 overflow-y-auto"> + {/* 기존 조항 정보 (읽기 전용) */} + <div className="space-y-4 p-4 bg-muted/30 rounded-lg mb-4"> + <div className="flex items-center justify-between"> + <h3 className="text-sm font-semibold">표준 조항 내용</h3> + <Badge variant="outline"> + {getDepthBadge(gtcClause?.depth || 0)} + </Badge> + </div> + + <div className="space-y-3"> + <div> + <Label className="text-xs text-muted-foreground">채번</Label> + <div className="text-sm font-mono mt-1">{gtcClause?.itemNumber}</div> + </div> + + {gtcClause?.category && ( + <div> + <Label className="text-xs text-muted-foreground">분류</Label> + <div className="text-sm mt-1">{gtcClause.category}</div> + </div> + )} + + <div> + <Label className="text-xs text-muted-foreground">소제목</Label> + <div className="text-sm font-medium mt-1">{gtcClause?.subtitle}</div> + </div> + + {gtcClause?.content && ( + <div> + <Label className="text-xs text-muted-foreground">상세항목</Label> + <div className="text-sm mt-1 p-2 bg-background rounded border"> + <pre className="whitespace-pre-wrap font-sans">{gtcClause.content}</pre> + </div> + </div> + )} + </div> + </div> + + <Separator className="my-4" /> + + {/* 벤더별 수정 폼 */} + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 p-4"> + <div className="flex items-center gap-2 mb-4"> + <AlertCircle className="h-4 w-4 text-blue-500" /> + <p className="text-sm text-muted-foreground"> + 수정할 항목에 체크하고 내용을 입력하세요. 체크하지 않은 항목은 표준 조항의 내용을 그대로 사용합니다. + </p> + </div> + + {/* 협의 상태 */} + <FormField + control={form.control} + name="reviewStatus" + render={({ field }) => ( + <FormItem> + <FormLabel>협의 상태</FormLabel> + <Select onValueChange={field.onChange} defaultValue={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="협의 상태를 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="draft">초안</SelectItem> + <SelectItem value="pending">협의 대기</SelectItem> + <SelectItem value="reviewing">협의 중</SelectItem> + <SelectItem value="approved">승인됨</SelectItem> + <SelectItem value="rejected">거부됨</SelectItem> + <SelectItem value="revised">수정됨</SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 제외 여부 */} + <FormField + control={form.control} + name="isExcluded" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel> + 이 조항을 벤더 계약에서 제외 + </FormLabel> + <FormDescription> + 체크하면 최종 계약서에서 이 조항이 제외됩니다 + </FormDescription> + </div> + </FormItem> + )} + /> + + {/* 채번 수정 */} + <div className="space-y-2 border rounded-lg p-3"> + <FormField + control={form.control} + name="isNumberModified" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <FormLabel className="font-normal cursor-pointer"> + 채번 수정 + </FormLabel> + </FormItem> + )} + /> + + {form.watch("isNumberModified") && ( + <FormField + control={form.control} + name="modifiedItemNumber" + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="수정할 채번 입력" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + {/* 분류 수정 */} + <div className="space-y-2 border rounded-lg p-3"> + <FormField + control={form.control} + name="isCategoryModified" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <FormLabel className="font-normal cursor-pointer"> + 분류 수정 + </FormLabel> + </FormItem> + )} + /> + + {form.watch("isCategoryModified") && ( + <FormField + control={form.control} + name="modifiedCategory" + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="수정할 분류 입력" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + {/* 소제목 수정 */} + <div className="space-y-2 border rounded-lg p-3"> + <FormField + control={form.control} + name="isSubtitleModified" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <FormLabel className="font-normal cursor-pointer"> + 소제목 수정 + </FormLabel> + </FormItem> + )} + /> + + {form.watch("isSubtitleModified") && ( + <FormField + control={form.control} + name="modifiedSubtitle" + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="수정할 소제목 입력" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + {/* 내용 수정 */} + <div className="space-y-2 border rounded-lg p-3"> + <FormField + control={form.control} + name="isContentModified" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <FormLabel className="font-normal cursor-pointer"> + 상세항목 수정 + </FormLabel> + </FormItem> + )} + /> + + {form.watch("isContentModified") && ( + <FormField + control={form.control} + name="modifiedContent" + render={({ field }) => ( + <FormItem> + <FormControl> + <MarkdownImageEditor + content={field.value || ""} + images={images} + onChange={handleContentImageChange} + placeholder="수정할 내용을 입력하세요..." + rows={6} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + {/* 협의 이력 표시 섹션 */} +{vendorInfo?.negotiationHistory && vendorInfo.negotiationHistory.length > 0 && ( + <div className="space-y-3 mb-4"> + <h3 className="text-sm font-semibold">협의 이력</h3> + <div className="border rounded-lg p-3 max-h-64 overflow-y-auto space-y-3"> + {vendorInfo.negotiationHistory.map((history, index) => ( + <div key={index} className="border-b pb-3 last:border-b-0 last:pb-0"> + <div className="flex justify-between text-xs text-muted-foreground mb-1"> + <div className="font-medium"> + {history.actorName || "시스템"} + {history.previousStatus && history.newStatus && ( + <span className="ml-2 text-xs"> + ({history.previousStatus} → {history.newStatus}) + </span> + )} + </div> + <div> + {new Date(history.createdAt).toLocaleString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + })} + </div> + </div> + {history.comment && ( + <div className="text-sm p-2 bg-muted/30 rounded"> + {history.comment} + </div> + )} + </div> + ))} + </div> + </div> +)} + + {/* 협의 노트 */} + <FormField + control={form.control} + name="negotiationNote" + render={({ field }) => ( + <FormItem> + <FormLabel>협의 메모</FormLabel> + <FormControl> + <Textarea + placeholder="협의 과정에서의 메모나 특이사항을 기록하세요..." + {...field} + rows={3} + /> + </FormControl> + <FormDescription> + 벤더와의 협의 내용이나 변경 사유를 기록합니다 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </form> + </Form> + </div> + + <SheetFooter className="gap-2 pt-2 sm:space-x-0 flex-shrink-0"> + <SheetClose asChild> + <Button type="button" variant="outline"> + 취소 + </Button> + </SheetClose> + + <Button + onClick={form.handleSubmit(onSubmit)} + disabled={isUpdatePending} + > + {isUpdatePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 저장 + </Button> + </SheetFooter> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
