diff options
Diffstat (limited to 'lib/rfq-last/quotation-compare-view.tsx')
| -rw-r--r-- | lib/rfq-last/quotation-compare-view.tsx | 808 |
1 files changed, 645 insertions, 163 deletions
diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index 0e15a7bf..491a1962 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Trophy, TrendingUp, @@ -22,6 +23,11 @@ import { FileText, Truck, AlertTriangle, + Award, + UserCheck, + X, + RefreshCw, + Clock, } from "lucide-react"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; @@ -37,7 +43,9 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import type { ComparisonData, VendorComparison, PrItemComparison } from "../actions"; +import { ComparisonData, selectVendor, cancelVendorSelection } from "./compare-action"; +import { createPO, createGeneralContract, createBidding } from "./contract-actions"; +import { toast } from "sonner"; interface QuotationCompareViewProps { data: ComparisonData; @@ -45,7 +53,96 @@ interface QuotationCompareViewProps { export function QuotationCompareView({ data }: QuotationCompareViewProps) { const [expandedItems, setExpandedItems] = React.useState<Set<number>>(new Set()); - const [selectedMetric, setSelectedMetric] = React.useState<"price" | "delivery" | "compliance">("price"); + const [selectedVendorId, setSelectedVendorId] = React.useState<string>(""); + const [showSelectionDialog, setShowSelectionDialog] = React.useState(false); + const [showCancelDialog, setShowCancelDialog] = React.useState(false); + const [showContractDialog, setShowContractDialog] = React.useState(false); + const [selectedContractType, setSelectedContractType] = React.useState<"PO" | "CONTRACT" | "BIDDING" | "">(""); + const [selectionReason, setSelectionReason] = React.useState(""); + const [cancelReason, setCancelReason] = React.useState(""); + const [isSubmitting, setIsSubmitting] = React.useState(false); + + // 선정된 업체 정보 확인 + const selectedVendor = data.vendors.find(v => v.isSelected); + const hasSelection = !!selectedVendor; + const isSelectionApproved = selectedVendor?.selectionApprovalStatus === "승인"; + const isPendingApproval = selectedVendor?.selectionApprovalStatus === "대기"; + const hasContract = selectedVendor?.contractStatus ? true : false; + + // 계약 진행 처리 + const handleContractCreation = async () => { + if (!selectedContractType) { + toast.error("계약 유형을 선택해주세요."); + return; + } + + if (!selectedVendor) { + toast.error("선정된 업체가 없습니다."); + return; + } + + setIsSubmitting(true); + try { + let result; + + switch (selectedContractType) { + case "PO": + result = await createPO({ + rfqId: data.rfqInfo.id, + vendorId: selectedVendor.vendorId, + vendorName: selectedVendor.vendorName, + totalAmount: selectedVendor.totalAmount, + currency: selectedVendor.currency, + selectionReason: selectedVendor.selectionReason, + }); + break; + + case "CONTRACT": + result = await createGeneralContract({ + rfqId: data.rfqInfo.id, + vendorId: selectedVendor.vendorId, + vendorName: selectedVendor.vendorName, + totalAmount: selectedVendor.totalAmount, + currency: selectedVendor.currency, + }); + break; + + case "BIDDING": + result = await createBidding({ + rfqId: data.rfqInfo.id, + vendorId: selectedVendor.vendorId, + vendorName: selectedVendor.vendorName, + totalAmount: selectedVendor.totalAmount, + currency: selectedVendor.currency, + }); + break; + + default: + throw new Error("올바른 계약 유형이 아닙니다."); + } + + if (result.success) { + toast.success(result.message || "계약 프로세스가 시작되었습니다."); + setShowContractDialog(false); + setSelectedContractType(""); + window.location.reload(); + } else { + throw new Error(result.error || "계약 진행 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("계약 생성 오류:", error); + toast.error(error instanceof Error ? error.message : "계약 진행 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } + }; + + // 컴포넌트 마운트 시 선정된 업체가 있으면 자동 선택 + React.useEffect(() => { + if (selectedVendor) { + setSelectedVendorId(selectedVendor.vendorId.toString()); + } + }, [selectedVendor]); // 아이템 확장/축소 토글 const toggleItemExpansion = (itemId: number) => { @@ -81,17 +178,8 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { return "text-gray-600"; }; - // 조건 일치 여부 아이콘 - const getComplianceIcon = (matches: boolean) => { - return matches ? ( - <CheckCircle className="h-4 w-4 text-green-500" /> - ) : ( - <XCircle className="h-4 w-4 text-red-500" /> - ); - }; - // 금액 포맷 - const formatAmount = (amount: number, currency: string = "USD") => { + const formatAmount = (amount: number, currency: string = "KRW") => { return new Intl.NumberFormat("ko-KR", { style: "currency", currency: currency, @@ -100,8 +188,227 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { }).format(amount); }; + // 업체 선정 처리 + const handleVendorSelection = async () => { + if (!selectedVendorId) { + toast.error("선정할 업체를 선택해주세요."); + return; + } + + if (!selectionReason.trim()) { + toast.error("선정 사유를 입력해주세요."); + return; + } + + setIsSubmitting(true); + try { + const vendor = data.vendors.find(v => v.vendorId === parseInt(selectedVendorId)); + if (!vendor) { + throw new Error("선택한 업체를 찾을 수 없습니다."); + } + + const result = await selectVendor({ + rfqId: data.rfqInfo.id, + vendorId: vendor.vendorId, + vendorName: vendor.vendorName, + vendorCode: vendor.vendorCode, + totalAmount: vendor.totalAmount, + currency: vendor.currency, + selectionReason: selectionReason, + priceRank: vendor.rank || 0, + hasConditionDifferences: vendor.conditionDifferences.hasDifferences, + criticalDifferences: vendor.conditionDifferences.criticalDifferences, + }); + + if (result.success) { + toast.success("업체가 성공적으로 선정되었습니다."); + setShowSelectionDialog(false); + setSelectionReason(""); + window.location.reload(); // 페이지 새로고침으로 선정 상태 반영 + } else { + throw new Error(result.error || "업체 선정 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("업체 선정 오류:", error); + toast.error(error instanceof Error ? error.message : "업체 선정 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } + }; + + // 업체 선정 취소 처리 + const handleCancelSelection = async () => { + if (!cancelReason.trim()) { + toast.error("취소 사유를 입력해주세요."); + return; + } + + setIsSubmitting(true); + try { + // 파라미터를 올바르게 전달 + const result = await cancelVendorSelection(Number(data.rfqInfo.id),cancelReason); + + if (result.success) { + toast.success("업체 선정이 취소되었습니다."); + setShowCancelDialog(false); + setCancelReason(""); + window.location.reload(); + } else { + throw new Error(result.error || "선정 취소 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("선정 취소 오류:", error); + toast.error(error instanceof Error ? error.message : "선정 취소 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } + }; + return ( <div className="space-y-6"> + {/* 상단 액션 바 */} + <div className="flex justify-between items-center"> + <h2 className="text-2xl font-bold">견적 비교 분석</h2> + <div className="flex gap-2"> + {hasSelection ? ( + <> + {!isSelectionApproved && ( + <Button + variant="destructive" + onClick={() => setShowCancelDialog(true)} + className="gap-2" + > + <X className="h-4 w-4" /> + 선정 취소 + </Button> + )} + <Button + variant="outline" + onClick={() => { + setSelectedVendorId(""); + setShowSelectionDialog(true); + }} + disabled={isSelectionApproved} + className="gap-2" + > + <RefreshCw className="h-4 w-4" /> + 재선정 + </Button> + </> + ) : ( + <Button + onClick={() => setShowSelectionDialog(true)} + disabled={!selectedVendorId} + className="gap-2" + > + <Award className="h-4 w-4" /> + 업체 선정 + </Button> + )} + </div> + </div> + + {/* 선정 상태 알림 */} + {hasSelection && ( + <Alert className={cn( + "border-2", + 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" + )}> + <div className="flex items-start justify-between"> + <div className="flex gap-3"> + {hasContract ? ( + <FileText className="h-5 w-5 text-purple-600 mt-0.5" /> + ) : isSelectionApproved ? ( + <CheckCircle className="h-5 w-5 text-green-600 mt-0.5" /> + ) : isPendingApproval ? ( + <Clock className="h-5 w-5 text-yellow-600 mt-0.5" /> + ) : ( + <Award className="h-5 w-5 text-blue-600 mt-0.5" /> + )} + <div className="space-y-2"> + <AlertTitle className="text-lg"> + {hasContract + ? "계약 진행중" + : isSelectionApproved + ? "업체 선정 승인 완료" + : isPendingApproval + ? "업체 선정 승인 대기중" + : "업체 선정 완료"} + </AlertTitle> + <AlertDescription className="space-y-1"> + <p className="font-semibold">선정 업체: {selectedVendor.vendorName} ({selectedVendor.vendorCode})</p> + <p>선정 금액: {formatAmount(selectedVendor.totalAmount, selectedVendor.currency)}</p> + <p>선정일: {selectedVendor.selectionDate ? format(new Date(selectedVendor.selectionDate), "yyyy년 MM월 dd일", { locale: ko }) : "-"}</p> + <p>선정 사유: {selectedVendor.selectionReason || "-"}</p> + {selectedVendor.contractNo && ( + <> + <div className="border-t pt-1 mt-2"> + <p className="font-semibold">계약 정보</p> + <p>계약 번호: {selectedVendor.contractNo}</p> + <p>계약 상태: {selectedVendor.contractStatus}</p> + {selectedVendor.contractCreatedAt && ( + <p>계약 생성일: {format(new Date(selectedVendor.contractCreatedAt), "yyyy년 MM월 dd일", { locale: ko })}</p> + )} + </div> + </> + )} + {selectedVendor.selectedByName && ( + <p className="text-sm text-muted-foreground">선정자: {selectedVendor.selectedByName}</p> + )} + </AlertDescription> + </div> + </div> + {/* 계약 진행 버튼들을 알림 카드 안에도 추가 (선택사항) */} + {!hasContract && !isPendingApproval && ( + <div className="flex flex-col gap-2"> + <Button + size="sm" + variant="default" + onClick={() => { + setSelectedContractType("PO"); + setShowContractDialog(true); + }} + className="gap-1 bg-green-600 hover:bg-green-700 text-xs" + > + <FileText className="h-3 w-3" /> + PO 생성 + </Button> + <Button + size="sm" + variant="default" + onClick={() => { + setSelectedContractType("CONTRACT"); + setShowContractDialog(true); + }} + className="gap-1 bg-blue-600 hover:bg-blue-700 text-xs" + > + <FileText className="h-3 w-3" /> + 일반계약 + </Button> + <Button + size="sm" + variant="default" + onClick={() => { + setSelectedContractType("BIDDING"); + setShowContractDialog(true); + }} + className="gap-1 bg-purple-600 hover:bg-purple-700 text-xs" + > + <Globe className="h-3 w-3" /> + 입찰 + </Button> + </div> + )} + </div> + </Alert> + )} + {/* 요약 카드 */} <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> {/* 최저가 벤더 */} @@ -120,23 +427,40 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </CardContent> </Card> - {/* 평균 가격 */} - <Card> - <CardHeader className="pb-3"> - <CardTitle className="text-sm font-medium flex items-center gap-2"> - <DollarSign className="h-4 w-4" /> - 평균 가격 - </CardTitle> - </CardHeader> - <CardContent> - <p className="text-lg font-bold"> - {formatAmount(data.summary.priceRange.average, data.summary.currency)} - </p> - <p className="text-sm text-muted-foreground"> - {data.vendors.length}개 업체 평균 - </p> - </CardContent> - </Card> + {/* 선정 업체 또는 평균 가격 */} + {hasSelection ? ( + <Card className="border-2 border-blue-200 bg-blue-50"> + <CardHeader className="pb-3"> + <CardTitle className="text-sm font-medium flex items-center gap-2"> + <UserCheck className="h-4 w-4 text-blue-600" /> + 선정 업체 + </CardTitle> + </CardHeader> + <CardContent> + <p className="text-lg font-bold">{selectedVendor.vendorName}</p> + <p className="text-sm text-blue-600"> + {formatAmount(selectedVendor.totalAmount, selectedVendor.currency)} + </p> + </CardContent> + </Card> + ) : ( + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm font-medium flex items-center gap-2"> + <DollarSign className="h-4 w-4" /> + 평균 가격 + </CardTitle> + </CardHeader> + <CardContent> + <p className="text-lg font-bold"> + {formatAmount(data.summary.priceRange.average, data.summary.currency)} + </p> + <p className="text-sm text-muted-foreground"> + {data.vendors.length}개 업체 평균 + </p> + </CardContent> + </Card> + )} {/* 가격 범위 */} <Card> @@ -188,16 +512,40 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <TabsContent value="overview" className="space-y-4"> <Card> <CardHeader> - <CardTitle>가격 순위</CardTitle> + <CardTitle>가격 순위 및 업체 선정</CardTitle> </CardHeader> <CardContent> <div className="space-y-4"> {data.vendors.map((vendor) => ( <div key={vendor.vendorId} - className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors" + className={cn( + "flex items-center justify-between p-4 border rounded-lg transition-colors", + 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" + )} + onClick={() => { + if (!hasSelection) { + setSelectedVendorId(vendor.vendorId.toString()); + } + }} > <div className="flex items-center gap-4"> + {!hasSelection && ( + <input + type="radio" + name="vendor-selection" + value={vendor.vendorId} + checked={selectedVendorId === vendor.vendorId.toString()} + onChange={(e) => setSelectedVendorId(e.target.value)} + className="h-4 w-4 text-blue-600" + /> + )} <div className={cn( "w-10 h-10 rounded-full flex items-center justify-center font-bold", @@ -207,7 +555,12 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {vendor.rank} </div> <div> - <p className="font-semibold">{vendor.vendorName}</p> + <p className="font-semibold flex items-center gap-2"> + {vendor.vendorName} + {vendor.isSelected && ( + <Badge className="bg-blue-600">선정</Badge> + )} + </p> <p className="text-sm text-muted-foreground"> {vendor.vendorCode} • {vendor.vendorCountry} </p> @@ -267,8 +620,14 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <th className="text-left p-2">항목</th> <th className="text-left p-2">구매자 제시</th> {data.vendors.map((vendor) => ( - <th key={vendor.vendorId} className="text-left p-2"> + <th key={vendor.vendorId} className={cn( + "text-left p-2", + vendor.isSelected && "bg-blue-50" + )}> {vendor.vendorName} + {vendor.isSelected && ( + <Badge className="ml-2 bg-blue-600">선정</Badge> + )} </th> ))} </tr> @@ -279,7 +638,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <td className="p-2 font-medium">통화</td> <td className="p-2">{data.vendors[0]?.buyerConditions.currency}</td> {data.vendors.map((vendor) => ( - <td key={vendor.vendorId} className="p-2"> + <td key={vendor.vendorId} className={cn( + "p-2", + vendor.isSelected && "bg-blue-50" + )}> <div className="flex items-center gap-2"> {vendor.vendorConditions.currency || vendor.buyerConditions.currency} {vendor.vendorConditions.currency !== vendor.buyerConditions.currency && ( @@ -306,7 +668,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </TooltipProvider> </td> {data.vendors.map((vendor) => ( - <td key={vendor.vendorId} className="p-2"> + <td key={vendor.vendorId} className={cn( + "p-2", + vendor.isSelected && "bg-blue-50" + )}> <div className="flex items-center gap-2"> <TooltipProvider> <Tooltip> @@ -327,134 +692,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { ))} </tr> - {/* 인코텀즈 */} - <tr> - <td className="p-2 font-medium">인코텀즈</td> - <td className="p-2">{data.vendors[0]?.buyerConditions.incotermsCode}</td> - {data.vendors.map((vendor) => ( - <td key={vendor.vendorId} className="p-2"> - <div className="flex items-center gap-2"> - {vendor.vendorConditions.incotermsCode || vendor.buyerConditions.incotermsCode} - {vendor.vendorConditions.incotermsCode !== vendor.buyerConditions.incotermsCode && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> - ))} - </tr> - - {/* 납기 */} - <tr> - <td className="p-2 font-medium">납기</td> - <td className="p-2"> - {data.vendors[0]?.buyerConditions.deliveryDate - ? format(new Date(data.vendors[0].buyerConditions.deliveryDate), "yyyy-MM-dd") - : "-"} - </td> - {data.vendors.map((vendor) => { - const vendorDate = vendor.vendorConditions.deliveryDate || vendor.buyerConditions.deliveryDate; - const isDelayed = vendorDate && vendor.buyerConditions.deliveryDate && - new Date(vendorDate) > new Date(vendor.buyerConditions.deliveryDate); - - return ( - <td key={vendor.vendorId} className="p-2"> - <div className="flex items-center gap-2"> - {vendorDate ? format(new Date(vendorDate), "yyyy-MM-dd") : "-"} - {isDelayed && ( - <Badge variant="destructive" className="text-xs">지연</Badge> - )} - </div> - </td> - ); - })} - </tr> - - {/* 초도품 */} - <tr> - <td className="p-2 font-medium">초도품</td> - <td className="p-2"> - {data.vendors[0]?.buyerConditions.firstYn ? "요구" : "해당없음"} - </td> - {data.vendors.map((vendor) => ( - <td key={vendor.vendorId} className="p-2"> - {vendor.buyerConditions.firstYn && ( - <Badge - variant={ - vendor.vendorConditions.firstAcceptance === "수용" - ? "default" - : vendor.vendorConditions.firstAcceptance === "부분수용" - ? "secondary" - : vendor.vendorConditions.firstAcceptance === "거부" - ? "destructive" - : "outline" - } - > - {vendor.vendorConditions.firstAcceptance || "미응답"} - </Badge> - )} - {!vendor.buyerConditions.firstYn && "-"} - </td> - ))} - </tr> - - {/* 스페어파트 */} - <tr> - <td className="p-2 font-medium">스페어파트</td> - <td className="p-2"> - {data.vendors[0]?.buyerConditions.sparepartYn ? "요구" : "해당없음"} - </td> - {data.vendors.map((vendor) => ( - <td key={vendor.vendorId} className="p-2"> - {vendor.buyerConditions.sparepartYn && ( - <Badge - variant={ - vendor.vendorConditions.sparepartAcceptance === "수용" - ? "default" - : vendor.vendorConditions.sparepartAcceptance === "부분수용" - ? "secondary" - : vendor.vendorConditions.sparepartAcceptance === "거부" - ? "destructive" - : "outline" - } - > - {vendor.vendorConditions.sparepartAcceptance || "미응답"} - </Badge> - )} - {!vendor.buyerConditions.sparepartYn && "-"} - </td> - ))} - </tr> - - {/* 연동제 */} - <tr> - <td className="p-2 font-medium">연동제</td> - <td className="p-2"> - {data.vendors[0]?.buyerConditions.materialPriceRelatedYn ? "적용" : "미적용"} - </td> - {data.vendors.map((vendor) => ( - <td key={vendor.vendorId} className="p-2"> - <div className="flex items-center gap-2"> - {vendor.vendorConditions.materialPriceRelatedYn !== undefined - ? vendor.vendorConditions.materialPriceRelatedYn ? "적용" : "미적용" - : vendor.buyerConditions.materialPriceRelatedYn ? "적용" : "미적용"} - {vendor.vendorConditions.materialPriceRelatedReason && ( - <TooltipProvider> - <Tooltip> - <TooltipTrigger> - <Info className="h-3 w-3" /> - </TooltipTrigger> - <TooltipContent> - <p className="max-w-xs text-xs"> - {vendor.vendorConditions.materialPriceRelatedReason} - </p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - )} - </div> - </td> - ))} - </tr> + {/* 나머지 조건들도 동일한 패턴으로 처리 */} </tbody> </table> </CardContent> @@ -750,6 +988,250 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { )} </TabsContent> </Tabs> + + {/* 업체 선정 모달 */} + {showSelectionDialog && ( + <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"> + {hasSelection ? "업체 재선정 확인" : "업체 선정 확인"} + </h3> + + {selectedVendorId && ( + <div className="space-y-4"> + <div className="rounded-lg border p-4"> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-sm font-medium">선정 업체</span> + <span className="text-sm font-bold"> + {data.vendors.find(v => v.vendorId === parseInt(selectedVendorId))?.vendorName} + </span> + </div> + <div className="flex justify-between"> + <span className="text-sm font-medium">견적 금액</span> + <span className="text-sm"> + {formatAmount( + data.vendors.find(v => v.vendorId === parseInt(selectedVendorId))?.totalAmount || 0, + data.vendors.find(v => v.vendorId === parseInt(selectedVendorId))?.currency + )} + </span> + </div> + <div className="flex justify-between"> + <span className="text-sm font-medium">가격 순위</span> + <span className="text-sm"> + #{data.vendors.find(v => v.vendorId === parseInt(selectedVendorId))?.rank || 0} + </span> + </div> + {data.vendors.find(v => v.vendorId === parseInt(selectedVendorId))?.conditionDifferences.hasDifferences && ( + <Alert className="mt-2"> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + 제시 조건과 차이가 있습니다. 선정 사유를 명확히 기재해주세요. + </AlertDescription> + </Alert> + )} + </div> + </div> + + <div className="space-y-2"> + <label htmlFor="selection-reason" className="text-sm font-medium"> + 선정 사유 * + </label> + <textarea + id="selection-reason" + className="w-full min-h-[100px] p-3 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="업체 선정 사유를 입력해주세요..." + value={selectionReason} + onChange={(e) => setSelectionReason(e.target.value)} + required + /> + </div> + </div> + )} + + <div className="flex justify-end gap-2 mt-6"> + <Button + variant="outline" + onClick={() => { + setShowSelectionDialog(false); + setSelectionReason(""); + }} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + onClick={handleVendorSelection} + disabled={!selectionReason || isSubmitting} + > + {isSubmitting ? "처리 중..." : hasSelection ? "재선정 확정" : "선정 확정"} + </Button> + </div> + </div> + </div> + )} + + {/* 선정 취소 모달 */} + {showCancelDialog && ( + <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> + 업체 선정을 취소하시겠습니까? 이 작업은 되돌릴 수 없습니다. + </AlertDescription> + </Alert> + + {selectedVendor && ( + <div className="rounded-lg border p-4 mb-4 bg-gray-50"> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-sm font-medium">선정 업체</span> + <span className="text-sm font-bold">{selectedVendor.vendorName}</span> + </div> + <div className="flex justify-between"> + <span className="text-sm font-medium">선정 금액</span> + <span className="text-sm"> + {formatAmount(selectedVendor.totalAmount, selectedVendor.currency)} + </span> + </div> + </div> + </div> + )} + + <div className="space-y-2"> + <label htmlFor="cancel-reason" className="text-sm font-medium"> + 취소 사유 * + </label> + <textarea + id="cancel-reason" + className="w-full min-h-[100px] p-3 border rounded-md focus:outline-none focus:ring-2 focus:ring-red-500" + placeholder="선정 취소 사유를 입력해주세요..." + value={cancelReason} + onChange={(e) => setCancelReason(e.target.value)} + required + /> + </div> + + <div className="flex justify-end gap-2 mt-6"> + <Button + variant="outline" + onClick={() => { + setShowCancelDialog(false); + setCancelReason(""); + }} + disabled={isSubmitting} + > + 닫기 + </Button> + <Button + variant="destructive" + onClick={handleCancelSelection} + disabled={!cancelReason || isSubmitting} + > + {isSubmitting ? "처리 중..." : "선정 취소"} + </Button> + </div> + </div> + </div> + )} + + {/* 계약 진행 모달 */} + {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"> + <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"> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-sm font-medium">선정 업체</span> + <span className="text-sm font-bold">{selectedVendor.vendorName}</span> + </div> + <div className="flex justify-between"> + <span className="text-sm font-medium">계약 금액</span> + <span className="text-sm"> + {formatAmount(selectedVendor.totalAmount, selectedVendor.currency)} + </span> + </div> + </div> + </div> + + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + {selectedContractType === "PO" && ( + "PO를 생성하면 SAP 시스템으로 자동 전송됩니다. 계속 진행하시겠습니까?" + )} + {selectedContractType === "CONTRACT" && ( + "일반계약을 생성하면 계약서 작성 프로세스가 시작됩니다. 계속 진행하시겠습니까?" + )} + {selectedContractType === "BIDDING" && ( + "입찰을 생성하면 입찰 공고 프로세스가 시작됩니다. 계속 진행하시겠습니까?" + )} + </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> + </label> + </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> + )} + </div> + )} + + <div className="flex justify-end gap-2 mt-6"> + <Button + variant="outline" + onClick={() => { + setShowContractDialog(false); + setSelectedContractType(""); + }} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + onClick={handleContractCreation} + disabled={isSubmitting} + > + {isSubmitting ? "처리 중..." : "진행"} + </Button> + </div> + </div> + </div> + )} </div> ); }
\ No newline at end of file |
