diff options
Diffstat (limited to 'lib/rfq-last/vendor/avl-vendor-dialog.tsx')
| -rw-r--r-- | lib/rfq-last/vendor/avl-vendor-dialog.tsx | 628 |
1 files changed, 125 insertions, 503 deletions
diff --git a/lib/rfq-last/vendor/avl-vendor-dialog.tsx b/lib/rfq-last/vendor/avl-vendor-dialog.tsx index 2efd96b9..67a71cc5 100644 --- a/lib/rfq-last/vendor/avl-vendor-dialog.tsx +++ b/lib/rfq-last/vendor/avl-vendor-dialog.tsx @@ -10,37 +10,28 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Checkbox } from "@/components/ui/checkbox"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Loader2, - X, - FileText, - Shield, - Globe, - Settings, - Link, +import { + Loader2, + Eye, + Globe, CheckCircle, - Info, - AlertCircle, - Building2 + Building2, + Info } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; -import { getAvlVendorsForRfq, addAvlVendorsToRfq } from "../service"; +import { getAvlVendorsForRfq } from "../service"; interface AvlVendor { id: number; vendorId: number | null; - vendorName: string; + vendorName: string | null; vendorCode: string | null; - avlVendorName: string; + avlVendorName: string | null; tier: string | null; headquarterLocation: string | null; manufacturingLocation: string | null; @@ -54,79 +45,35 @@ interface AvlVendor { remark: string | null; } -interface VendorContract { - vendorId: number; - agreementYn: boolean; - ndaYn: boolean; - gtcType: "general" | "project" | "none"; -} +// 더 이상 계약 인터페이스 필요 없음 - 참조용으로만 사용 interface AvlVendorDialogProps { open: boolean; onOpenChange: (open: boolean) => void; rfqId: number; - rfqCode?: string; - onSuccess: () => void; } export function AvlVendorDialog({ open, onOpenChange, rfqId, - rfqCode, - onSuccess, }: AvlVendorDialogProps) { - const [isLoading, setIsLoading] = React.useState(false); const [isLoadingAvl, setIsLoadingAvl] = React.useState(false); const [avlVendors, setAvlVendors] = React.useState<AvlVendor[]>([]); - const [selectedVendorIds, setSelectedVendorIds] = React.useState<Set<number>>(new Set()); - const [activeTab, setActiveTab] = React.useState<"vendors" | "contracts">("vendors"); - const [vendorContracts, setVendorContracts] = React.useState<VendorContract[]>([]); const [existingVendorIds, setExistingVendorIds] = React.useState<Set<number>>(new Set()); - - // 일괄 적용용 기본값 - const [defaultContract, setDefaultContract] = React.useState({ - agreementYn: true, - ndaYn: true, - gtcType: "none" as "general" | "project" | "none" - }); - // AVL 벤더 로드 + // AVL 벤더 로드 (참조용) const loadAvlVendors = React.useCallback(async () => { setIsLoadingAvl(true); try { const result = await getAvlVendorsForRfq(rfqId); if (result.success && result.vendors) { - setAvlVendors(result.vendors); - - // 이미 RFQ에 추가된 벤더 ID 설정 + setAvlVendors(result.vendors as AvlVendor[]); + + // 이미 RFQ에 추가된 벤더 ID 설정 (참조용) const existingIds = new Set(result.existingVendorIds || []); setExistingVendorIds(existingIds); - - // AVL에서 가져온 모든 벤더를 기본 선택 (이미 추가된 것 제외) - const defaultSelected = new Set( - result.vendors - .filter(v => v.vendorId && !existingIds.has(v.vendorId)) - .map(v => v.vendorId!) - ); - setSelectedVendorIds(defaultSelected); - - // 초기 계약 설정 - const initialContracts = result.vendors - .filter(v => v.vendorId && defaultSelected.has(v.vendorId)) - .map(v => { - const isInternational = v.headquarterLocation && - v.headquarterLocation !== "KR" && - v.headquarterLocation !== "한국"; - return { - vendorId: v.vendorId!, - agreementYn: true, - ndaYn: true, - gtcType: isInternational ? "general" : "none" as const - }; - }); - setVendorContracts(initialContracts); - + if (result.vendors.length === 0) { toast.info("해당 프로젝트와 자재그룹에 대한 AVL 벤더가 없습니다."); } @@ -152,139 +99,22 @@ export function AvlVendorDialog({ React.useEffect(() => { if (!open) { setAvlVendors([]); - setSelectedVendorIds(new Set()); - setVendorContracts([]); - setActiveTab("vendors"); - setDefaultContract({ - agreementYn: true, - ndaYn: true, - gtcType: "none" - }); + setExistingVendorIds(new Set()); } }, [open]); - // 벤더 선택 토글 - const toggleVendorSelection = (vendorId: number) => { - const newSelection = new Set(selectedVendorIds); - if (newSelection.has(vendorId)) { - newSelection.delete(vendorId); - setVendorContracts(contracts => - contracts.filter(c => c.vendorId !== vendorId) - ); - } else { - newSelection.add(vendorId); - const vendor = avlVendors.find(v => v.vendorId === vendorId); - if (vendor) { - const isInternational = vendor.headquarterLocation && - vendor.headquarterLocation !== "KR" && - vendor.headquarterLocation !== "한국"; - setVendorContracts(contracts => [ - ...contracts, - { - vendorId, - agreementYn: defaultContract.agreementYn, - ndaYn: defaultContract.ndaYn, - gtcType: isInternational ? defaultContract.gtcType : "none" - } - ]); - } - } - setSelectedVendorIds(newSelection); - }; - - // 개별 벤더의 계약 설정 업데이트 - 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 = avlVendors.find(v => v.vendorId === c.vendorId); - const isInternational = vendor?.headquarterLocation && - vendor.headquarterLocation !== "KR" && - vendor.headquarterLocation !== "한국"; - return { - ...c, - agreementYn: defaultContract.agreementYn, - ndaYn: defaultContract.ndaYn, - gtcType: isInternational ? defaultContract.gtcType : "none" - }; - }) - ); - toast.success("모든 벤더에 기본계약 설정이 적용되었습니다."); - }; - - // 제출 처리 - const handleSubmit = async () => { - if (selectedVendorIds.size === 0) { - toast.error("최소 1개 이상의 벤더를 선택해주세요."); - return; - } - - setIsLoading(true); - try { - const selectedVendors = avlVendors.filter(v => - v.vendorId && selectedVendorIds.has(v.vendorId) - ); - - const result = await addAvlVendorsToRfq({ - rfqId, - vendors: selectedVendors.map(v => ({ - vendorId: v.vendorId!, - vendorName: v.vendorName, - vendorCode: v.vendorCode, - contractRequirements: vendorContracts.find(c => c.vendorId === v.vendorId) || { - agreementYn: true, - ndaYn: true, - gtcType: "none" as const - } - })) - }); - - if (result.success) { - toast.success( - <div> - <p>{result.addedCount}개의 AVL 벤더가 추가되었습니다.</p> - {result.skippedCount && result.skippedCount > 0 && ( - <p className="text-sm text-muted-foreground mt-1"> - {result.skippedCount}개는 이미 추가되어 있어 건너뛰었습니다. - </p> - )} - </div> - ); - onSuccess(); - onOpenChange(false); - } else { - toast.error(result.error || "벤더 추가에 실패했습니다."); - } - } catch (error) { - console.error("Submit error:", error); - toast.error("오류가 발생했습니다."); - } finally { - setIsLoading(false); - } - }; - - // 선택 가능한 벤더 필터링 - const selectableVendors = avlVendors.filter(v => v.vendorId); - const selectedVendors = selectableVendors.filter(v => selectedVendorIds.has(v.vendorId!)); + // 더 이상 핸들러 함수나 필터링이 필요 없음 - 참조용으로만 사용 return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-5xl max-h-[90vh] 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 className="flex items-center gap-2"> - <Link className="h-5 w-5 text-primary" /> - AVL 벤더 연동 + <Eye className="h-5 w-5 text-primary" /> + AVL 벤더 목록 조회 </DialogTitle> <DialogDescription> - 프로젝트 AVL에 등록된 벤더를 RFQ에 추가합니다. 선택된 벤더에게 견적 요청을 발송할 수 있습니다. + 동일한 자재그룹코드를 다루는 AVL 등록 벤더들의 목록을 확인할 수 있습니다. </DialogDescription> </DialogHeader> @@ -293,340 +123,132 @@ export function AvlVendorDialog({ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> </div> ) : ( - <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. AVL 벤더 선택 - {selectedVendorIds.size > 0 && ( - <Badge variant="secondary" className="ml-2"> - {selectedVendorIds.size} - </Badge> - )} - </TabsTrigger> - <TabsTrigger value="contracts" disabled={selectedVendorIds.size === 0}> - 2. 기본계약 설정 - </TabsTrigger> - </TabsList> - - <TabsContent value="vendors" className="flex-1 flex flex-col px-6 py-4 overflow-hidden min-h-0"> - {avlVendors.length === 0 ? ( - <Alert> - <AlertCircle className="h-4 w-4" /> - <AlertDescription> - 해당 프로젝트와 자재그룹에 대한 AVL 벤더가 없습니다. - </AlertDescription> - </Alert> - ) : ( - <Card className="flex flex-col flex-1 min-h-0"> - <CardHeader> - <CardTitle className="text-lg flex items-center justify-between"> - <span>AVL 벤더 목록</span> - <Badge variant="outline"> - 총 {avlVendors.length}개 업체 - </Badge> - </CardTitle> - <CardDescription> - AVL에서 자동으로 가져온 벤더입니다. 필요한 벤더를 선택하세요. - </CardDescription> - </CardHeader> - <CardContent className="flex-1 min-h-0"> - <ScrollArea className="h-[400px] pr-4"> - <div className="space-y-2"> - {avlVendors.map((vendor) => { - const isDisabled = !vendor.vendorId || existingVendorIds.has(vendor.vendorId); - const isSelected = vendor.vendorId && selectedVendorIds.has(vendor.vendorId); - const isInternational = vendor.headquarterLocation && - vendor.headquarterLocation !== "KR" && - vendor.headquarterLocation !== "한국"; - - return ( - <div - key={vendor.id} - className={cn( - "flex items-center justify-between p-3 rounded-lg border", - isDisabled && "opacity-50 bg-muted/30", - isSelected && !isDisabled && "bg-primary/5 border-primary/30" - )} - > - <div className="flex items-center gap-3 flex-1"> - <Checkbox - checked={isSelected} - onCheckedChange={() => vendor.vendorId && toggleVendorSelection(vendor.vendorId)} - disabled={isDisabled} - /> - - <div className="flex items-center gap-2 flex-1"> - <Building2 className="h-4 w-4 text-muted-foreground" /> - <div className="flex flex-col"> - <div className="flex items-center gap-2"> - <span className="font-medium">{vendor.vendorName}</span> - {vendor.vendorCode && ( - <Badge variant="outline" className="text-xs"> - {vendor.vendorCode} - </Badge> - )} - {existingVendorIds.has(vendor.vendorId!) && ( - <Badge variant="secondary" className="text-xs"> - <CheckCircle className="h-3 w-3 mr-1" /> - 추가됨 - </Badge> - )} - </div> - <div className="flex items-center gap-2 mt-1"> - {vendor.tier && ( - <Badge variant="outline" className="text-xs"> - 등급: {vendor.tier} - </Badge> - )} - {isInternational ? ( - <Badge variant="secondary" className="text-xs"> - <Globe className="h-3 w-3 mr-1" /> - {vendor.headquarterLocation} - </Badge> - ) : ( - <Badge variant="default" className="text-xs"> - 국내 - </Badge> - )} - {vendor.materialGroupName && ( - <span className="text-xs text-muted-foreground"> - {vendor.materialGroupName} - </span> - )} - {vendor.isAgent && ( - <Badge variant="warning" className="text-xs"> - Agent - </Badge> - )} - </div> - </div> - </div> + <div className="flex-1 flex flex-col px-6 py-4 overflow-hidden min-h-0"> + {avlVendors.length === 0 ? ( + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + 해당 프로젝트와 자재그룹에 대한 AVL 벤더가 없습니다. + </AlertDescription> + </Alert> + ) : ( + <Card className="flex flex-col flex-1 min-h-0"> + <CardHeader> + <CardTitle className="text-lg flex items-center justify-between"> + <span>AVL 등록 벤더 목록</span> + <Badge variant="outline"> + 총 {avlVendors.length}개 업체 + </Badge> + </CardTitle> + <CardDescription> + 동일한 자재그룹코드를 다루는 AVL 등록 벤더들의 정보입니다. + </CardDescription> + </CardHeader> + <CardContent className="flex-1 min-h-0"> + <ScrollArea className="h-[500px] pr-4"> + <div className="space-y-3"> + {avlVendors.map((vendor) => { + const isInCurrentRfq = vendor.vendorId && existingVendorIds.has(vendor.vendorId); + const isInternational = vendor.headquarterLocation && + vendor.headquarterLocation !== "KR" && + vendor.headquarterLocation !== "한국"; - <div className="flex items-center gap-1"> - {vendor.hasAvl && ( - <Badge variant="success" className="text-xs"> - AVL + return ( + <div + key={vendor.id} + className={cn( + "flex items-center justify-between p-4 rounded-lg border bg-card", + isInCurrentRfq && "bg-blue-50 border-blue-200" + )} + > + <div className="flex items-center gap-3 flex-1"> + <Building2 className="h-5 w-5 text-muted-foreground flex-shrink-0" /> + <div className="flex flex-col flex-1 min-w-0"> + <div className="flex items-center gap-2 flex-wrap"> + <span className="font-medium">{vendor.vendorName}</span> + {vendor.vendorCode && ( + <Badge variant="outline" className="text-xs"> + {vendor.vendorCode} + </Badge> + )} + {isInCurrentRfq && ( + <Badge variant="default" className="text-xs bg-blue-600"> + <CheckCircle className="h-3 w-3 mr-1" /> + RFQ 참여중 </Badge> )} - {vendor.isBcc && ( + </div> + <div className="flex items-center gap-2 mt-1 flex-wrap"> + {vendor.tier && ( <Badge variant="outline" className="text-xs"> - BCC + 등급: {vendor.tier} </Badge> )} - {vendor.isBlacklist && ( - <Badge variant="destructive" className="text-xs"> - Blacklist + {isInternational ? ( + <Badge variant="secondary" className="text-xs"> + <Globe className="h-3 w-3 mr-1" /> + {vendor.headquarterLocation} + </Badge> + ) : ( + <Badge variant="default" className="text-xs"> + 국내 </Badge> )} - </div> - </div> - </div> - ); - })} - </div> - </ScrollArea> - </CardContent> - </Card> - )} - </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.vendorId); - const isInternational = vendor.headquarterLocation && - vendor.headquarterLocation !== "KR" && - vendor.headquarterLocation !== "한국"; - - 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"> - {vendor.vendorCode && ( - <Badge variant="outline">{vendor.vendorCode}</Badge> + {vendor.materialGroupName && ( + <span className="text-xs text-muted-foreground"> + {vendor.materialGroupName} + </span> + )} + {vendor.isAgent && ( + <Badge variant="secondary" className="text-xs"> + Agent + </Badge> )} - <span className="font-medium">{vendor.vendorName}</span> - <Badge - variant={isInternational ? "secondary" : "default"} - className="text-xs" - > - {vendor.headquarterLocation || "미지정"} - </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) => - vendor.vendorId && updateVendorContract(vendor.vendorId, "agreementYn", !!checked) - } - /> - <label className="text-sm">기술자료 제공</label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - checked={contract?.ndaYn || false} - onCheckedChange={(checked) => - vendor.vendorId && updateVendorContract(vendor.vendorId, "ndaYn", !!checked) - } - /> - <label className="text-sm">NDA</label> - </div> </div> - - {isInternational && vendor.vendorId && ( - <div className="space-y-1"> - <Label className="text-xs">GTC</Label> - <RadioGroup - value={contract?.gtcType || "none"} - onValueChange={(value) => - updateVendorContract(vendor.vendorId!, "gtcType", value) - } - > - <div className="flex items-center space-x-2"> - <RadioGroupItem value="none" id={`${vendor.vendorId}-none`} /> - <label htmlFor={`${vendor.vendorId}-none`} className="text-xs">없음</label> - </div> - <div className="flex items-center space-x-2"> - <RadioGroupItem value="general" id={`${vendor.vendorId}-general`} /> - <label htmlFor={`${vendor.vendorId}-general`} className="text-xs">General</label> - </div> - <div className="flex items-center space-x-2"> - <RadioGroupItem value="project" id={`${vendor.vendorId}-project`} /> - <label htmlFor={`${vendor.vendorId}-project`} className="text-xs">Project</label> - </div> - </RadioGroup> - </div> - )} - - {!isInternational && ( - <div className="text-xs text-muted-foreground"> - 국내 업체 - GTC 불필요 + {vendor.remark && ( + <div className="text-xs text-muted-foreground mt-1"> + {vendor.remark} </div> )} </div> </div> - ); - })} - </div> - </ScrollArea> - </CardContent> - </Card> - </div> - </TabsContent> - </Tabs> + + <div className="flex items-center gap-1 flex-shrink-0"> + {vendor.hasAvl && ( + <Badge variant="default" className="text-xs bg-green-600"> + <CheckCircle className="h-3 w-3 mr-1" /> + AVL + </Badge> + )} + {vendor.isBcc && ( + <Badge variant="outline" className="text-xs"> + BCC + </Badge> + )} + {vendor.isBlacklist && ( + <Badge variant="destructive" className="text-xs"> + Blacklist + </Badge> + )} + </div> + </div> + ); + })} + </div> + </ScrollArea> + </CardContent> + </Card> + )} + </div> )} <DialogFooter className="p-6 pt-0 border-t"> <Button variant="outline" onClick={() => onOpenChange(false)} - disabled={isLoading} > - 취소 + 닫기 </Button> - {activeTab === "vendors" && selectedVendorIds.size > 0 && ( - <Button onClick={() => setActiveTab("contracts")}> - 다음: 기본계약 설정 - </Button> - )} - {activeTab === "contracts" && ( - <Button - onClick={handleSubmit} - disabled={isLoading || selectedVendorIds.size === 0} - > - {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {selectedVendorIds.size > 0 - ? `${selectedVendorIds.size}개 AVL 벤더 추가` - : 'AVL 벤더 추가' - } - </Button> - )} </DialogFooter> </DialogContent> </Dialog> |
