From 9cda8482660a87fd98c9ee43f507d75ff75b4e23 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 28 Nov 2025 03:12:57 +0000 Subject: (최겸) 구매 입찰 피드백 반영(90%) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bidding/create/bidding-create-dialog.tsx | 451 +++++++++++++------- .../bidding/manage/bidding-basic-info-editor.tsx | 472 ++++++++++++++------- .../bidding/manage/bidding-companies-editor.tsx | 8 +- components/bidding/manage/bidding-items-editor.tsx | 35 +- .../bidding/manage/bidding-schedule-editor.tsx | 21 + .../receive/bidding-participants-dialog.tsx | 216 ++++++++++ 6 files changed, 900 insertions(+), 303 deletions(-) create mode 100644 components/bidding/receive/bidding-participants-dialog.tsx (limited to 'components') diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index f298721b..bb7880f5 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { UseFormReturn } from 'react-hook-form' -import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign, Plus } from 'lucide-react' +import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign, Plus, Check, ChevronsUpDown } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' @@ -26,6 +26,20 @@ import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { cn } from '@/lib/utils' import type { CreateBiddingSchema } from '@/lib/bidding/validation' import { contractTypeLabels, biddingTypeLabels, awardCountLabels, biddingNoticeTypeLabels } from '@/db/schema' @@ -589,37 +603,62 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control} name="biddingConditions.paymentTerms" render={({ field }) => ( - + SHI 지급조건 * - - - + + + + + + + + + + + 검색 결과가 없습니다. + + {paymentTermsOptions.map((option) => ( + { + setBiddingConditions(prev => ({ + ...prev, + paymentTerms: option.code + })) + field.onChange(option.code) + }} + > + + {option.code} {option.description && `(${option.description})`} + + ))} + + + + + )} @@ -632,37 +671,62 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control} name="biddingConditions.incoterms" render={({ field }) => ( - + SHI 인도조건 * - + + + + + + + + + + + 검색 결과가 없습니다. + + {incotermsOptions.map((option) => ( + { + setBiddingConditions(prev => ({ + ...prev, + incoterms: option.code + })) + field.onChange(option.code) + }} + > + + {option.code} {option.description && `(${option.description})`} + + ))} + + + + + )} @@ -699,31 +763,60 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control} name="biddingConditions.taxConditions" render={({ field }) => ( - + SHI 매입부가가치세 * - - - + + + + + + + + + + + 검색 결과가 없습니다. + + {TAX_CONDITIONS.map((condition) => ( + { + setBiddingConditions(prev => ({ + ...prev, + taxConditions: condition.code + })) + field.onChange(condition.code) + }} + > + + {condition.name} + + ))} + + + + + )} @@ -733,37 +826,62 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control} name="biddingConditions.shippingPort" render={({ field }) => ( - + SHI 선적지 - + + + + + + + + + + + 검색 결과가 없습니다. + + {shippingPlaces.map((place) => ( + { + setBiddingConditions(prev => ({ + ...prev, + shippingPort: place.code + })) + field.onChange(place.code) + }} + > + + {place.code} {place.description && `(${place.description})`} + + ))} + + + + + )} @@ -776,43 +894,68 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp control={form.control} name="biddingConditions.destinationPort" render={({ field }) => ( - + SHI 하역지 - + + + + + + + + + + + 검색 결과가 없습니다. + + {destinationPlaces.map((place) => ( + { + setBiddingConditions(prev => ({ + ...prev, + destinationPort: place.code + })) + field.onChange(place.code) + }} + > + + {place.code} {place.description && `(${place.description})`} + + ))} + + + + + )} /> - {/* ( @@ -829,6 +972,8 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp })) field.onChange(e.target.value) }} + min="1900-01-01" + max="2100-12-31" /> @@ -849,7 +994,12 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp 계약기간 시작 - + @@ -866,7 +1016,12 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp 계약기간 종료 - + diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index 90923825..27a2c097 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useForm } from 'react-hook-form' -import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign } from 'lucide-react' +import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign, Check, ChevronsUpDown } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' @@ -25,6 +25,20 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { cn } from '@/lib/utils' // CreateBiddingInput 타입 정의가 없으므로 CreateBiddingSchema를 확장하여 사용합니다. import { getBiddingById, updateBiddingBasicInfo, getBiddingConditions, getBiddingNotice, updateBiddingConditions, getBiddingNoticeTemplate } from '@/lib/bidding/service' import { getPurchaseGroupCodes } from '@/components/common/selectors/purchase-group-code' @@ -270,7 +284,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB } // Procurement 데이터 로드 - const [paymentTermsData, incotermsData, shippingData, destinationData, purchaseGroupCodes, procurementManagers] = await Promise.all([ + const [paymentTermsData, incotermsData, shippingData, destinationData] = await Promise.all([ getPaymentTermsForSelection().catch(() => []), getIncotermsForSelection().catch(() => []), getPlaceOfShippingForSelection().catch(() => []), @@ -284,14 +298,20 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB DISPLAY_NAME: bidding.bidPicName || '', PURCHASE_GROUP_CODE: bidding.bidPicCode || '', user: { - id: bidding.bidPicUserId || undefined, + id: bidding.bidPicId || undefined, + name: bidding.bidPicName || '', + email: '', + employeeNumber: null, } }) setSelectedSupplyPic({ DISPLAY_NAME: bidding.supplyPicName || '', PROCUREMENT_MANAGER_CODE: bidding.supplyPicCode || '', user: { - id: bidding.supplyPicUserId || undefined, + id: bidding.supplyPicId || undefined, + name: bidding.supplyPicName || '', + email: '', + employeeNumber: null, } }) // // 입찰담당자 및 조달담당자 초기 선택값 설정 @@ -554,7 +574,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB ( 입찰유형 - @@ -575,7 +595,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB ( 계약구분 - @@ -603,7 +623,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB 기타 입찰유형 * - + @@ -656,7 +676,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB ( 낙찰업체 수 - @@ -691,6 +711,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB field.onChange(code.DISPLAY_NAME || '') }} placeholder="입찰담당자 선택" + disabled={readonly} /> @@ -711,6 +732,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB field.onChange(manager.DISPLAY_NAME || '') }} placeholder="조달담당자 선택" + disabled={readonly} /> @@ -723,7 +745,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB 구매조직 * - @@ -747,7 +769,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB ( 통화 - @@ -770,7 +792,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB ( 구매유형 * - @@ -801,7 +823,13 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB 계약기간 시작 - + @@ -814,7 +842,13 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB 계약기간 종료 - + @@ -853,91 +887,173 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB {/* 1행: SHI 지급조건, SHI 매입부가가치세 */}
-
+
SHI 지급조건 * - + + + + + + + + + 검색 결과가 없습니다. + + {paymentTermsOptions.map((option) => ( + { + setBiddingConditions(prev => ({ + ...prev, + paymentTerms: option.code + })) + }} + > + + {option.code} {option.description && `(${option.description})`} + + ))} + + + + +
-
+
SHI 매입부가가치세 * - + + + + + + + + + 검색 결과가 없습니다. + + {TAX_CONDITIONS.map((condition) => ( + { + setBiddingConditions(prev => ({ + ...prev, + taxConditions: condition.code + })) + }} + > + + {condition.name} + + ))} + + + + +
{/* 2행: SHI 인도조건, SHI 인도조건2 */}
-
+
SHI 인도조건 * - + + + + + + + + + 검색 결과가 없습니다. + + {incotermsOptions.map((option) => ( + { + setBiddingConditions(prev => ({ + ...prev, + incoterms: option.code + })) + }} + > + + {option.code} {option.description && `(${option.description})`} + + ))} + + + + +
@@ -951,70 +1067,123 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB incotermsOption: e.target.value })) }} + disabled={readonly} />
{/* 3행: SHI 선적지, SHI 하역지 */}
-
+
SHI 선적지 - + + + + + + + + + 검색 결과가 없습니다. + + {shippingPlaces.map((place) => ( + { + setBiddingConditions(prev => ({ + ...prev, + shippingPort: place.code + })) + }} + > + + {place.code} {place.description && `(${place.description})`} + + ))} + + + + +
-
+
SHI 하역지 - + + + + + + + + + 검색 결과가 없습니다. + + {destinationPlaces.map((place) => ( + { + setBiddingConditions(prev => ({ + ...prev, + destinationPort: place.code + })) + }} + > + + {place.code} {place.description && `(${place.description})`} + + ))} + + + + +
@@ -1045,6 +1214,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB })) }} id="price-adjustment" + disabled={readonly} /> {biddingConditions.isPriceAdjustmentApplicable ? "적용" : "미적용"} @@ -1067,7 +1237,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB })) }} rows={3} - readOnly={readonly} + disabled={readonly} />
@@ -1135,15 +1305,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }} onDropRejected={() => { toast({ - title: "File upload rejected", - description: "Please check file size and type.", + title: "파일 업로드 거부", + description: "파일 크기와 유형을 확인해주세요.", variant: "destructive", }) }} + disabled={readonly} > - + 파일을 드래그하거나 클릭하여 업로드 @@ -1194,6 +1365,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB variant="ghost" size="sm" onClick={() => handleDeleteDocument(doc.id)} + disabled={readonly} > 삭제 @@ -1227,15 +1399,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }} onDropRejected={() => { toast({ - title: "File upload rejected", - description: "Please check file size and type.", + title: "파일 업로드 거부", + description: "파일 크기와 유형을 확인해주세요.", variant: "destructive", }) }} + disabled={readonly} > - + 파일을 드래그하거나 클릭하여 업로드 @@ -1281,6 +1454,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB variant="ghost" size="sm" onClick={() => handleDeleteDocument(doc.id)} + disabled={readonly} > 삭제 diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index f6b3a3f0..6634f528 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -494,7 +494,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC

{!readonly && ( - @@ -532,6 +532,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC handleVendorSelect(vendor)} + disabled={readonly} /> {vendor.vendorName} @@ -565,6 +566,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC onCheckedChange={(checked) => handleTogglePriceAdjustmentQuestion(vendor.id, checked as boolean) } + disabled={readonly} /> {vendor.isPriceAdjustmentApplicableQuestion ? '예' : '아니오'} @@ -577,6 +579,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC size="sm" onClick={() => handleRemoveVendor(vendor.id)} className="text-red-600 hover:text-red-800" + disabled={readonly} > @@ -607,6 +610,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC variant="outline" onClick={handleOpenAddContactFromVendor} className="flex items-center gap-2" + disabled={readonly} > 업체 담당자 추가 @@ -614,6 +618,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index ef0aa568..9d858f40 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -807,7 +807,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems setRepresentativeItem(item.id)} - disabled={items.length <= 1 && item.isRepresentative} + disabled={(items.length <= 1 && item.isRepresentative) || readonly} title="대표 아이템" /> @@ -831,6 +831,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems } }} placeholder="프로젝트 선택" + disabled={readonly} /> @@ -942,21 +943,25 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems updatePRItem(item.id, { quantity: e.target.value })} className="h-8 text-xs" required + disabled={readonly} /> ) : ( updatePRItem(item.id, { totalWeight: e.target.value })} className="h-8 text-xs" required + disabled={readonly} /> )} @@ -966,6 +971,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.quantityUnit || 'EA'} onValueChange={(value) => updatePRItem(item.id, { quantityUnit: value })} required + disabled={readonly} > @@ -984,6 +990,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.weightUnit || 'KG'} onValueChange={(value) => updatePRItem(item.id, { weightUnit: value })} required + disabled={readonly} > @@ -1004,6 +1011,9 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} className="h-8 text-xs" required + disabled={readonly} + min="1900-01-01" + max="2100-12-31" /> @@ -1015,12 +1025,14 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.priceUnit || ''} onChange={(e) => updatePRItem(item.id, { priceUnit: e.target.value })} className="h-8 text-xs" + disabled={readonly} /> updatePRItem(item.id, { materialWeight: e.target.value })} className="h-8 text-xs" + disabled={readonly} /> @@ -1057,6 +1070,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={formatNumberWithCommas(item.targetUnitPrice)} onChange={(e) => updatePRItem(item.id, { targetUnitPrice: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" + disabled={readonly} /> @@ -1072,6 +1086,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems updatePRItem(item.id, { budgetCurrency: value })} + disabled={readonly} > @@ -1116,12 +1133,14 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={formatNumberWithCommas(item.actualAmount)} onChange={(e) => updatePRItem(item.id, { actualAmount: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" + disabled={readonly} />