diff options
Diffstat (limited to 'lib/vendors/table/update-vendor-sheet.tsx')
| -rw-r--r-- | lib/vendors/table/update-vendor-sheet.tsx | 710 |
1 files changed, 552 insertions, 158 deletions
diff --git a/lib/vendors/table/update-vendor-sheet.tsx b/lib/vendors/table/update-vendor-sheet.tsx index e65c4b1c..08994b6a 100644 --- a/lib/vendors/table/update-vendor-sheet.tsx +++ b/lib/vendors/table/update-vendor-sheet.tsx @@ -3,7 +3,25 @@ import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" -import { Loader } from "lucide-react" +import { + Loader, + Activity, + AlertCircle, + AlertTriangle, + ClipboardList, + FilePenLine, + XCircle, + ClipboardCheck, + FileCheck2, + FileX2, + BadgeCheck, + CheckCircle2, + Circle as CircleIcon, + User, + Building, + AlignLeft, + Calendar +} from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" @@ -14,6 +32,7 @@ import { FormItem, FormLabel, FormMessage, + FormDescription } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { @@ -33,27 +52,143 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" +import { useSession } from "next-auth/react" // Import useSession -import { Vendor } from "@/db/schema/vendors" +import { VendorWithType, vendors } from "@/db/schema/vendors" import { updateVendorSchema, type UpdateVendorSchema } from "../validations" import { modifyVendor } from "../service" -// 예: import { modifyVendor } from "@/lib/vendors/service" interface UpdateVendorSheetProps extends React.ComponentPropsWithRef<typeof Sheet> { - vendor: Vendor | null + vendor: VendorWithType | null +} +type StatusType = (typeof vendors.status.enumValues)[number]; + +type StatusConfig = { + Icon: React.ElementType; + className: string; + label: string; +}; + +// 상태 표시 유틸리티 함수 +const getStatusConfig = (status: StatusType): StatusConfig => { + switch(status) { + case "PENDING_REVIEW": + return { + Icon: ClipboardList, + className: "text-yellow-600", + label: "가입 신청 중" + }; + case "IN_REVIEW": + return { + Icon: FilePenLine, + className: "text-blue-600", + label: "심사 중" + }; + case "REJECTED": + return { + Icon: XCircle, + className: "text-red-600", + label: "심사 거부됨" + }; + case "IN_PQ": + return { + Icon: ClipboardCheck, + className: "text-purple-600", + label: "PQ 진행 중" + }; + case "PQ_SUBMITTED": + return { + Icon: FileCheck2, + className: "text-indigo-600", + label: "PQ 제출" + }; + case "PQ_FAILED": + return { + Icon: FileX2, + className: "text-red-600", + label: "PQ 실패" + }; + case "PQ_APPROVED": + return { + Icon: BadgeCheck, + className: "text-green-600", + label: "PQ 통과" + }; + case "APPROVED": + return { + Icon: CheckCircle2, + className: "text-green-600", + label: "승인됨" + }; + case "READY_TO_SEND": + return { + Icon: CheckCircle2, + className: "text-emerald-600", + label: "MDG 송부대기" + }; + case "ACTIVE": + return { + Icon: Activity, + className: "text-emerald-600", + label: "활성 상태" + }; + case "INACTIVE": + return { + Icon: AlertCircle, + className: "text-gray-600", + label: "비활성 상태" + }; + case "BLACKLISTED": + return { + Icon: AlertTriangle, + className: "text-slate-800", + label: "거래 금지" + }; + default: + return { + Icon: CircleIcon, + className: "text-gray-600", + label: status + }; + } +}; + +// 신용평가기관 목록 +const creditAgencies = [ + { value: "NICE", label: "NICE평가정보" }, + { value: "KIS", label: "KIS (한국신용평가)" }, + { value: "KED", label: "KED (한국기업데이터)" }, + { value: "SCI", label: "SCI평가정보" }, +] + +// 신용등급 스케일 +const creditRatingScaleMap: Record<string, string[]> = { + NICE: ["AAA", "AA", "A", "BBB", "BB", "B", "C", "D"], + KIS: ["AAA", "AA+", "AA", "A+", "A", "BBB+", "BBB", "BB", "B", "C"], + KED: ["AAA", "AA", "A", "BBB", "BB", "B", "CCC", "CC", "C", "D"], + SCI: ["AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "BBB+", "BBB-", "B"], +} + +// 현금흐름등급 스케일 +const cashFlowRatingScaleMap: Record<string, string[]> = { + NICE: ["우수", "양호", "보통", "미흡", "불량"], + KIS: ["A+", "A", "B+", "B", "C", "D"], + KED: ["1등급", "2등급", "3등급", "4등급", "5등급"], + SCI: ["Level 1", "Level 2", "Level 3", "Level 4"], } // 폼 컴포넌트 export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) { const [isPending, startTransition] = React.useTransition() + const [selectedAgency, setSelectedAgency] = React.useState<string>(vendor?.creditAgency || "NICE") - console.log(vendor) - - // RHF + Zod + // 폼 정의 - UpdateVendorSchema 타입을 직접 사용 const form = useForm<UpdateVendorSchema>({ resolver: zodResolver(updateVendorSchema), defaultValues: { + // 업체 기본 정보 vendorName: vendor?.vendorName ?? "", vendorCode: vendor?.vendorCode ?? "", address: vendor?.address ?? "", @@ -61,7 +196,18 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) phone: vendor?.phone ?? "", email: vendor?.email ?? "", website: vendor?.website ?? "", + creditRating: vendor?.creditRating ?? "", + cashFlowRating: vendor?.cashFlowRating ?? "", status: vendor?.status ?? "ACTIVE", + vendorTypeId: vendor?.vendorTypeId ?? undefined, + + // 구매담당자 정보 (기본값은 비어있음) + buyerName: "", + buyerDepartment: "", + contractStartDate: undefined, + contractEndDate: undefined, + internalNotes: "", + // evaluationScore: "", }, }) @@ -75,191 +221,439 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) phone: vendor?.phone ?? "", email: vendor?.email ?? "", website: vendor?.website ?? "", + creditRating: vendor?.creditRating ?? "", + cashFlowRating: vendor?.cashFlowRating ?? "", status: vendor?.status ?? "ACTIVE", + vendorTypeId: vendor?.vendorTypeId ?? undefined, + + // 구매담당자 필드는 유지 + buyerName: form.getValues("buyerName"), + buyerDepartment: form.getValues("buyerDepartment"), + contractStartDate: form.getValues("contractStartDate"), + contractEndDate: form.getValues("contractEndDate"), + internalNotes: form.getValues("internalNotes"), + // evaluationScore: form.getValues("evaluationScore"), }); } }, [vendor, form]); - console.log(form.getValues()) + // 신용평가기관 변경 시 등급 필드를 초기화하는 효과 + React.useEffect(() => { + // 선택된 평가기관에 따라 현재 선택된 등급이 유효한지 확인 + const currentCreditRating = form.getValues("creditRating"); + const currentCashFlowRating = form.getValues("cashFlowRating"); + + // 선택된 기관에 따른 유효한 등급 목록 + const validCreditRatings = creditRatingScaleMap[selectedAgency] || []; + const validCashFlowRatings = cashFlowRatingScaleMap[selectedAgency] || []; + + // 현재 등급이 유효하지 않으면 초기화 + if (currentCreditRating && !validCreditRatings.includes(currentCreditRating)) { + form.setValue("creditRating", ""); + } + + if (currentCashFlowRating && !validCashFlowRatings.includes(currentCashFlowRating)) { + form.setValue("cashFlowRating", ""); + } + + // 신용평가기관 필드 업데이트 + if(selectedAgency){ + form.setValue("creditAgency", selectedAgency as "NICE" | "KIS" | "KED" | "SCI"); + } + + }, [selectedAgency, form]); + // 제출 핸들러 async function onSubmit(data: UpdateVendorSchema) { if (!vendor) return + const { data: session } = useSession() - startTransition(async () => { - // 서버 액션 or API - // const { error } = await modifyVendor({ id: vendor.id, ...data }) - // 여기선 간단 예시 - try { - // 예시: - const { error } = await modifyVendor({ id: String(vendor.id), ...data }) - if (error) throw new Error(error) - - toast.success("Vendor updated!") - form.reset() - props.onOpenChange?.(false) - } catch (err: any) { - toast.error(String(err)) - } - }) - } + if (!session?.user?.id) { + toast.error("사용자 인증 정보를 찾을 수 없습니다.") + return + } + startTransition(async () => { + try { + // Add status change comment if status has changed + const oldStatus = vendor.status ?? "ACTIVE" // Default to ACTIVE if undefined + const newStatus = data.status ?? "ACTIVE" // Default to ACTIVE if undefined + + const statusComment = + oldStatus !== newStatus + ? `상태 변경: ${getStatusConfig(oldStatus).label} → ${getStatusConfig(newStatus).label}` + : "" // Empty string instead of undefined + + // 업체 정보 업데이트 - userId와 상태 변경 코멘트 추가 + const { error } = await modifyVendor({ + id: String(vendor.id), + userId: Number(session.user.id), // Add user ID from session + comment: statusComment, // Add comment for status changes + ...data // 모든 데이터 전달 - 서비스 함수에서 필요한 필드만 처리 + }) + + if (error) throw new Error(error) + + toast.success("업체 정보가 업데이트되었습니다!") + form.reset() + props.onOpenChange?.(false) + } catch (err: any) { + toast.error(String(err)) + } + }) +} return ( <Sheet {...props}> - <SheetContent className="flex flex-col gap-6 sm:max-w-md"> + <SheetContent className="flex flex-col gap-6 sm:max-w-lg overflow-y-auto"> <SheetHeader className="text-left"> - <SheetTitle>Update Vendor</SheetTitle> + <SheetTitle>업체 정보 수정</SheetTitle> <SheetDescription> - Update the vendor details and save the changes + 업체 세부 정보를 수정하고 변경 사항을 저장하세요 </SheetDescription> </SheetHeader> <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4"> - {/* vendorName */} - <FormField - control={form.control} - name="vendorName" - render={({ field }) => ( - <FormItem> - <FormLabel>Vendor Name</FormLabel> - <FormControl> - <Input placeholder="Vendor Name" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6"> + {/* 업체 기본 정보 섹션 */} + <div className="space-y-4"> + <div className="flex items-center"> + <Building className="mr-2 h-5 w-5 text-muted-foreground" /> + <h3 className="text-sm font-medium">업체 기본 정보</h3> + </div> + <FormDescription> + 업체가 제공한 기본 정보입니다. 필요시 수정하세요. + </FormDescription> + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + {/* vendorName */} + <FormField + control={form.control} + name="vendorName" + render={({ field }) => ( + <FormItem> + <FormLabel>업체명</FormLabel> + <FormControl> + <Input placeholder="업체명 입력" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* vendorCode */} + <FormField + control={form.control} + name="vendorCode" + render={({ field }) => ( + <FormItem> + <FormLabel>업체 코드</FormLabel> + <FormControl> + <Input placeholder="예: ABC123" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* address */} + <FormField + control={form.control} + name="address" + render={({ field }) => ( + <FormItem className="md:col-span-2"> + <FormLabel>주소</FormLabel> + <FormControl> + <Input placeholder="주소 입력" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* country */} + <FormField + control={form.control} + name="country" + render={({ field }) => ( + <FormItem> + <FormLabel>국가</FormLabel> + <FormControl> + <Input placeholder="예: 대한민국" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* phone */} + <FormField + control={form.control} + name="phone" + render={({ field }) => ( + <FormItem> + <FormLabel>전화번호</FormLabel> + <FormControl> + <Input placeholder="예: 010-1234-5678" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* email */} + <FormField + control={form.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormLabel>이메일</FormLabel> + <FormControl> + <Input placeholder="예: info@company.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* website */} + <FormField + control={form.control} + name="website" + render={({ field }) => ( + <FormItem> + <FormLabel>웹사이트</FormLabel> + <FormControl> + <Input placeholder="예: https://www.company.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> - {/* vendorCode */} - <FormField - control={form.control} - name="vendorCode" - render={({ field }) => ( - <FormItem> - <FormLabel>Vendor Code</FormLabel> - <FormControl> - <Input placeholder="Code123" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + {/* status with icons */} + <FormField + control={form.control} + name="status" + render={({ field }) => { + // 현재 선택된 상태의 구성 정보 가져오기 + const selectedConfig = getStatusConfig(field.value ?? "ACTIVE"); + const SelectedIcon = selectedConfig?.Icon || CircleIcon; - {/* address */} - <FormField - control={form.control} - name="address" - render={({ field }) => ( - <FormItem> - <FormLabel>Address</FormLabel> - <FormControl> - <Input placeholder="123 Main St" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + return ( + <FormItem> + <FormLabel>업체승인상태</FormLabel> + <FormControl> + <Select + value={field.value} + onValueChange={field.onChange} + > + <SelectTrigger className="w-full"> + <SelectValue> + {field.value && ( + <div className="flex items-center"> + <SelectedIcon className={`mr-2 h-4 w-4 ${selectedConfig.className}`} /> + <span>{selectedConfig.label}</span> + </div> + )} + </SelectValue> + </SelectTrigger> + <SelectContent> + <SelectGroup> + {vendors.status.enumValues.map((status) => { + const config = getStatusConfig(status); + const StatusIcon = config.Icon; + return ( + <SelectItem key={status} value={status}> + <div className="flex items-center"> + <StatusIcon className={`mr-2 h-4 w-4 ${config.className}`} /> + <span>{config.label}</span> + </div> + </SelectItem> + ); + })} + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> - {/* country */} - <FormField - control={form.control} - name="country" - render={({ field }) => ( - <FormItem> - <FormLabel>Country</FormLabel> - <FormControl> - <Input placeholder="USA" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + + {/* 신용평가기관 선택 */} + <FormField + control={form.control} + name="creditAgency" + render={({ field }) => ( + <FormItem> + <FormLabel>신용평가기관</FormLabel> + <FormControl> + <Select + value={field.value || ""} + onValueChange={(value) => { + field.onChange(value); + setSelectedAgency(value); + }} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="평가기관 선택" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + {creditAgencies.map((agency) => ( + <SelectItem key={agency.value} value={agency.value}> + {agency.label} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 평가년도 - 나중에 추가 가능 */} + + {/* 신용등급 - 선택된 기관에 따라 옵션 변경 */} + <FormField + control={form.control} + name="creditRating" + render={({ field }) => ( + <FormItem> + <FormLabel>신용등급</FormLabel> + <FormControl> + <Select + value={field.value || ""} + onValueChange={field.onChange} + disabled={!selectedAgency} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="신용등급 선택" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + {(creditRatingScaleMap[selectedAgency] || []).map((rating) => ( + <SelectItem key={rating} value={rating}> + {rating} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 현금흐름등급 - 선택된 기관에 따라 옵션 변경 */} + <FormField + control={form.control} + name="cashFlowRating" + render={({ field }) => ( + <FormItem> + <FormLabel>현금흐름등급</FormLabel> + <FormControl> + <Select + value={field.value || ""} + onValueChange={field.onChange} + disabled={!selectedAgency} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="현금흐름등급 선택" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + {(cashFlowRatingScaleMap[selectedAgency] || []).map((rating) => ( + <SelectItem key={rating} value={rating}> + {rating} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> - {/* phone */} - <FormField - control={form.control} - name="phone" - render={({ field }) => ( - <FormItem> - <FormLabel>Phone</FormLabel> - <FormControl> - <Input placeholder="+1 555-1234" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + </div> + </div> - {/* email */} - <FormField - control={form.control} - name="email" - render={({ field }) => ( - <FormItem> - <FormLabel>Email</FormLabel> - <FormControl> - <Input placeholder="vendor@example.com" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + {/* 구분선 */} + <Separator className="my-2" /> - {/* website */} - <FormField - control={form.control} - name="website" - render={({ field }) => ( - <FormItem> - <FormLabel>Website</FormLabel> - <FormControl> - <Input placeholder="https://www.vendor.com" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + {/* 구매담당자 입력 섹션 */} + <div className="space-y-4 bg-slate-50 p-4 rounded-md border border-slate-200"> + <div className="flex items-center"> + <User className="mr-2 h-5 w-5 text-blue-600" /> + <h3 className="text-sm font-medium text-blue-800">구매담당자 정보</h3> + </div> + <FormDescription> + 구매담당자가 관리하는 추가 정보입니다. 이 정보는 내부용으로만 사용됩니다. + </FormDescription> + + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + {/* 여기에 구매담당자 필드 추가 */} + <FormField + control={form.control} + name="buyerName" + render={({ field }) => ( + <FormItem> + <FormLabel>담당자 이름</FormLabel> + <FormControl> + <Input placeholder="담당자 이름" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="buyerDepartment" + render={({ field }) => ( + <FormItem> + <FormLabel>담당 부서</FormLabel> + <FormControl> + <Input placeholder="예: 구매부" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> - {/* status */} - <FormField - control={form.control} - name="status" - render={({ field }) => ( - <FormItem> - <FormLabel>Status</FormLabel> - <FormControl> - <Select - value={field.value} - onValueChange={field.onChange} - > - <SelectTrigger className="capitalize"> - <SelectValue placeholder="Select a status" /> - </SelectTrigger> - <SelectContent> - <SelectGroup> - {/* enum ["ACTIVE","INACTIVE","BLACKLISTED"] */} - <SelectItem value="ACTIVE">ACTIVE</SelectItem> - <SelectItem value="INACTIVE">INACTIVE</SelectItem> - <SelectItem value="BLACKLISTED">BLACKLISTED</SelectItem> - </SelectGroup> - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + + <FormField + control={form.control} + name="internalNotes" + render={({ field }) => ( + <FormItem className="md:col-span-2"> + <FormLabel>내부 메모</FormLabel> + <FormControl> + <Input placeholder="내부 참고사항을 입력하세요" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> <SheetFooter className="gap-2 pt-2 sm:space-x-0"> <SheetClose asChild> <Button type="button" variant="outline"> - Cancel + 취소 </Button> </SheetClose> <Button disabled={isPending}> {isPending && ( <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> )} - Save + 저장 </Button> </SheetFooter> </form> |
