diff options
Diffstat (limited to 'lib/rfq-last/vendor/add-vendor-dialog.tsx')
| -rw-r--r-- | lib/rfq-last/vendor/add-vendor-dialog.tsx | 438 |
1 files changed, 355 insertions, 83 deletions
diff --git a/lib/rfq-last/vendor/add-vendor-dialog.tsx b/lib/rfq-last/vendor/add-vendor-dialog.tsx index d8745298..8566763f 100644 --- a/lib/rfq-last/vendor/add-vendor-dialog.tsx +++ b/lib/rfq-last/vendor/add-vendor-dialog.tsx @@ -24,7 +24,7 @@ import { PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Check, ChevronsUpDown, Loader2, X, Plus } from "lucide-react"; +import { Check, ChevronsUpDown, Loader2, X, Plus, FileText, Shield, Globe, Settings } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { addVendorsToRfq } from "../service"; @@ -34,6 +34,17 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { ScrollArea } from "@/components/ui/scroll-area"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Info } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +interface VendorContract { + vendorId: number; + agreementYn: boolean; + ndaYn: boolean; + gtcType: "general" | "project" | "none"; +} interface AddVendorDialogProps { open: boolean; @@ -52,6 +63,17 @@ export function AddVendorDialog({ const [vendorOpen, setVendorOpen] = React.useState(false); const [vendorList, setVendorList] = React.useState<any[]>([]); const [selectedVendors, setSelectedVendors] = React.useState<any[]>([]); + const [activeTab, setActiveTab] = React.useState<"vendors" | "contracts">("vendors"); + + // 각 벤더별 기본계약 요구사항 상태 + const [vendorContracts, setVendorContracts] = React.useState<VendorContract[]>([]); + + // 일괄 적용용 기본값 + const [defaultContract, setDefaultContract] = React.useState({ + agreementYn: true, + ndaYn: true, + gtcType: "none" as "general" | "project" | "none" + }); // 벤더 로드 const loadVendors = React.useCallback(async () => { @@ -76,13 +98,33 @@ export function AddVendorDialog({ React.useEffect(() => { if (!open) { setSelectedVendors([]); + setVendorContracts([]); + setActiveTab("vendors"); + setDefaultContract({ + agreementYn: true, + ndaYn: true, + gtcType: "none" + }); } }, [open]); // 벤더 추가 const handleAddVendor = (vendor: any) => { if (!selectedVendors.find(v => v.id === vendor.id)) { - setSelectedVendors([...selectedVendors, vendor]); + const updatedVendors = [...selectedVendors, vendor]; + setSelectedVendors(updatedVendors); + + // 해당 벤더의 기본계약 설정 추가 + const isInternational = vendor.country && vendor.country !== "KR" && vendor.country !== "한국"; + setVendorContracts([ + ...vendorContracts, + { + vendorId: vendor.id, + agreementYn: defaultContract.agreementYn, + ndaYn: defaultContract.ndaYn, + gtcType: isInternational ? defaultContract.gtcType : "none" + } + ]); } setVendorOpen(false); }; @@ -90,9 +132,36 @@ export function AddVendorDialog({ // 벤더 제거 const handleRemoveVendor = (vendorId: number) => { setSelectedVendors(selectedVendors.filter(v => v.id !== vendorId)); + setVendorContracts(vendorContracts.filter(c => c.vendorId !== vendorId)); }; - // 제출 처리 - 벤더만 추가 + // 개별 벤더의 계약 설정 업데이트 + const updateVendorContract = (vendorId: number, field: string, value: any) => { + setVendorContracts(contracts => + contracts.map(c => + c.vendorId === vendorId ? { ...c, [field]: value } : c + ) + ); + }; + + // 모든 벤더에 일괄 적용 + const applyToAll = () => { + setVendorContracts(contracts => + contracts.map(c => { + const vendor = selectedVendors.find(v => v.id === c.vendorId); + const isInternational = vendor?.country && vendor.country !== "KR" && vendor.country !== "한국"; + return { + ...c, + agreementYn: defaultContract.agreementYn, + ndaYn: defaultContract.ndaYn, + gtcType: isInternational ? defaultContract.gtcType : "none" + }; + }) + ); + toast.success("모든 벤더에 기본계약 설정이 적용되었습니다."); + }; + + // 제출 처리 const handleSubmit = async () => { if (selectedVendors.length === 0) { toast.error("최소 1개 이상의 벤더를 선택해주세요."); @@ -102,18 +171,32 @@ export function AddVendorDialog({ setIsLoading(true); try { - const vendorIds = selectedVendors.map(v => v.id); - const result = await addVendorsToRfq({ - rfqId, - vendorIds, - // 기본값으로 벤더만 추가 (상세 조건은 나중에 일괄 입력) - conditions: null, - }); + // 각 벤더별로 개별 추가 + const results = await Promise.all( + selectedVendors.map(async (vendor) => { + const contract = vendorContracts.find(c => c.vendorId === vendor.id); + return addVendorsToRfq({ + rfqId, + vendorIds: [vendor.id], + conditions: null, + contractRequirements: contract || defaultContract + }); + }) + ); + + // 결과 확인 + const successCount = results.filter(r => r.success).length; + const failedCount = results.length - successCount; - if (result.success) { + if (successCount > 0) { toast.success( <div> - <p>{selectedVendors.length}개 벤더가 추가되었습니다.</p> + <p>{successCount}개 벤더가 추가되었습니다.</p> + {failedCount > 0 && ( + <p className="text-sm text-destructive mt-1"> + {failedCount}개 벤더 추가 실패 + </p> + )} <p className="text-sm text-muted-foreground mt-1"> 벤더 목록에서 '정보 일괄 입력' 버튼으로 조건을 설정하세요. </p> @@ -122,7 +205,7 @@ export function AddVendorDialog({ onSuccess(); onOpenChange(false); } else { - toast.error(result.error || "벤더 추가에 실패했습니다."); + toast.error("벤더 추가에 실패했습니다."); } } catch (error) { console.error("Submit error:", error); @@ -137,38 +220,41 @@ export function AddVendorDialog({ return selectedVendors.some(v => v.id === vendorId); }; + // 선택된 벤더가 있고 계약 탭으로 이동 가능한지 + const canProceedToContracts = selectedVendors.length > 0; + return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-2xl max-h-[80vh] p-0 flex flex-col"> + <DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col"> {/* 헤더 */} <DialogHeader className="p-6 pb-0"> <DialogTitle>벤더 추가</DialogTitle> <DialogDescription> - 견적 요청을 보낼 벤더를 선택하세요. 조건 설정은 추가 후 일괄로 진행할 수 있습니다. + 견적 요청을 보낼 벤더를 선택하고 각 벤더별 기본계약 요구사항을 설정하세요. </DialogDescription> </DialogHeader> - {/* 컨텐츠 영역 */} - <div className="flex-1 px-6 py-4 overflow-y-auto"> - <div className="space-y-4"> - {/* 안내 메시지 */} - <Alert> - <Info className="h-4 w-4" /> - <AlertDescription> - 여기서는 벤더만 선택합니다. 납기일, 결제조건 등의 상세 정보는 벤더 추가 후 - '정보 일괄 입력' 기능으로 한 번에 설정할 수 있습니다. - </AlertDescription> - </Alert> - - {/* 벤더 선택 카드 */} + {/* 탭 */} + <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as any)} className="flex-1 flex flex-col min-h-0"> + <TabsList className="mx-6 grid w-fit grid-cols-2"> + <TabsTrigger value="vendors"> + 1. 벤더 선택 + {selectedVendors.length > 0 && ( + <Badge variant="secondary" className="ml-2"> + {selectedVendors.length} + </Badge> + )} + </TabsTrigger> + <TabsTrigger value="contracts" disabled={!canProceedToContracts}> + 2. 기본계약 설정 + </TabsTrigger> + </TabsList> + + {/* 벤더 선택 탭 */} + <TabsContent value="vendors" className="flex-1 flex flex-col px-6 py-4 overflow-y-auto min-h-0"> <Card> <CardHeader> - <div className="flex items-center justify-between"> - <CardTitle className="text-lg">벤더 선택</CardTitle> - <Badge variant="outline" className="ml-2"> - {selectedVendors.length}개 선택됨 - </Badge> - </div> + <CardTitle className="text-lg">벤더 선택</CardTitle> <CardDescription> RFQ를 발송할 벤더를 선택하세요. 여러 개 선택 가능합니다. </CardDescription> @@ -196,11 +282,11 @@ export function AddVendorDialog({ <Command> <CommandInput placeholder="벤더명 또는 코드로 검색..." /> <CommandList - onWheel={(e) => { - e.stopPropagation(); // 이벤트 전파 차단 - const target = e.currentTarget; - target.scrollTop += e.deltaY; // 직접 스크롤 처리 - }} + onWheel={(e) => { + e.stopPropagation(); + const target = e.currentTarget; + target.scrollTop += e.deltaY; + }} > <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> <CommandGroup> @@ -218,9 +304,12 @@ export function AddVendorDialog({ </Badge> <span className="truncate">{vendor.vendorName}</span> {vendor.country && ( - <span className="text-xs text-muted-foreground ml-auto"> + <Badge + variant={vendor.country === "KR" || vendor.country === "한국" ? "default" : "secondary"} + className="ml-auto" + > {vendor.country} - </span> + </Badge> )} </div> </CommandItem> @@ -234,41 +323,46 @@ export function AddVendorDialog({ {/* 선택된 벤더 목록 */} {selectedVendors.length > 0 && ( <div className="space-y-2"> - <Label className="text-sm text-muted-foreground">선택된 벤더 목록</Label> - <ScrollArea className="h-[200px] w-full rounded-md border p-4"> - <div className="space-y-2"> - {selectedVendors.map((vendor, index) => ( - <div - key={vendor.id} - className="flex items-center justify-between p-2 rounded-lg bg-secondary/50" - > - <div className="flex items-center gap-2"> - <span className="text-sm text-muted-foreground"> - {index + 1}. - </span> - <Badge variant="outline"> - {vendor.vendorCode} + + <div className="space-y-2"> + {selectedVendors.map((vendor, index) => ( + <div + key={vendor.id} + className="flex items-center justify-between p-2 rounded-lg bg-secondary/50" + > + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground"> + {index + 1}. + </span> + <Badge variant="outline"> + {vendor.vendorCode} + </Badge> + <span className="text-sm font-medium"> + {vendor.vendorName} + </span> + {vendor.country && ( + <Badge + variant={vendor.country === "KR" || vendor.country === "한국" ? "default" : "secondary"} + className="text-xs" + > + {vendor.country} </Badge> - <span className="text-sm font-medium"> - {vendor.vendorName} - </span> - </div> - <Button - variant="ghost" - size="sm" - onClick={() => handleRemoveVendor(vendor.id)} - className="h-8 w-8 p-0" - > - <X className="h-4 w-4" /> - </Button> + )} </div> - ))} - </div> - </ScrollArea> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveVendor(vendor.id)} + className="h-8 w-8 p-0" + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> </div> )} - {/* 벤더가 없는 경우 메시지 */} {selectedVendors.length === 0 && ( <div className="text-center py-8 text-muted-foreground"> <p className="text-sm">아직 선택된 벤더가 없습니다.</p> @@ -278,8 +372,177 @@ export function AddVendorDialog({ </div> </CardContent> </Card> - </div> - </div> + </TabsContent> + + {/* 기본계약 설정 탭 */} + <TabsContent value="contracts" className="flex-1 flex flex-col px-6 py-4 overflow-hidden min-h-0"> + <div className="flex-1 overflow-y-auto space-y-4 min-h-0"> + {/* 일괄 적용 카드 */} + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-base flex items-center gap-2"> + <Settings className="h-4 w-4" /> + 일괄 적용 설정 + </CardTitle> + <CardDescription> + 모든 벤더에 동일한 설정을 적용할 수 있습니다. + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="flex items-center space-x-2"> + <Checkbox + id="default-agreement" + checked={defaultContract.agreementYn} + onCheckedChange={(checked) => + setDefaultContract({ ...defaultContract, agreementYn: !!checked }) + } + /> + <label htmlFor="default-agreement" className="text-sm font-medium"> + 기술자료 제공 동의 + </label> + </div> + <div className="flex items-center space-x-2"> + <Checkbox + id="default-nda" + checked={defaultContract.ndaYn} + onCheckedChange={(checked) => + setDefaultContract({ ...defaultContract, ndaYn: !!checked }) + } + /> + <label htmlFor="default-nda" className="text-sm font-medium"> + 비밀유지 계약 (NDA) + </label> + </div> + </div> + <div className="space-y-2"> + <Label className="text-sm">GTC (국외 업체용)</Label> + <RadioGroup + value={defaultContract.gtcType} + onValueChange={(value: any) => + setDefaultContract({ ...defaultContract, gtcType: value }) + } + > + <div className="flex items-center space-x-2"> + <RadioGroupItem value="none" id="default-gtc-none" /> + <label htmlFor="default-gtc-none" className="text-sm">없음</label> + </div> + <div className="flex items-center space-x-2"> + <RadioGroupItem value="general" id="default-gtc-general" /> + <label htmlFor="default-gtc-general" className="text-sm">General GTC</label> + </div> + <div className="flex items-center space-x-2"> + <RadioGroupItem value="project" id="default-gtc-project" /> + <label htmlFor="default-gtc-project" className="text-sm">Project GTC</label> + </div> + </RadioGroup> + </div> + </div> + <Button + variant="secondary" + size="sm" + onClick={applyToAll} + className="w-full" + > + 모든 벤더에 적용 + </Button> + </CardContent> + </Card> + + {/* 개별 벤더 설정 */} + <Card className="flex flex-col min-h-0"> + <CardHeader className="pb-3"> + <CardTitle className="text-base">개별 벤더 기본계약 설정</CardTitle> + <CardDescription> + 각 벤더별로 다른 기본계약을 요구할 수 있습니다. + </CardDescription> + </CardHeader> + <CardContent className="flex-1 min-h-0"> + <ScrollArea className="h-[250px] pr-4"> + <div className="space-y-4"> + {selectedVendors.map((vendor) => { + const contract = vendorContracts.find(c => c.vendorId === vendor.id); + const isInternational = vendor.country && vendor.country !== "KR" && vendor.country !== "한국"; + + return ( + <div key={vendor.id} className="border rounded-lg p-4 space-y-3"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Badge variant="outline">{vendor.vendorCode}</Badge> + <span className="font-medium">{vendor.vendorName}</span> + <Badge + variant={isInternational ? "secondary" : "default"} + className="text-xs" + > + {vendor.country || "미지정"} + </Badge> + </div> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="flex items-center space-x-2"> + <Checkbox + checked={contract?.agreementYn || false} + onCheckedChange={(checked) => + updateVendorContract(vendor.id, "agreementYn", !!checked) + } + /> + <label className="text-sm">기술자료 제공</label> + </div> + <div className="flex items-center space-x-2"> + <Checkbox + checked={contract?.ndaYn || false} + onCheckedChange={(checked) => + updateVendorContract(vendor.id, "ndaYn", !!checked) + } + /> + <label className="text-sm">NDA</label> + </div> + </div> + + {isInternational && ( + <div className="space-y-1"> + <Label className="text-xs">GTC</Label> + <RadioGroup + value={contract?.gtcType || "none"} + onValueChange={(value) => + updateVendorContract(vendor.id, "gtcType", value) + } + > + <div className="flex items-center space-x-2"> + <RadioGroupItem value="none" id={`${vendor.id}-none`} /> + <label htmlFor={`${vendor.id}-none`} className="text-xs">없음</label> + </div> + <div className="flex items-center space-x-2"> + <RadioGroupItem value="general" id={`${vendor.id}-general`} /> + <label htmlFor={`${vendor.id}-general`} className="text-xs">General</label> + </div> + <div className="flex items-center space-x-2"> + <RadioGroupItem value="project" id={`${vendor.id}-project`} /> + <label htmlFor={`${vendor.id}-project`} className="text-xs">Project</label> + </div> + </RadioGroup> + </div> + )} + + {!isInternational && ( + <div className="text-xs text-muted-foreground"> + 국내 업체 - GTC 불필요 + </div> + )} + </div> + </div> + ); + })} + </div> + </ScrollArea> + </CardContent> + </Card> + </div> + </TabsContent> + </Tabs> {/* 푸터 */} <DialogFooter className="p-6 pt-0 border-t"> @@ -290,16 +553,25 @@ export function AddVendorDialog({ > 취소 </Button> - <Button - onClick={handleSubmit} - disabled={isLoading || selectedVendors.length === 0} - > - {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {selectedVendors.length > 0 - ? `${selectedVendors.length}개 벤더 추가` - : '벤더 추가' - } - </Button> + {activeTab === "vendors" && canProceedToContracts && ( + <Button + onClick={() => setActiveTab("contracts")} + > + 다음: 기본계약 설정 + </Button> + )} + {activeTab === "contracts" && ( + <Button + onClick={handleSubmit} + disabled={isLoading || selectedVendors.length === 0} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {selectedVendors.length > 0 + ? `${selectedVendors.length}개 벤더 추가` + : '벤더 추가' + } + </Button> + )} </DialogFooter> </DialogContent> </Dialog> |
