diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-24 07:59:35 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-24 07:59:35 +0000 |
| commit | 4fe733d7d9d3d873fa395133e9a42cf9fc8c44dc (patch) | |
| tree | cd2429665d9fd55620c748c41d600f3e2cb0e685 /lib/rfq-last | |
| parent | ee52c983423fbc63373ce1dacb041d973da502df (diff) | |
(최겸) 구매 피드백 반영(용어 수정, 견적유형, 조건설정, 첨부파일 선택사항 등)
Diffstat (limited to 'lib/rfq-last')
| -rw-r--r-- | lib/rfq-last/table/create-general-rfq-dialog.tsx | 67 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-columns.tsx | 4 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx | 56 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/batch-update-conditions-dialog.tsx | 59 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/rfq-vendor-table.tsx | 118 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/send-rfq-dialog.tsx | 9 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/vendor-detail-dialog.tsx | 4 |
7 files changed, 181 insertions, 136 deletions
diff --git a/lib/rfq-last/table/create-general-rfq-dialog.tsx b/lib/rfq-last/table/create-general-rfq-dialog.tsx index 2c69f4b7..f7515787 100644 --- a/lib/rfq-last/table/create-general-rfq-dialog.tsx +++ b/lib/rfq-last/table/create-general-rfq-dialog.tsx @@ -70,22 +70,13 @@ const itemSchema = z.object({ // 일반견적 생성 폼 스키마 const createGeneralRfqSchema = z.object({ rfqType: z.string().min(1, "견적 종류를 선택해주세요"), - customRfqType: z.string().optional(), rfqTitle: z.string().min(1, "견적명을 입력해주세요"), dueDate: z.date({ - required_error: "마감일을 선택해주세요", + required_error: "제출마감일을 선택해주세요", }), - picUserId: z.number().min(1, "구매 담당자를 선택해주세요"), + picUserId: z.number().min(1, "견적담당자를 선택해주세요"), remark: z.string().optional(), items: z.array(itemSchema).min(1, "최소 하나의 아이템을 추가해주세요"), -}).refine((data) => { - if (data.rfqType === "기타") { - return data.customRfqType && data.customRfqType.trim().length > 0 - } - return true -}, { - message: "견적 종류를 직접 입력해주세요", - path: ["customRfqType"], }) type CreateGeneralRfqFormValues = z.infer<typeof createGeneralRfqSchema> @@ -120,7 +111,6 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp resolver: zodResolver(createGeneralRfqSchema), defaultValues: { rfqType: "", - customRfqType: "", rfqTitle: "", dueDate: undefined, picUserId: userId || undefined, @@ -142,12 +132,9 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp name: "items", }) - // 견적 종류 변경 시 customRfqType 필드 초기화 + // 견적 종류 변경 const handleRfqTypeChange = (value: string) => { form.setValue("rfqType", value) - if (value !== "기타") { - form.setValue("customRfqType", "") - } } // RFQ 코드 미리보기 생성 @@ -233,7 +220,6 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp if (!newOpen) { form.reset({ rfqType: "", - customRfqType: "", rfqTitle: "", dueDate: undefined, picUserId: userId || undefined, @@ -269,12 +255,9 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp setIsLoading(true) try { - // 견적 종류가 "기타"인 경우 customRfqType 사용 - const finalRfqType = data.rfqType === "기타" ? data.customRfqType || "기타" : data.rfqType - // 서버 액션 호출 const result = await createGeneralRfqAction({ - rfqType: finalRfqType, + rfqType: data.rfqType, rfqTitle: data.rfqTitle, dueDate: data.dueDate, picUserId: data.picUserId, @@ -325,7 +308,6 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp }) } - const isCustomRfqType = form.watch("rfqType") === "기타" return ( <Dialog open={open} onOpenChange={handleOpenChange}> @@ -360,7 +342,7 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp control={form.control} name="rfqType" render={({ field }) => ( - <FormItem> + <FormItem className="flex flex-col"> <FormLabel> 견적 종류 <span className="text-red-500">*</span> </FormLabel> @@ -371,50 +353,25 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp </SelectTrigger> </FormControl> <SelectContent> - <SelectItem value="정기견적">정기견적</SelectItem> - <SelectItem value="긴급견적">긴급견적</SelectItem> <SelectItem value="단가계약">단가계약</SelectItem> - <SelectItem value="기술견적">기술견적</SelectItem> - <SelectItem value="예산견적">예산견적</SelectItem> - <SelectItem value="기타">기타</SelectItem> + <SelectItem value="매각계약">매각계약</SelectItem> + <SelectItem value="일반계약">일반계약</SelectItem> </SelectContent> </Select> <FormMessage /> </FormItem> )} /> - - {/* 기타 견적 종류 입력 필드 */} - {isCustomRfqType && ( - <FormField - control={form.control} - name="customRfqType" - render={({ field }) => ( - <FormItem> - <FormLabel> - 견적 종류 직접 입력 <span className="text-red-500">*</span> - </FormLabel> - <FormControl> - <Input - placeholder="견적 종류를 입력해주세요" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - )} </div> - {/* 마감일 */} + {/* 제출마감일 */} <FormField control={form.control} name="dueDate" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel> - 마감일 <span className="text-red-500">*</span> + 제출마감일 <span className="text-red-500">*</span> </FormLabel> <Popover> <PopoverTrigger asChild> @@ -429,7 +386,7 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp {field.value ? ( format(field.value, "yyyy-MM-dd") ) : ( - <span>마감일을 선택하세요</span> + <span>제출마감일을 선택하세요 (미선택 시 생성일 +7일)</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> @@ -483,7 +440,7 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel> - 구매 담당자 <span className="text-red-500">*</span> + 견적담당자 <span className="text-red-500">*</span> </FormLabel> <FormControl> <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> @@ -503,7 +460,7 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp ) : ( <> <span className="truncate mr-1 flex-grow text-left"> - {selectedUser ? selectedUser.name : "구매 담당자를 선택하세요"} + {selectedUser ? selectedUser.name : "견적담당자를 선택하세요"} </span> <ChevronsUpDown className="h-4 w-4 opacity-50 flex-shrink-0" /> </> diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx index eaf00660..fc7f4415 100644 --- a/lib/rfq-last/table/rfq-table-columns.tsx +++ b/lib/rfq-last/table/rfq-table-columns.tsx @@ -501,8 +501,8 @@ export function getRfqColumns({ cell: ({ row }) => { const series = row.original.series; if (!series) return "-"; - const label = series === "SS" ? "시리즈 통합" : series === "II" ? "품목 통합" : series; - return <Badge variant="outline">{label}</Badge>; + // const label = series === "SS" ? "시리즈 통합" : series === "II" ? "품목 통합" : series; + return <Badge variant="outline">{series}</Badge>; }, size: 100, }, diff --git a/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx b/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx index a7135ea5..11b6bdaf 100644 --- a/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx +++ b/lib/rfq-last/vendor-response/vendor-quotations-table-columns.tsx @@ -254,11 +254,16 @@ export function getColumns({ const rfqCode = row.original.rfqCode const value = row.getValue("rfqType") - // F로 시작하지 않으면 빈 값 반환 - if (!rfqCode?.startsWith('F')) { - return null + // RFQ 코드의 앞자리에 따라 유형 결정 + if (rfqCode?.startsWith('I')) { + return "ITB" + } else if (rfqCode?.startsWith('R')) { + return "RFQ" + } else if (rfqCode?.startsWith('F')) { + return "일반견적" } + // 기존 rfqType 값이 있는 경우 (백업) const typeMap: Record<string, string> = { "ITB": "ITB", "RFQ": "RFQ", @@ -270,30 +275,29 @@ export function getColumns({ minSize: 80, maxSize: 120, enableResizing: true, - // F로 시작하지 않을 때 컬럼 숨기기 enableHiding: true, }, - { - accessorKey: "rfqTitle", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="RFQ 제목" /> - ), - cell: ({ row }) => { - const rfqCode = row.original.rfqCode - const value = row.getValue("rfqTitle") + // { + // accessorKey: "rfqTitle", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="RFQ 제목" /> + // ), + // cell: ({ row }) => { + // const rfqCode = row.original.rfqCode + // const value = row.getValue("rfqTitle") - // F로 시작하지 않으면 빈 값 반환 - if (!rfqCode?.startsWith('F')) { - return null - } + // // F로 시작하지 않으면 빈 값 반환 + // if (!rfqCode?.startsWith('F')) { + // return null + // } - return value || "-" - }, - minSize: 200, - maxSize: 400, - enableResizing: true, - enableHiding: true, - }, + // return value || "-" + // }, + // minSize: 200, + // maxSize: 400, + // enableResizing: true, + // enableHiding: true, + // }, { accessorKey: "projectName", header: ({ column }) => ( @@ -301,10 +305,10 @@ export function getColumns({ ), cell: ({ row }) => ( <div className="flex flex-col"> - <span className="font-mono text-xs text-muted-foreground"> + <span className="text-md font-medium`"> {row.original.projectCode} </span> - <span className="max-w-[200px] truncate" title={row.original.projectName || ""}> + <span className="max-w-[200px] truncate text-sm text-muted-foreground" title={row.original.projectName || ""}> {row.original.projectName || "-"} </span> </div> @@ -453,7 +457,7 @@ export function getColumns({ { accessorKey: "rfqSendDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="발송일" /> + <DataTableColumnHeaderSimple column={column} title="RFQ 접수일" /> ), cell: ({ row }) => { const value = row.getValue("rfqSendDate") diff --git a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx index af38ff45..893fd9a3 100644 --- a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx +++ b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx @@ -128,6 +128,7 @@ export function BatchUpdateConditionsDialog({ const [shippingOpen, setShippingOpen] = React.useState(false); const [destinationOpen, setDestinationOpen] = React.useState(false); const [calendarOpen, setCalendarOpen] = React.useState(false); + const [currencyOpen, setCurrencyOpen] = React.useState(false); // 체크박스로 각 필드 업데이트 여부 관리 const [fieldsToUpdate, setFieldsToUpdate] = React.useState({ @@ -381,7 +382,28 @@ export function BatchUpdateConditionsDialog({ {/* 기본 조건 설정 */} <Card> <CardHeader> - <CardTitle className="text-lg">기본 조건</CardTitle> + <div className="flex items-center justify-between"> + <CardTitle className="text-lg">기본 조건</CardTitle> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => { + // 기본조건만 전체 선택/해제 + const basicFields = ['currency', 'paymentTermsCode', 'incoterms', 'deliveryDate', 'contractDuration', 'taxCode', 'shipping']; + const allBasicSelected = basicFields.every(field => fieldsToUpdate[field as keyof typeof fieldsToUpdate]); + const newState = { ...fieldsToUpdate }; + + basicFields.forEach(field => { + newState[field as keyof typeof newState] = !allBasicSelected; + }); + + setFieldsToUpdate(newState); + }} + > + {['currency', 'paymentTermsCode', 'incoterms', 'deliveryDate', 'contractDuration', 'taxCode', 'shipping'].every(field => fieldsToUpdate[field as keyof typeof fieldsToUpdate]) ? '전체 해제' : '전체 선택'} + </Button> + </div> </CardHeader> <CardContent className="space-y-4"> {/* 통화 */} @@ -405,11 +427,12 @@ export function BatchUpdateConditionsDialog({ </FormLabel> <div className="col-span-2"> <FormControl> - <Popover> + <Popover open={currencyOpen} onOpenChange={setCurrencyOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" + aria-expanded={currencyOpen} className="w-full justify-between" disabled={!fieldsToUpdate.currency} > @@ -433,7 +456,10 @@ export function BatchUpdateConditionsDialog({ <CommandItem key={currency} value={currency} - onSelect={() => field.onChange(currency)} + onSelect={() => { + field.onChange(currency); + setCurrencyOpen(false); + }} > {currency} <Check @@ -1017,9 +1043,12 @@ export function BatchUpdateConditionsDialog({ <div className="flex items-center gap-4"> <Checkbox checked={fieldsToUpdate.materialPrice} - onCheckedChange={(checked) => - setFieldsToUpdate({ ...fieldsToUpdate, materialPrice: !!checked }) - } + onCheckedChange={(checked) => { + setFieldsToUpdate({ ...fieldsToUpdate, materialPrice: !!checked }); + if (checked) { + form.setValue("materialPriceRelatedYn", true); + } + }} /> <FormField control={form.control} @@ -1053,9 +1082,12 @@ export function BatchUpdateConditionsDialog({ <div className="flex items-center gap-4"> <Checkbox checked={fieldsToUpdate.sparepart} - onCheckedChange={(checked) => - setFieldsToUpdate({ ...fieldsToUpdate, sparepart: !!checked }) - } + onCheckedChange={(checked) => { + setFieldsToUpdate({ ...fieldsToUpdate, sparepart: !!checked }); + if (checked) { + form.setValue("sparepartYn", true); + } + }} /> <FormField control={form.control} @@ -1108,9 +1140,12 @@ export function BatchUpdateConditionsDialog({ <div className="flex items-center gap-4"> <Checkbox checked={fieldsToUpdate.first} - onCheckedChange={(checked) => - setFieldsToUpdate({ ...fieldsToUpdate, first: !!checked }) - } + onCheckedChange={(checked) => { + setFieldsToUpdate({ ...fieldsToUpdate, first: !!checked }); + if (checked) { + form.setValue("firstYn", true); + } + }} /> <FormField control={form.control} diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx index ad89d1dc..98d53f5d 100644 --- a/lib/rfq-last/vendor/rfq-vendor-table.tsx +++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx @@ -488,6 +488,37 @@ export function RfqVendorTable({ } }, [rfqId, rfqCode, router]); + // vendor status에 따른 category 분류 함수 + const getVendorCategoryFromStatus = React.useCallback((status: string | null): string => { + if (!status) return "미분류"; + + const categoryMap: Record<string, string> = { + "PENDING_REVIEW": "발굴업체", // 가입 신청 중 + "IN_REVIEW": "잠재업체", // 심사 중 + "REJECTED": "발굴업체", // 심사 거부됨 + "IN_PQ": "잠재업체", // PQ 진행 중 + "PQ_SUBMITTED": "잠재업체", // PQ 제출 + "PQ_FAILED": "잠재업체", // PQ 실패 + "PQ_APPROVED": "잠재업체", // PQ 통과 + "APPROVED": "잠재업체", // 승인됨 + "READY_TO_SEND": "잠재업체", // 정규등록검토 + "ACTIVE": "정규업체", // 활성 상태 (실제 거래 중) + "INACTIVE": "중지업체", // 비활성 상태 + "BLACKLISTED": "중지업체", // 거래 금지 + }; + + return categoryMap[status] || "미분류"; + }, []); + + // vendorCountry를 보기 좋은 형태로 변환 + const formatVendorCountry = React.useCallback((country: string | null): string => { + if (!country) return "미지정"; + + // KR 또는 한국이면 내자, 그 외는 전부 외자 + const isLocal = country === "KR" || country === "한국"; + return isLocal ? `내자(${country})` : `외자(${country})`; + }, []); + // 액션 처리 const handleAction = React.useCallback(async (action: string, vendor: any) => { switch (action) { @@ -636,7 +667,15 @@ export function RfqVendorTable({ header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="업체분류" />, filterFn: createFilterFn("text"), - cell: ({ row }) => row.original.vendorCategory || "-", + cell: ({ row }) => { + const status = row.original.status; + const category = getVendorCategoryFromStatus(status); + return ( + <Badge variant="outline" className="text-xs"> + {category} + </Badge> + ); + }, size: 100, }, { @@ -646,10 +685,11 @@ export function RfqVendorTable({ cell: ({ row }) => { const country = row.original.vendorCountry; + const formattedCountry = formatVendorCountry(country); const isLocal = country === "KR" || country === "한국"; return ( <Badge variant={isLocal ? "default" : "secondary"}> - {country || "-"} + {formattedCountry} </Badge> ); }, @@ -1503,6 +1543,37 @@ export function RfqVendorTable({ {selectedRows.length > 0 && ( <> + {/* 정보 일괄 입력 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => setIsBatchUpdateOpen(true)} + disabled={isLoadingSendData} + > + <Settings2 className="h-4 w-4 mr-2" /> + 정보 일괄 입력 ({selectedRows.length}) + </Button> + + {/* RFQ 발송 버튼 */} + <Button + variant="outline" + size="sm" + onClick={handleBulkSend} + disabled={isLoadingSendData || selectedRows.length === 0} + > + {isLoadingSendData ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 데이터 준비중... + </> + ) : ( + <> + <Send className="h-4 w-4 mr-2" /> + RFQ 발송 ({shortListCount}) + </> + )} + </Button> + {/* Short List 확정 버튼 */} {rfqCode?.startsWith("I") && <Button @@ -1539,37 +1610,6 @@ export function RfqVendorTable({ 견적 비교 {quotationCount > 0 && ` (${quotationCount})`} </Button> - - {/* 정보 일괄 입력 버튼 */} - <Button - variant="outline" - size="sm" - onClick={() => setIsBatchUpdateOpen(true)} - disabled={isLoadingSendData} - > - <Settings2 className="h-4 w-4 mr-2" /> - 정보 일괄 입력 ({selectedRows.length}) - </Button> - - {/* RFQ 발송 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleBulkSend} - disabled={isLoadingSendData || selectedRows.length === 0} - > - {isLoadingSendData ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - 데이터 준비중... - </> - ) : ( - <> - <Send className="h-4 w-4 mr-2" /> - RFQ 발송 ({shortListCount}) - </> - )} - </Button> </> )} @@ -1679,6 +1719,18 @@ export function RfqVendorTable({ }} /> )} + + {/* AVL 벤더 연동 다이얼로그 */} + <AvlVendorDialog + open={isAvlDialogOpen} + onOpenChange={setIsAvlDialogOpen} + rfqId={rfqId} + rfqCode={rfqCode} + onSuccess={() => { + setIsAvlDialogOpen(false); + router.refresh(); + }} + /> </> ); }
\ No newline at end of file diff --git a/lib/rfq-last/vendor/send-rfq-dialog.tsx b/lib/rfq-last/vendor/send-rfq-dialog.tsx index 34777864..ed43d87f 100644 --- a/lib/rfq-last/vendor/send-rfq-dialog.tsx +++ b/lib/rfq-last/vendor/send-rfq-dialog.tsx @@ -653,10 +653,7 @@ export function SendRfqDialog({ return; } - if (selectedAttachments.length === 0) { - toast.warning("최소 하나 이상의 첨부파일을 선택해주세요."); - return; - } + // 첨부파일은 선택사항 - 없어도 발송 가능 // 재발송 업체 확인 const resendVendors = vendorsWithRecipients.filter(v => v.sendVersion && v.sendVersion > 0); @@ -1350,12 +1347,12 @@ export function SendRfqDialog({ </Button> <Button onClick={handleSend} - disabled={isSending || isGeneratingPdfs || selectedAttachments.length === 0} + disabled={isSending || isGeneratingPdfs} > {isGeneratingPdfs ? ( <> <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> - 계약서 생성중... ({Math.round(pdfGenerationProgress)}%) + RFQ 송부중... ({Math.round(pdfGenerationProgress)}%) </> ) : isSending ? ( <> diff --git a/lib/rfq-last/vendor/vendor-detail-dialog.tsx b/lib/rfq-last/vendor/vendor-detail-dialog.tsx index e4c78656..54aada1d 100644 --- a/lib/rfq-last/vendor/vendor-detail-dialog.tsx +++ b/lib/rfq-last/vendor/vendor-detail-dialog.tsx @@ -253,8 +253,8 @@ export function VendorResponseDetailDialog({ <div className="flex items-center gap-2"> <Globe className="h-4 w-4 text-muted-foreground" /> <span className="text-sm text-muted-foreground">국가</span> - <Badge variant={data.vendorCountry === "KR" ? "default" : "secondary"} className="ml-auto"> - {data.vendorCountry} + <Badge variant={data.vendorCountry === "KR" || data.vendorCountry === "한국" ? "default" : "secondary"} className="ml-auto"> + {data.vendorCountry === "KR" || data.vendorCountry === "한국" ? `내자(${data.vendorCountry})` : `외자(${data.vendorCountry})`} </Badge> </div> </div> |
