diff options
Diffstat (limited to 'lib/rfq-last/table/rfq-assign-pic-dialog.tsx')
| -rw-r--r-- | lib/rfq-last/table/rfq-assign-pic-dialog.tsx | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/lib/rfq-last/table/rfq-assign-pic-dialog.tsx b/lib/rfq-last/table/rfq-assign-pic-dialog.tsx new file mode 100644 index 00000000..89dda979 --- /dev/null +++ b/lib/rfq-last/table/rfq-assign-pic-dialog.tsx @@ -0,0 +1,311 @@ +"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 { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Check, ChevronsUpDown, Loader2, User, Users } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; +import { getPUsersForFilter } from "@/lib/rfq-last/service"; +import { assignPicToRfqs } from "../service"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; + +interface User { + id: number; + name: string; + userCode?: string; + email?: string; +} + +interface RfqAssignPicDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedRfqIds: number[]; + selectedRfqCodes: string[]; + onSuccess?: () => void; +} + +export function RfqAssignPicDialog({ + open, + onOpenChange, + selectedRfqIds, + selectedRfqCodes, + onSuccess, +}: RfqAssignPicDialogProps) { + const [users, setUsers] = React.useState<User[]>([]); + const [isLoadingUsers, setIsLoadingUsers] = React.useState(false); + const [isAssigning, setIsAssigning] = React.useState(false); + const [selectedUser, setSelectedUser] = React.useState<User | null>(null); + const [userPopoverOpen, setUserPopoverOpen] = React.useState(false); + const [userSearchTerm, setUserSearchTerm] = React.useState(""); + + // ITB만 필터링 (rfqCode가 "I"로 시작하는 것) + const itbCodes = React.useMemo(() => { + return selectedRfqCodes.filter(code => code.startsWith("I")); + }, [selectedRfqCodes]); + + const itbIds = React.useMemo(() => { + return selectedRfqIds.filter((id, index) => selectedRfqCodes[index]?.startsWith("I")); + }, [selectedRfqIds, selectedRfqCodes]); + + // 유저 목록 로드 + 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(); + // 다이얼로그 열릴 때 초기화 + setSelectedUser(null); + setUserSearchTerm(""); + } + }, [open]); + + // 유저 검색 + const filteredUsers = React.useMemo(() => { + if (!userSearchTerm) return users; + + const lowerSearchTerm = userSearchTerm.toLowerCase(); + return users.filter( + (user) => + user.name.toLowerCase().includes(lowerSearchTerm) || + user.userCode?.toLowerCase().includes(lowerSearchTerm) + ); + }, [users, userSearchTerm]); + + const handleSelectUser = (user: User) => { + setSelectedUser(user); + setUserPopoverOpen(false); + }; + + const handleAssign = async () => { + if (!selectedUser) { + toast.error("담당자를 선택해주세요"); + return; + } + + if (itbIds.length === 0) { + toast.error("선택한 항목 중 ITB가 없습니다"); + return; + } + + setIsAssigning(true); + try { + const result = await assignPicToRfqs({ + rfqIds: itbIds, + picUserId: selectedUser.id, + }); + + if (result.success) { + toast.success(result.message); + onSuccess?.(); + onOpenChange(false); + } else { + toast.error(result.message); + } + } catch (error) { + console.error("담당자 지정 오류:", error); + toast.error("담당자 지정 중 오류가 발생했습니다"); + } finally { + setIsAssigning(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Users className="h-5 w-5" /> + 담당자 지정 + </DialogTitle> + <DialogDescription> + 선택한 ITB에 구매 담당자를 지정합니다 + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 선택된 ITB 정보 */} + <div className="space-y-2"> + <label className="text-sm font-medium">선택된 ITB</label> + <div className="p-3 bg-muted rounded-md"> + <div className="flex items-center gap-2 mb-2"> + <Badge variant="secondary">{itbCodes.length}건</Badge> + {itbCodes.length !== selectedRfqCodes.length && ( + <span className="text-xs text-muted-foreground"> + (전체 {selectedRfqCodes.length}건 중) + </span> + )} + </div> + <div className="max-h-[100px] overflow-y-auto"> + <div className="flex flex-wrap gap-1"> + {itbCodes.slice(0, 10).map((code, index) => ( + <Badge key={index} variant="outline" className="text-xs"> + {code} + </Badge> + ))} + {itbCodes.length > 10 && ( + <Badge variant="outline" className="text-xs"> + +{itbCodes.length - 10}개 + </Badge> + )} + </div> + </div> + </div> + {itbCodes.length === 0 && ( + <Alert className="border-orange-200 bg-orange-50"> + <AlertDescription className="text-orange-800"> + 선택한 항목 중 ITB (I로 시작하는 코드)가 없습니다. + </AlertDescription> + </Alert> + )} + </div> + + {/* 담당자 선택 */} + <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 || itbCodes.length === 0} + > + {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-[460px] 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> + {selectedUser && ( + <p className="text-xs text-muted-foreground"> + 선택한 담당자: {selectedUser.name} + {selectedUser.userCode && ` (${selectedUser.userCode})`} + </p> + )} + </div> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isAssigning} + > + 취소 + </Button> + <Button + type="submit" + onClick={handleAssign} + disabled={!selectedUser || itbCodes.length === 0 || isAssigning} + > + {isAssigning ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 지정 중... + </> + ) : ( + "담당자 지정" + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
