diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-03 10:35:57 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-03 10:35:57 +0000 |
| commit | a2bc455f654e011c53968b0d3a14389d7259847e (patch) | |
| tree | 6ff60b8ef0880aaa4cf2c9d4f234772fb0a74537 /lib/bidding/list | |
| parent | bfe354f7633f62350e61eb784cbf1926079339d1 (diff) | |
(최겸) 구매 입찰 개발(벤더 응찰 개발 및 기본계약 요청 개발 필)
Diffstat (limited to 'lib/bidding/list')
| -rw-r--r-- | lib/bidding/list/biddings-table-columns.tsx | 4 | ||||
| -rw-r--r-- | lib/bidding/list/create-bidding-dialog.tsx | 253 |
2 files changed, 238 insertions, 19 deletions
diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index c936de33..48a77954 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -558,7 +558,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef <Edit className="mr-2 h-4 w-4" /> 수정 </DropdownMenuItem> - <DropdownMenuSeparator /> + {/* <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => setRowAction({ row, type: "copy" })}> <Package className="mr-2 h-4 w-4" /> 복사 생성 @@ -566,7 +566,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef <DropdownMenuItem onClick={() => setRowAction({ row, type: "manage_companies" })}> <Users className="mr-2 h-4 w-4" /> 참여업체 관리 - </DropdownMenuItem> + </DropdownMenuItem> */} </DropdownMenuContent> </DropdownMenu> ), diff --git a/lib/bidding/list/create-bidding-dialog.tsx b/lib/bidding/list/create-bidding-dialog.tsx index 90204dc9..e5bfcae4 100644 --- a/lib/bidding/list/create-bidding-dialog.tsx +++ b/lib/bidding/list/create-bidding-dialog.tsx @@ -120,7 +120,7 @@ interface PRItemInfo { } // 탭 순서 정의 -const TAB_ORDER = ["basic", "contract", "schedule", "details", "manager"] as const +const TAB_ORDER = ["basic", "contract", "schedule", "conditions", "details", "manager"] as const type TabType = typeof TAB_ORDER[number] export function CreateBiddingDialog() { @@ -154,6 +154,18 @@ export function CreateBiddingDialog() { // 파일 첨부를 위해 선택된 아이템 ID const [selectedItemForFile, setSelectedItemForFile] = React.useState<string | null>(null) + // 입찰 조건 상태 + const [biddingConditions, setBiddingConditions] = React.useState({ + paymentTerms: "", + taxConditions: "", + incoterms: "", + contractDeliveryDate: "", + shippingPort: "", + destinationPort: "", + isPriceAdjustmentApplicable: false, + sparePartOptions: "", + }) + // 사양설명회 파일 추가 const addMeetingFiles = (files: File[]) => { setSpecMeetingInfo(prev => ({ @@ -257,6 +269,12 @@ export function CreateBiddingDialog() { (specMeetingInfo.meetingDate && specMeetingInfo.location && specMeetingInfo.contactPerson)), hasErrors: !!(formErrors.submissionStartDate || formErrors.submissionEndDate) }, + conditions: { + isValid: biddingConditions.paymentTerms.trim() !== "" && + biddingConditions.taxConditions.trim() !== "" && + biddingConditions.incoterms.trim() !== "", + hasErrors: false + }, details: { isValid: true, // 세부내역은 선택사항 hasErrors: false @@ -405,6 +423,8 @@ export function CreateBiddingDialog() { } else { toast.error("제출 시작일시와 마감일시를 입력해주세요") } + } else if (activeTab === "conditions") { + toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건)") } return } @@ -444,6 +464,7 @@ export function CreateBiddingDialog() { meetingFiles: specMeetingInfo.meetingFiles } : null, prItems: prItems.length > 0 ? prItems : [], + biddingConditions: biddingConditions, } const result = await createBidding(extendedData, userId) @@ -517,6 +538,16 @@ export function CreateBiddingDialog() { }) setPrItems([]) setSelectedItemForFile(null) + setBiddingConditions({ + paymentTerms: "", + taxConditions: "", + incoterms: "", + contractDeliveryDate: "", + shippingPort: "", + destinationPort: "", + isPriceAdjustmentApplicable: false, + sparePartOptions: "", + }) setActiveTab("basic") setShowSuccessDialog(false) // 추가 setCreatedBiddingId(null) // 추가 @@ -545,7 +576,7 @@ export function CreateBiddingDialog() { // 성공 다이얼로그 핸들러들 const handleNavigateToDetail = () => { if (createdBiddingId) { - router.push(`/evcp/biddings/${createdBiddingId}`) + router.push(`/evcp/bid/${createdBiddingId}`) } setShowSuccessDialog(false) setCreatedBiddingId(null) @@ -566,7 +597,7 @@ export function CreateBiddingDialog() { 신규 입찰 </Button> </DialogTrigger> - <DialogContent className="max-w-6xl h-[90vh] p-0 flex flex-col"> + <DialogContent className="max-w-7xl h-[90vh] p-0 flex flex-col"> {/* 고정 헤더 */} <div className="flex-shrink-0 p-6 border-b"> <DialogHeader> @@ -586,29 +617,87 @@ export function CreateBiddingDialog() { {/* 탭 영역 */} <div className="flex-1 overflow-hidden"> <Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col"> - <div className="px-6 pt-4"> - <TabsList className="grid w-full grid-cols-5"> - <TabsTrigger value="basic" className="relative"> - 기본 정보 + <div className="px-6"> + <div className="flex space-x-1 bg-muted p-1 rounded-lg overflow-x-auto"> + <button + type="button" + onClick={() => setActiveTab("basic")} + className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ + activeTab === "basic" + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + }`} + > + 기본정보 {!tabValidation.basic.isValid && ( <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> )} - </TabsTrigger> - <TabsTrigger value="contract" className="relative"> - 계약 정보 + </button> + <button + type="button" + onClick={() => setActiveTab("contract")} + className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ + activeTab === "contract" + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + }`} + > + 계약정보 {!tabValidation.contract.isValid && ( <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> )} - </TabsTrigger> - <TabsTrigger value="schedule" className="relative"> - 일정 & 회의 + </button> + <button + type="button" + onClick={() => setActiveTab("schedule")} + className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ + activeTab === "schedule" + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + }`} + > + 일정회의 {!tabValidation.schedule.isValid && ( <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> )} - </TabsTrigger> - <TabsTrigger value="details">세부내역</TabsTrigger> - <TabsTrigger value="manager">담당자 & 기타</TabsTrigger> - </TabsList> + </button> + <button + type="button" + onClick={() => setActiveTab("conditions")} + className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ + activeTab === "conditions" + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + }`} + > + 입찰조건 + {!tabValidation.conditions.isValid && ( + <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> + )} + </button> + <button + type="button" + onClick={() => setActiveTab("details")} + className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ + activeTab === "details" + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + }`} + > + 세부내역 + </button> + <button + type="button" + onClick={() => setActiveTab("manager")} + className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ + activeTab === "manager" + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + }`} + > + 담당자 + </button> + </div> </div> <div className="flex-1 overflow-y-auto p-6"> @@ -1193,6 +1282,128 @@ export function CreateBiddingDialog() { </Card> </TabsContent> + {/* 입찰 조건 탭 */} + <TabsContent value="conditions" className="mt-0 space-y-6"> + <Card> + <CardHeader> + <CardTitle>입찰 조건</CardTitle> + <p className="text-sm text-muted-foreground"> + 벤더가 사전견적 시 참고할 입찰 조건을 설정하세요 + </p> + </CardHeader> + <CardContent className="space-y-6"> + <div className="grid grid-cols-2 gap-6"> + <div className="space-y-2"> + <label className="text-sm font-medium"> + 지급조건 <span className="text-red-500">*</span> + </label> + <Input + placeholder="예: 월말결제, 60일" + value={biddingConditions.paymentTerms} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + paymentTerms: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium"> + 세금조건 <span className="text-red-500">*</span> + </label> + <Input + + value={biddingConditions.taxConditions} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + taxConditions: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium"> + 운송조건(인코텀즈) <span className="text-red-500">*</span> + </label> + <Input + placeholder="예: FOB, CIF 등" + value={biddingConditions.incoterms} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + incoterms: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium"> + 계약 납품일 + </label> + <Input + type="date" + value={biddingConditions.contractDeliveryDate} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + contractDeliveryDate: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium">선적지</label> + <Input + placeholder="예: 부산항, 인천항" + value={biddingConditions.shippingPort} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + shippingPort: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium">도착지</label> + <Input + placeholder="예: 현장 직납, 창고 납품" + value={biddingConditions.destinationPort} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + destinationPort: e.target.value + }))} + /> + </div> + </div> + + <div className="flex items-center space-x-2"> + <Switch + id="price-adjustment" + checked={biddingConditions.isPriceAdjustmentApplicable} + onCheckedChange={(checked) => setBiddingConditions(prev => ({ + ...prev, + isPriceAdjustmentApplicable: checked + }))} + /> + <label htmlFor="price-adjustment" className="text-sm font-medium"> + 연동제 적용 가능 + </label> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium">스페어파트 옵션</label> + <Textarea + placeholder="스페어파트 관련 옵션을 입력하세요" + value={biddingConditions.sparePartOptions} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + sparePartOptions: e.target.value + }))} + rows={3} + /> + </div> + </CardContent> + </Card> + </TabsContent> + {/* 세부내역 탭 */} <TabsContent value="details" className="mt-0 space-y-6"> <Card> @@ -1657,6 +1868,14 @@ export function CreateBiddingDialog() { )} </span> )} + {activeTab === "conditions" && ( + <span> + 입찰 조건을 설정하세요 + {!tabValidation.conditions.isValid && ( + <span className="text-red-500 ml-2">• 필수 항목이 누락되었습니다</span> + )} + </span> + )} {activeTab === "details" && "세부내역 아이템을 관리하세요 (선택사항)"} {activeTab === "manager" && "담당자 정보를 확인하고 입찰을 생성하세요"} </div> |
