diff options
Diffstat (limited to 'lib/itb/table/create-rfq-dialog.tsx')
| -rw-r--r-- | lib/itb/table/create-rfq-dialog.tsx | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/lib/itb/table/create-rfq-dialog.tsx b/lib/itb/table/create-rfq-dialog.tsx new file mode 100644 index 00000000..57a4b9d4 --- /dev/null +++ b/lib/itb/table/create-rfq-dialog.tsx @@ -0,0 +1,380 @@ +// components/purchase-requests/create-rfq-dialog.tsx +"use client"; + +import * as React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { + FileText, + Package, + AlertCircle, + CheckCircle, + User, + ChevronsUpDown, + Check, + Loader2, + Info +} from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import type { PurchaseRequestView } from "@/db/schema"; +import { approvePurchaseRequestsAndCreateRfqs } from "../service"; +import { getPUsersForFilter } from "@/lib/rfq-last/service"; +import { useRouter } from "next/navigation"; + +interface CreateRfqDialogProps { + requests: PurchaseRequestView[]; + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess?: () => void; +} + +export function CreateRfqDialog({ + requests, + open, + onOpenChange, + onSuccess, +}: CreateRfqDialogProps) { + const [isLoading, setIsLoading] = React.useState(false); + const [userPopoverOpen, setUserPopoverOpen] = React.useState(false); + const [users, setUsers] = React.useState<any[]>([]); + const [selectedUser, setSelectedUser] = React.useState<any>(null); + const [isLoadingUsers, setIsLoadingUsers] = React.useState(false); + const [userSearchTerm, setUserSearchTerm] = React.useState(""); + const router = useRouter(); + + // 유저 목록 로드 + React.useEffect(() => { + const loadUsers = async () => { + setIsLoadingUsers(true); + try { + const userList = await getPUsersForFilter(); + setUsers(userList); + } catch (error) { + console.log("사용자 목록 로드 오류:", error); + toast.error("사용자 목록을 불러오는데 실패했습니다"); + } finally { + setIsLoadingUsers(false); + } + }; + + if (open) { + loadUsers(); + } + }, [open]); + + // 검색된 사용자 필터링 + const filteredUsers = React.useMemo(() => { + if (!userSearchTerm) return users; + + return users.filter(user => + user.name.toLowerCase().includes(userSearchTerm.toLowerCase()) || + user.userCode?.toLowerCase().includes(userSearchTerm.toLowerCase()) + ); + }, [users, userSearchTerm]); + + // 유효한 요청만 필터링 (이미 RFQ 생성된 것 제외) + const validRequests = requests.filter(r => r.status !== "RFQ생성완료"); + const invalidRequests = requests.filter(r => r.status === "RFQ생성완료"); + + const handleSelectUser = (user: any) => { + setSelectedUser(user); + setUserPopoverOpen(false); + }; + + const handleSubmit = async () => { + if (validRequests.length === 0) { + toast.error("RFQ를 생성할 수 있는 구매 요청이 없습니다"); + return; + } + + try { + setIsLoading(true); + + const requestIds = validRequests.map(r => r.id); + const results = await approvePurchaseRequestsAndCreateRfqs( + requestIds, + selectedUser?.id + ); + + const successCount = results.filter(r => r.success).length; + const skipCount = results.filter(r => r.skipped).length; + + if (successCount > 0) { + toast.success(`${successCount}개의 RFQ가 생성되었습니다`); + } + + if (skipCount > 0) { + toast.info(`${skipCount}개는 이미 RFQ가 생성되어 건너뛰었습니다`); + } + + onOpenChange(false); + onSuccess?.(); + router.refresh() + } catch (error) { + console.error("RFQ 생성 오류:", error); + toast.error("RFQ 생성 중 오류가 발생했습니다"); + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + if (!isLoading) { + setSelectedUser(null); + setUserSearchTerm(""); + onOpenChange(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={handleClose}> + <DialogContent className="max-w-4xl"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + RFQ 생성 + </DialogTitle> + <DialogDescription> + 선택한 구매 요청을 기반으로 RFQ를 생성합니다. + {invalidRequests.length > 0 && " 이미 RFQ가 생성된 항목은 제외됩니다."} + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 경고 메시지 */} + {invalidRequests.length > 0 && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + {invalidRequests.length}개 항목은 이미 RFQ가 생성되어 제외됩니다. + </AlertDescription> + </Alert> + )} + + {/* 구매 담당자 선택 */} + <div className="space-y-2"> + <label className="text-sm font-medium"> + 구매 담당자 (선택사항) + </label> + <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> + <PopoverTrigger asChild> + <Button + type="button" + variant="outline" + className="w-full justify-between h-10" + disabled={isLoadingUsers} + > + {isLoadingUsers ? ( + <> + <span>담당자 로딩 중...</span> + <Loader2 className="ml-2 h-4 w-4 animate-spin" /> + </> + ) : ( + <> + <span className="flex items-center gap-2"> + <User className="h-4 w-4" /> + {selectedUser ? ( + <> + {selectedUser.name} + {selectedUser.userCode && ( + <span className="text-muted-foreground"> + ({selectedUser.userCode}) + </span> + )} + </> + ) : ( + <span className="text-muted-foreground"> + 구매 담당자를 선택하세요 (선택사항) + </span> + )} + </span> + <ChevronsUpDown className="h-4 w-4 opacity-50" /> + </> + )} + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput + placeholder="이름 또는 코드로 검색..." + value={userSearchTerm} + onValueChange={setUserSearchTerm} + /> + <CommandList className="max-h-[300px]"> + <CommandEmpty>검색 결과가 없습니다</CommandEmpty> + <CommandGroup> + {filteredUsers.map((user) => ( + <CommandItem + key={user.id} + onSelect={() => handleSelectUser(user)} + className="flex items-center justify-between" + > + <span className="flex items-center gap-2"> + <User className="h-4 w-4" /> + {user.name} + {user.userCode && ( + <span className="text-muted-foreground text-sm"> + ({user.userCode}) + </span> + )} + </span> + <Check + className={cn( + "h-4 w-4", + selectedUser?.id === user.id ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <p className="text-xs text-muted-foreground"> + 구매 담당자를 선택하지 않으면 나중에 지정할 수 있습니다 + </p> + </div> + + {/* RFQ 생성 대상 목록 */} + <div className="space-y-2"> + <label className="text-sm font-medium"> + RFQ 생성 대상 ({validRequests.length}개) + </label> + <div className="border rounded-lg max-h-[300px] overflow-y-auto"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[140px]">요청번호</TableHead> + <TableHead>요청제목</TableHead> + <TableHead className="w-[120px]">프로젝트</TableHead> + <TableHead className="w-[100px]">패키지</TableHead> + <TableHead className="w-[80px] text-center">품목</TableHead> + <TableHead className="w-[80px] text-center">첨부</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {validRequests.length === 0 ? ( + <TableRow> + <TableCell colSpan={6} className="text-center text-muted-foreground py-8"> + RFQ를 생성할 수 있는 구매 요청이 없습니다 + </TableCell> + </TableRow> + ) : ( + validRequests.map((request) => ( + <TableRow key={request.id}> + <TableCell className="font-mono text-sm"> + {request.requestCode} + </TableCell> + <TableCell className="max-w-[250px]"> + <div className="truncate" title={request.requestTitle}> + {request.requestTitle} + </div> + </TableCell> + <TableCell> + <div className="truncate" title={request.projectName}> + {request.projectCode} + </div> + </TableCell> + <TableCell> + <div className="truncate" title={request.packageName}> + {request.packageNo} + </div> + </TableCell> + <TableCell className="text-center"> + {request.itemCount > 0 && ( + <Badge variant="secondary" className="gap-1"> + <Package className="h-3 w-3" /> + {request.itemCount} + </Badge> + )} + </TableCell> + <TableCell className="text-center"> + {request.attachmentCount > 0 && ( + <Badge variant="secondary" className="gap-1"> + <FileText className="h-3 w-3" /> + {request.attachmentCount} + </Badge> + )} + </TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + </div> + </div> + + {/* 안내 메시지 */} + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + <ul className="list-disc list-inside space-y-1 text-sm"> + <li>RFQ 생성 시 구매 요청의 첨부파일이 자동으로 이관됩니다</li> + <li>구매 요청 상태가 "RFQ생성완료"로 변경됩니다</li> + <li>각 구매 요청별로 개별 RFQ가 생성됩니다</li> + </ul> + </AlertDescription> + </Alert> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={handleClose} + disabled={isLoading} + > + 취소 + </Button> + <Button + onClick={handleSubmit} + disabled={isLoading || validRequests.length === 0} + > + {isLoading ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + RFQ 생성 중... + </> + ) : ( + <> + <CheckCircle className="mr-2 h-4 w-4" /> + RFQ 생성 ({validRequests.length}개) + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
