diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
| commit | cf8dac0c6490469dab88a560004b0c07dbd48612 (patch) | |
| tree | b9e76061e80d868331e6b4277deecb9086f845f3 /lib/rfq-last/quotation-compare-view.tsx | |
| parent | e5745fc0268bbb5770bc14a55fd58a0ec30b466e (diff) | |
(대표님) rfq, 계약, 서명 등
Diffstat (limited to 'lib/rfq-last/quotation-compare-view.tsx')
| -rw-r--r-- | lib/rfq-last/quotation-compare-view.tsx | 447 |
1 files changed, 385 insertions, 62 deletions
diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index 491a1962..28c8b3b1 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -46,6 +46,7 @@ import { import { ComparisonData, selectVendor, cancelVendorSelection } from "./compare-action"; import { createPO, createGeneralContract, createBidding } from "./contract-actions"; import { toast } from "sonner"; +import { useRouter } from "next/navigation" interface QuotationCompareViewProps { data: ComparisonData; @@ -61,6 +62,61 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { const [selectionReason, setSelectionReason] = React.useState(""); const [cancelReason, setCancelReason] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); + const router = useRouter() + + const [selectedGeneralContractType, setSelectedGeneralContractType] = React.useState(""); + const [contractStartDate, setContractStartDate] = React.useState(""); + const [contractEndDate, setContractEndDate] = React.useState(""); + + // 계약종류 옵션 + const contractTypes = [ + { value: 'UP', label: 'UP - 자재단가계약' }, + { value: 'LE', label: 'LE - 임대차계약' }, + { value: 'IL', label: 'IL - 개별운송계약' }, + { value: 'AL', label: 'AL - 연간운송계약' }, + { value: 'OS', label: 'OS - 외주용역계약' }, + { value: 'OW', label: 'OW - 도급계약' }, + { value: 'IS', label: 'IS - 검사계약' }, + { value: 'LO', label: 'LO - LOI (의향서)' }, + { value: 'FA', label: 'FA - Frame Agreement' }, + { value: 'SC', label: 'SC - 납품합의계약' }, + { value: 'OF', label: 'OF - 클레임상계계약' }, + { value: 'AW', label: 'AW - 사전작업합의' }, + { value: 'AD', label: 'AD - 사전납품합의' }, + { value: 'AM', label: 'AM - 설계계약' }, + { value: 'SC_SELL', label: 'SC - 폐기물매각계약' }, + ]; + + // 입찰 관련 state 추가 + const [biddingContractType, setBiddingContractType] = React.useState<"unit_price" | "general" | "sale" | "">(""); + const [biddingType, setBiddingType] = React.useState<string>(""); + const [awardCount, setAwardCount] = React.useState<"single" | "multiple">("single"); + const [biddingStartDate, setBiddingStartDate] = React.useState(""); + const [biddingEndDate, setBiddingEndDate] = React.useState(""); + + // 입찰 옵션들 + const biddingContractTypes = [ + { value: 'unit_price', label: '단가계약' }, + { value: 'general', label: '일반계약' }, + { value: 'sale', label: '매각계약' } + ]; + + const biddingTypes = [ + { value: 'equipment', label: '기자재' }, + { value: 'construction', label: '공사' }, + { value: 'service', label: '용역' }, + { value: 'lease', label: '임차' }, + { value: 'steel_stock', label: '형강스톡' }, + { value: 'piping', label: '배관' }, + { value: 'transport', label: '운송' }, + { value: 'waste', label: '폐기물' }, + { value: 'sale', label: '매각' } + ]; + + const awardCounts = [ + { value: 'single', label: '단수' }, + { value: 'multiple', label: '복수' } + ]; // 선정된 업체 정보 확인 const selectedVendor = data.vendors.find(v => v.isSelected); @@ -76,15 +132,56 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { return; } + // 일반계약인 경우 계약종류와 날짜 확인 + if (selectedContractType === "CONTRACT") { + if (!selectedGeneralContractType) { + toast.error("계약종류를 선택해주세요."); + return; + } + if (!contractStartDate) { + toast.error("계약 시작일을 선택해주세요."); + return; + } + if (!contractEndDate) { + toast.error("계약 종료일을 선택해주세요."); + return; + } + if (new Date(contractStartDate) >= new Date(contractEndDate)) { + toast.error("계약 종료일은 시작일보다 이후여야 합니다."); + return; + } + } + + // 입찰 검증 + if (selectedContractType === "BIDDING") { + if (!biddingContractType) { + toast.error("계약구분을 선택해주세요."); + return; + } + if (!biddingType) { + toast.error("입찰유형을 선택해주세요."); + return; + } + if (!biddingStartDate || !biddingEndDate) { + toast.error("입찰 기간을 입력해주세요."); + return; + } + if (new Date(biddingStartDate) >= new Date(biddingEndDate)) { + toast.error("입찰 마감일은 시작일보다 이후여야 합니다."); + return; + } + } + if (!selectedVendor) { toast.error("선정된 업체가 없습니다."); return; } setIsSubmitting(true); + try { let result; - + switch (selectedContractType) { case "PO": result = await createPO({ @@ -94,9 +191,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, selectionReason: selectedVendor.selectionReason, + }); break; - + case "CONTRACT": result = await createGeneralContract({ rfqId: data.rfqInfo.id, @@ -104,9 +202,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, + contractStartDate: new Date(contractStartDate), + contractEndDate: new Date(contractEndDate), + contractType: selectedGeneralContractType, // 계약종류 추가 + }); break; - + case "BIDDING": result = await createBidding({ rfqId: data.rfqInfo.id, @@ -114,9 +216,14 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, + contractType: biddingContractType, + biddingType: biddingType, + awardCount: awardCount, + biddingStartDate: new Date(biddingStartDate), + biddingEndDate: new Date(biddingEndDate), }); break; - + default: throw new Error("올바른 계약 유형이 아닙니다."); } @@ -124,8 +231,17 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { if (result.success) { toast.success(result.message || "계약 프로세스가 시작되었습니다."); setShowContractDialog(false); + // 모든 state 초기화 setSelectedContractType(""); - window.location.reload(); + setSelectedGeneralContractType(""); + setContractStartDate(""); + setContractEndDate(""); + setBiddingContractType(""); + setBiddingType(""); + setAwardCount("single"); + setBiddingStartDate(""); + setBiddingEndDate(""); + router.refresh(); } else { throw new Error(result.error || "계약 진행 중 오류가 발생했습니다."); } @@ -224,7 +340,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { toast.success("업체가 성공적으로 선정되었습니다."); setShowSelectionDialog(false); setSelectionReason(""); - window.location.reload(); // 페이지 새로고침으로 선정 상태 반영 + router.refresh() } else { throw new Error(result.error || "업체 선정 중 오류가 발생했습니다."); } @@ -246,13 +362,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { setIsSubmitting(true); try { // 파라미터를 올바르게 전달 - const result = await cancelVendorSelection(Number(data.rfqInfo.id),cancelReason); + const result = await cancelVendorSelection(Number(data.rfqInfo.id), cancelReason); if (result.success) { toast.success("업체 선정이 취소되었습니다."); setShowCancelDialog(false); setCancelReason(""); - window.location.reload(); + router.refresh() } else { throw new Error(result.error || "선정 취소 중 오류가 발생했습니다."); } @@ -312,13 +428,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {hasSelection && ( <Alert className={cn( "border-2", - hasContract + hasContract ? "border-purple-500 bg-purple-50" - : isSelectionApproved - ? "border-green-500 bg-green-50" - : isPendingApproval - ? "border-yellow-500 bg-yellow-50" - : "border-blue-500 bg-blue-50" + : isSelectionApproved + ? "border-green-500 bg-green-50" + : isPendingApproval + ? "border-yellow-500 bg-yellow-50" + : "border-blue-500 bg-blue-50" )}> <div className="flex items-start justify-between"> <div className="flex gap-3"> @@ -335,11 +451,11 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <AlertTitle className="text-lg"> {hasContract ? "계약 진행중" - : isSelectionApproved - ? "업체 선정 승인 완료" - : isPendingApproval - ? "업체 선정 승인 대기중" - : "업체 선정 완료"} + : isSelectionApproved + ? "업체 선정 승인 완료" + : isPendingApproval + ? "업체 선정 승인 대기중" + : "업체 선정 완료"} </AlertTitle> <AlertDescription className="space-y-1"> <p className="font-semibold">선정 업체: {selectedVendor.vendorName} ({selectedVendor.vendorCode})</p> @@ -521,13 +637,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { key={vendor.vendorId} className={cn( "flex items-center justify-between p-4 border rounded-lg transition-colors", - vendor.isSelected - ? "bg-blue-100 border-blue-400 border-2" + vendor.isSelected + ? "bg-blue-100 border-blue-400 border-2" : hasSelection - ? "opacity-60" - : selectedVendorId === vendor.vendorId.toString() - ? "bg-blue-50 border-blue-300 cursor-pointer" - : "hover:bg-gray-50 cursor-pointer" + ? "opacity-60" + : selectedVendorId === vendor.vendorId.toString() + ? "bg-blue-50 border-blue-300 cursor-pointer" + : "hover:bg-gray-50 cursor-pointer" )} onClick={() => { if (!hasSelection) { @@ -683,10 +799,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </TooltipContent> </Tooltip> </TooltipProvider> - {vendor.vendorConditions.paymentTermsCode && - vendor.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} + {vendor.vendorConditions.paymentTermsCode && + vendor.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} </div> </td> ))} @@ -770,8 +886,8 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {quote.deliveryDate ? format(new Date(quote.deliveryDate), "yyyy-MM-dd") : quote.leadTime - ? `${quote.leadTime}일` - : "-"} + ? `${quote.leadTime}일` + : "-"} </td> <td className="p-2"> {quote.manufacturer && ( @@ -817,7 +933,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <div> <p className="text-muted-foreground">가격 차이</p> <p className="font-semibold"> - {((item.priceAnalysis.highestPrice - item.priceAnalysis.lowestPrice) / + {((item.priceAnalysis.highestPrice - item.priceAnalysis.lowestPrice) / item.priceAnalysis.lowestPrice * 100).toFixed(1)}% </p> </div> @@ -848,7 +964,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <div className="space-y-3"> {data.vendors.map((vendor) => { if (!vendor.conditionDifferences.hasDifferences) return null; - + return ( <div key={vendor.vendorId} className="p-3 border rounded-lg"> <p className="font-medium mb-2">{vendor.vendorName}</p> @@ -930,12 +1046,12 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { // 가격 순위와 조건 차이를 고려한 점수 계산 const scoredVendors = data.vendors.map(v => ({ ...v, - score: (v.rank || 10) + v.conditionDifferences.criticalDifferences.length * 3 + - v.conditionDifferences.differences.length + score: (v.rank || 10) + v.conditionDifferences.criticalDifferences.length * 3 + + v.conditionDifferences.differences.length })); scoredVendors.sort((a, b) => a.score - b.score); const recommended = scoredVendors[0]; - + return ( <div> <p className="text-sm text-purple-700"> @@ -963,7 +1079,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <div className="space-y-4"> {data.vendors.map((vendor) => { if (!vendor.generalRemark && !vendor.technicalProposal) return null; - + return ( <div key={vendor.vendorId} className="border rounded-lg p-4"> <p className="font-medium mb-2">{vendor.vendorName}</p> @@ -996,7 +1112,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <h3 className="text-lg font-semibold mb-4"> {hasSelection ? "업체 재선정 확인" : "업체 선정 확인"} </h3> - + {selectedVendorId && ( <div className="space-y-4"> <div className="rounded-lg border p-4"> @@ -1076,7 +1192,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center"> <div className="bg-white rounded-lg p-6 max-w-lg w-full mx-4"> <h3 className="text-lg font-semibold mb-4">업체 선정 취소</h3> - + <Alert className="mb-4"> <AlertTriangle className="h-4 w-4" /> <AlertDescription> @@ -1141,13 +1257,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 계약 진행 모달 */} {showContractDialog && ( <div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center"> - <div className="bg-white rounded-lg p-6 max-w-lg w-full mx-4"> + <div className="bg-white rounded-lg p-6 max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto"> <h3 className="text-lg font-semibold mb-4"> {selectedContractType === "PO" && "PO (SAP) 생성"} {selectedContractType === "CONTRACT" && "일반계약 생성"} {selectedContractType === "BIDDING" && "입찰 생성"} </h3> - + {selectedVendor && ( <div className="space-y-4"> <div className="rounded-lg border p-4 bg-gray-50"> @@ -1180,32 +1296,222 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </AlertDescription> </Alert> - {/* 추가 옵션이 필요한 경우 여기에 추가 */} + {/* 일반계약 선택 시 계약종류 및 기간 선택 */} {selectedContractType === "CONTRACT" && ( - <div className="space-y-2"> - <p className="text-sm font-medium">계약 옵션</p> - <div className="space-y-2 text-sm"> - <label className="flex items-center gap-2"> - <input type="checkbox" className="rounded" /> - <span>표준계약서 사용</span> - </label> - <label className="flex items-center gap-2"> - <input type="checkbox" className="rounded" /> - <span>전자서명 요청</span> + <div className="space-y-4"> + {/* 계약종류 선택 */} + <div className="space-y-2"> + <label htmlFor="contract-type" className="text-sm font-medium"> + 계약종류 선택 * </label> + <select + id="contract-type" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + value={selectedGeneralContractType} + onChange={(e) => setSelectedGeneralContractType(e.target.value)} + required + > + <option value="">계약종류를 선택하세요</option> + {contractTypes.map(type => ( + <option key={type.value} value={type.value}> + {type.label} + </option> + ))} + </select> + </div> + + {/* 계약 기간 */} + <div className="grid grid-cols-2 gap-3"> + <div className="space-y-2"> + <label htmlFor="start-date" className="text-sm font-medium"> + 계약 시작일 * + </label> + <input + id="start-date" + type="date" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + value={contractStartDate} + onChange={(e) => setContractStartDate(e.target.value)} + min={new Date().toISOString().split('T')[0]} + required + /> + </div> + + <div className="space-y-2"> + <label htmlFor="end-date" className="text-sm font-medium"> + 계약 종료일 * + </label> + <input + id="end-date" + type="date" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + value={contractEndDate} + onChange={(e) => setContractEndDate(e.target.value)} + min={contractStartDate || new Date().toISOString().split('T')[0]} + required + /> + </div> + </div> + + {/* 계약 기간 표시 */} + {contractStartDate && contractEndDate && ( + <div className="p-3 bg-blue-50 rounded-md"> + <p className="text-sm text-blue-700"> + 계약 기간: {format(new Date(contractStartDate), "yyyy년 MM월 dd일", { locale: ko })} ~ {format(new Date(contractEndDate), "yyyy년 MM월 dd일", { locale: ko })} + <span className="ml-2 font-medium"> + ({Math.ceil((new Date(contractEndDate).getTime() - new Date(contractStartDate).getTime()) / (1000 * 60 * 60 * 24))}일간) + </span> + </p> + </div> + )} + + {/* 계약 옵션 */} + <div className="space-y-2"> + <p className="text-sm font-medium">추가 옵션</p> + <div className="space-y-2 text-sm"> + <label className="flex items-center gap-2"> + <input type="checkbox" className="rounded" /> + <span>표준계약서 사용</span> + </label> + <label className="flex items-center gap-2"> + <input type="checkbox" className="rounded" /> + <span>전자서명 요청</span> + </label> + <label className="flex items-center gap-2"> + <input type="checkbox" className="rounded" /> + <span>자동 연장 조항 포함</span> + </label> + </div> </div> </div> )} - + {/* 입찰 옵션 */} {selectedContractType === "BIDDING" && ( - <div className="space-y-2"> - <p className="text-sm font-medium">입찰 유형</p> - <select className="w-full px-3 py-2 border rounded-md"> - <option value="">선택하세요</option> - <option value="open">공개입찰</option> - <option value="limited">제한입찰</option> - <option value="private">지명입찰</option> - </select> + <div className="space-y-4"> + {/* 계약구분 선택 */} + <div className="space-y-2"> + <label htmlFor="bidding-contract-type" className="text-sm font-medium"> + 계약구분 * + </label> + <select + id="bidding-contract-type" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500" + value={biddingContractType} + onChange={(e) => setBiddingContractType(e.target.value as any)} + required + > + <option value="">계약구분을 선택하세요</option> + {biddingContractTypes.map(type => ( + <option key={type.value} value={type.value}> + {type.label} + </option> + ))} + </select> + </div> + + {/* 입찰유형 선택 */} + <div className="space-y-2"> + <label htmlFor="bidding-type" className="text-sm font-medium"> + 입찰유형 * + </label> + <select + id="bidding-type" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500" + value={biddingType} + onChange={(e) => setBiddingType(e.target.value)} + required + > + <option value="">입찰유형을 선택하세요</option> + {biddingTypes.map(type => ( + <option key={type.value} value={type.value}> + {type.label} + </option> + ))} + </select> + </div> + + {/* 낙찰수 선택 */} + <div className="space-y-2"> + <label htmlFor="award-count" className="text-sm font-medium"> + 낙찰수 + </label> + <select + id="award-count" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500" + value={awardCount} + onChange={(e) => setAwardCount(e.target.value as any)} + > + {awardCounts.map(type => ( + <option key={type.value} value={type.value}> + {type.label} + </option> + ))} + </select> + </div> + + {/* 입찰 기간 */} + <div className="grid grid-cols-2 gap-3"> + <div className="space-y-2"> + <label htmlFor="bidding-start-date" className="text-sm font-medium"> + 입찰 시작일 * + </label> + <input + id="bidding-start-date" + type="date" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500" + value={biddingStartDate} + onChange={(e) => setBiddingStartDate(e.target.value)} + min={new Date().toISOString().split('T')[0]} + required + /> + </div> + + <div className="space-y-2"> + <label htmlFor="bidding-end-date" className="text-sm font-medium"> + 입찰 마감일 * + </label> + <input + id="bidding-end-date" + type="date" + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500" + value={biddingEndDate} + onChange={(e) => setBiddingEndDate(e.target.value)} + min={biddingStartDate || new Date().toISOString().split('T')[0]} + required + /> + </div> + </div> + + {/* 입찰 기간 표시 */} + {biddingStartDate && biddingEndDate && ( + <div className="p-3 bg-purple-50 rounded-md"> + <p className="text-sm text-purple-700"> + 입찰 기간: {format(new Date(biddingStartDate), "yyyy년 MM월 dd일", { locale: ko })} ~ {format(new Date(biddingEndDate), "yyyy년 MM월 dd일", { locale: ko })} + <span className="ml-2 font-medium"> + ({Math.ceil((new Date(biddingEndDate).getTime() - new Date(biddingStartDate).getTime()) / (1000 * 60 * 60 * 24))}일간) + </span> + </p> + </div> + )} + + {/* 추가 옵션 */} + <div className="space-y-2"> + <p className="text-sm font-medium">추가 옵션</p> + <div className="space-y-2 text-sm"> + <label className="flex items-center gap-2"> + <input type="checkbox" className="rounded" /> + <span>긴급입찰</span> + </label> + <label className="flex items-center gap-2"> + <input type="checkbox" className="rounded" /> + <span>사양설명회 개최</span> + </label> + <label className="flex items-center gap-2"> + <input type="checkbox" className="rounded" /> + <span>공개입찰</span> + </label> + </div> + </div> </div> )} </div> @@ -1216,7 +1522,16 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { variant="outline" onClick={() => { setShowContractDialog(false); + // 모든 state 초기화 setSelectedContractType(""); + setSelectedGeneralContractType(""); + setContractStartDate(""); + setContractEndDate(""); + setBiddingContractType(""); + setBiddingType(""); + setAwardCount("single"); + setBiddingStartDate(""); + setBiddingEndDate(""); }} disabled={isSubmitting} > @@ -1224,7 +1539,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </Button> <Button onClick={handleContractCreation} - disabled={isSubmitting} + disabled={ + isSubmitting || + (selectedContractType === "CONTRACT" && + (!selectedGeneralContractType || !contractStartDate || !contractEndDate)) || + (selectedContractType === "BIDDING" && + (!biddingContractType || !biddingType || !biddingStartDate || !biddingEndDate)) + } > {isSubmitting ? "처리 중..." : "진행"} </Button> @@ -1232,6 +1553,8 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </div> </div> )} + + </div> ); }
\ No newline at end of file |
