diff options
Diffstat (limited to 'lib/rfq-last')
| -rw-r--r-- | lib/rfq-last/table/create-general-rfq-dialog.tsx | 214 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-assign-pic-dialog.tsx | 279 |
2 files changed, 150 insertions, 343 deletions
diff --git a/lib/rfq-last/table/create-general-rfq-dialog.tsx b/lib/rfq-last/table/create-general-rfq-dialog.tsx index 1d369648..7263f20f 100644 --- a/lib/rfq-last/table/create-general-rfq-dialog.tsx +++ b/lib/rfq-last/table/create-general-rfq-dialog.tsx @@ -5,8 +5,7 @@ import { useForm, useFieldArray } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { format } from "date-fns" -import { CalendarIcon, Plus, Loader2, Trash2, PlusCircle, Check, ChevronsUpDown } from "lucide-react" -import { useRouter } from "next/navigation" +import { CalendarIcon, Plus, Loader2, Trash2, PlusCircle } from "lucide-react" import { useSession } from "next-auth/react" import { Button } from "@/components/ui/button" @@ -42,22 +41,18 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover" -import { - Command, - CommandInput, - CommandList, - CommandGroup, - CommandItem, - CommandEmpty, -} from "@/components/ui/command" import { Calendar } from "@/components/ui/calendar" import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { toast } from "sonner" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" -import { createGeneralRfqAction, getPUsersForFilter, previewGeneralRfqCode } from "../service" +import { createGeneralRfqAction, previewGeneralRfqCode } from "../service" import { ProjectSelector } from "@/components/ProjectSelector" +import { + PurchaseGroupCodeSingleSelector, + PurchaseGroupCodeWithUser +} from "@/components/common/selectors/purchase-group-code" // 아이템 스키마 const itemSchema = z.object({ @@ -90,21 +85,12 @@ interface CreateGeneralRfqDialogProps { export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProps) { const [open, setOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) - const [users, setUsers] = React.useState<Array<{ id: number; name: string }>>([]) - const [isLoadingUsers, setIsLoadingUsers] = React.useState(false) - const [userPopoverOpen, setUserPopoverOpen] = React.useState(false) - const [userSearchTerm, setUserSearchTerm] = React.useState("") + const [selectedPurchaseGroupCode, setSelectedPurchaseGroupCode] = React.useState<PurchaseGroupCodeWithUser | undefined>(undefined) + const [selectorOpen, setSelectorOpen] = React.useState(false) const [previewCode, setPreviewCode] = React.useState("") const [isLoadingPreview, setIsLoadingPreview] = React.useState(false) - const userOptionIdsRef = React.useRef<Record<number, string>>({}) - const router = useRouter() const { data: session } = useSession() - // 고유 ID 생성 - const buttonId = React.useMemo(() => `user-button-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, []) - const popoverContentId = React.useMemo(() => `user-popover-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, []) - const commandId = React.useMemo(() => `user-command-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, []) - const userId = React.useMemo(() => { return session?.user?.id ? Number(session.user.id) : null; }, [session]); @@ -171,49 +157,24 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp return () => subscription.unsubscribe() }, [form, generatePreviewCode]) - // 사용자 목록 로드 - React.useEffect(() => { - const loadUsers = async () => { - setIsLoadingUsers(true) - try { - const userList = await getPUsersForFilter() - setUsers(userList) - } catch (error) { - console.log("사용자 목록 로드 오류:", error) - toast.error("사용자 목록을 불러오는데 실패했습니다") - } finally { - setIsLoadingUsers(false) - } - } - loadUsers() - }, []) - - // 세션 사용자 ID로 기본값 설정 - React.useEffect(() => { - if (userId && !form.getValues("picUserId")) { - form.setValue("picUserId", userId) + // 구매그룹코드 선택 핸들러 + const handlePurchaseGroupCodeSelect = React.useCallback((code: PurchaseGroupCodeWithUser) => { + setSelectedPurchaseGroupCode(code) + + // 사용자 정보가 있으면 폼에 설정 + if (code.user) { + form.setValue("picUserId", code.user.id) + } else { + // 유저 정보가 없는 경우 경고 + toast.warning( + `해당 구매그룹코드(${code.PURCHASE_GROUP_CODE})의 사번 정보의 유저가 없습니다`, + { + description: `사번: ${code.EMPLOYEE_NUMBER}`, + duration: 5000, + } + ) } - }, [userId, form]) - - // 사용자 검색 필터링 - const userOptions = React.useMemo(() => { - return users.filter((user) => - user.name.toLowerCase().includes(userSearchTerm.toLowerCase()) - ) - }, [users, userSearchTerm]) - - // 선택된 사용자 찾기 - const selectedUser = React.useMemo(() => { - const picUserId = form.watch("picUserId") - return users.find(user => user.id === picUserId) - }, [users, form.watch("picUserId")]) - - // 사용자 선택 핸들러 - const handleSelectUser = (user: { id: number; name: string }) => { - form.setValue("picUserId", user.id) - setUserPopoverOpen(false) - setUserSearchTerm("") - } + }, [form]) // 다이얼로그 열림/닫힘 처리 및 폼 리셋 const handleOpenChange = (newOpen: boolean) => { @@ -238,8 +199,7 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp }, ], }) - setUserSearchTerm("") - setUserPopoverOpen(false) + setSelectedPurchaseGroupCode(undefined) setPreviewCode("") setIsLoadingPreview(false) } @@ -463,92 +423,47 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp )} /> - {/* 구매 담당자 - 검색 가능한 셀렉터로 변경 */} + {/* 구매 담당자 - 구매그룹코드 선택기 */} <FormField control={form.control} name="picUserId" - render={({ field }) => ( + render={() => ( <FormItem className="flex flex-col"> <FormLabel> - 견적담당자 <span className="text-red-500">*</span> + 견적담당자 (구매그룹코드) <span className="text-red-500">*</span> </FormLabel> <FormControl> - <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> - <PopoverTrigger asChild> - <Button - key={buttonId} - type="button" - variant="outline" - className="w-full justify-between relative h-9" - disabled={isLoadingUsers} - > - {isLoadingUsers ? ( - <> - <span>담당자 로딩 중...</span> - <Loader2 className="ml-2 h-4 w-4 animate-spin" /> - </> - ) : ( - <> - <span className="truncate mr-1 flex-grow text-left"> - {selectedUser ? selectedUser.name : "견적담당자를 선택하세요"} - </span> - <ChevronsUpDown className="h-4 w-4 opacity-50 flex-shrink-0" /> - </> + <Button + type="button" + variant="outline" + className="w-full justify-start h-auto min-h-[36px]" + onClick={() => setSelectorOpen(true)} + > + {selectedPurchaseGroupCode ? ( + <div className="flex flex-col items-start gap-1 w-full"> + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="font-mono text-xs"> + {selectedPurchaseGroupCode.PURCHASE_GROUP_CODE} + </Badge> + <span className="text-sm">{selectedPurchaseGroupCode.DISPLAY_NAME}</span> + </div> + {selectedPurchaseGroupCode.user && ( + <div className="text-xs text-muted-foreground"> + 담당자: {selectedPurchaseGroupCode.user.name} ({selectedPurchaseGroupCode.user.email}) + </div> )} - </Button> - </PopoverTrigger> - <PopoverContent key={popoverContentId} className="w-[300px] p-0"> - <Command key={commandId}> - <CommandInput - key={`${commandId}-input`} - placeholder="담당자 검색..." - value={userSearchTerm} - onValueChange={setUserSearchTerm} - /> - <CommandList - key={`${commandId}-list`} - className="max-h-[300px]" - onWheel={(e) => { - e.stopPropagation(); // 이벤트 전파 차단 - const target = e.currentTarget; - target.scrollTop += e.deltaY; // 직접 스크롤 처리 - }} - > - <CommandEmpty key={`${commandId}-empty`}>담당자를 찾을 수 없습니다.</CommandEmpty> - <CommandGroup key={`${commandId}-group`}> - {userOptions.map((user, userIndex) => { - if (!userOptionIdsRef.current[user.id]) { - userOptionIdsRef.current[user.id] = - `user-${user.id}-${Date.now()}-${Math.random() - .toString(36) - .slice(2, 9)}` - } - const optionId = userOptionIdsRef.current[user.id] - - return ( - <CommandItem - key={`${optionId}-${userIndex}`} - onSelect={() => handleSelectUser(user)} - value={user.name} - className="truncate" - title={user.name} - > - <span className="truncate">{user.name}</span> - <Check - key={`${optionId}-check`} - className={cn( - "ml-auto h-4 w-4 flex-shrink-0", - selectedUser?.id === user.id ? "opacity-100" : "opacity-0" - )} - /> - </CommandItem> - ) - })} - </CommandGroup> - </CommandList> - </Command> - </PopoverContent> - </Popover> + {!selectedPurchaseGroupCode.user && ( + <div className="text-xs text-orange-600"> + ⚠️ 연결된 사용자가 없습니다 + </div> + )} + </div> + ) : ( + <span className="text-muted-foreground text-sm"> + 구매그룹코드를 선택하세요 + </span> + )} + </Button> </FormControl> {/* RFQ 코드 미리보기 */} {previewCode && ( @@ -779,6 +694,17 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp </Button> </DialogFooter> </DialogContent> + + {/* 구매그룹코드 선택 다이얼로그 */} + <PurchaseGroupCodeSingleSelector + open={selectorOpen} + onOpenChange={setSelectorOpen} + selectedCode={selectedPurchaseGroupCode} + onCodeSelect={handlePurchaseGroupCodeSelect} + title="견적 담당자 선택" + description="일반견적의 담당자를 구매그룹코드로 선택하세요" + showConfirmButtons={false} + /> </Dialog> ) }
\ No newline at end of file diff --git a/lib/rfq-last/table/rfq-assign-pic-dialog.tsx b/lib/rfq-last/table/rfq-assign-pic-dialog.tsx index 94dde779..9ca34ccd 100644 --- a/lib/rfq-last/table/rfq-assign-pic-dialog.tsx +++ b/lib/rfq-last/table/rfq-assign-pic-dialog.tsx @@ -10,36 +10,15 @@ import { 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 { Loader2, Users } from "lucide-react"; 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 | null; - userCode: string | null; - deptName: string | null; - isAbsent: boolean | null; - isDeletedOnNonSap: boolean | null; - email?: string; -} +import { + PurchaseGroupCodeSingleSelector, + PurchaseGroupCodeWithUser +} from "@/components/common/selectors/purchase-group-code"; interface RfqAssignPicDialogProps { open: boolean; @@ -56,12 +35,9 @@ export function RfqAssignPicDialog({ selectedRfqCodes, onSuccess, }: RfqAssignPicDialogProps) { - const [users, setUsers] = React.useState<User[]>([] as 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(""); + const [selectedCode, setSelectedCode] = React.useState<PurchaseGroupCodeWithUser | undefined>(undefined); + const [selectorOpen, setSelectorOpen] = React.useState(false); // ITB만 필터링 (rfqCode가 "I"로 시작하는 것) const itbCodes = React.useMemo(() => { @@ -72,50 +48,36 @@ export function RfqAssignPicDialog({ 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 as User[]); - } catch (error) { - console.log("사용자 목록 로드 오류:", error); - toast.error("사용자 목록을 불러오는데 실패했습니다"); - } finally { - setIsLoadingUsers(false); - } - }; - if (open) { - loadUsers(); - // 다이얼로그 열릴 때 초기화 - setSelectedUser(null); - setUserSearchTerm(""); + setSelectedCode(undefined); } }, [open]); - // 유저 검색 - const filteredUsers = React.useMemo(() => { - if (!userSearchTerm || !userSearchTerm.trim()) return users; - - const searchTerm = userSearchTerm.trim(); - return users.filter( - (user) => - (user.name && user.name.includes(searchTerm)) || - (user.userCode && user.userCode.toLowerCase().includes(searchTerm.toLowerCase())) || - (user.deptName && user.deptName.includes(searchTerm)) - ); - }, [users, userSearchTerm]); - - const handleSelectUser = (user: User) => { - setSelectedUser(user); - setUserPopoverOpen(false); + const handleCodeSelect = (code: PurchaseGroupCodeWithUser) => { + setSelectedCode(code); + + // 유저 정보가 없는 경우 toast로 알림 + if (!code.user) { + toast.warning( + `해당 구매그룹코드(${code.PURCHASE_GROUP_CODE})의 사번 정보의 유저가 없습니다`, + { + description: `사번: ${code.EMPLOYEE_NUMBER}`, + duration: 5000, + } + ); + } }; const handleAssign = async () => { - if (!selectedUser) { - toast.error("담당자를 선택해주세요"); + if (!selectedCode) { + toast.error("구매그룹코드를 선택해주세요"); + return; + } + + if (!selectedCode.user) { + toast.error("선택한 구매그룹코드에 연결된 사용자가 없습니다"); return; } @@ -128,7 +90,7 @@ export function RfqAssignPicDialog({ try { const result = await assignPicToRfqs({ rfqIds: itbIds, - picUserId: selectedUser.id, + picUserId: selectedCode.user.id, }); if (result.success) { @@ -196,144 +158,63 @@ export function RfqAssignPicDialog({ )} </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" /> - </> + <label className="text-sm font-medium">구매 담당자 (구매그룹코드)</label> + <Button + type="button" + variant="outline" + className="w-full justify-start h-auto min-h-[40px]" + disabled={itbCodes.length === 0} + onClick={() => setSelectorOpen(true)} + > + {selectedCode ? ( + <div className="flex flex-col items-start gap-1 w-full"> + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="font-mono"> + {selectedCode.PURCHASE_GROUP_CODE} + </Badge> + <span>{selectedCode.DISPLAY_NAME}</span> + </div> + {selectedCode.user && ( + <div className="text-xs text-muted-foreground"> + 사용자: {selectedCode.user.name} ({selectedCode.user.email}) + </div> )} - </Button> - </PopoverTrigger> - <PopoverContent className="w-[460px] p-0"> - <Command shouldFilter={false}> - <CommandInput - placeholder="이름, 구매그룹코드 또는 부서로 검색..." - value={userSearchTerm} - onValueChange={setUserSearchTerm} - /> - <CommandList className="max-h-[300px]"> - <CommandEmpty>검색 결과가 없습니다</CommandEmpty> - <CommandGroup - className="overflow-y-auto" - onWheel={(e) => { - // 마우스 휠 스크롤이 제대로 작동하도록 이벤트 전파 허용 - e.stopPropagation(); - }} - > - {filteredUsers.map((user) => ( - <CommandItem - key={user.id} - value={`${user.name || ''} ${user.userCode || ''} ${user.deptName || ''}`} - onSelect={() => handleSelectUser(user)} - className="flex items-center justify-between" - > - <div className="flex flex-col gap-1"> - <div className="flex items-center gap-2"> - <User className="h-4 w-4" /> - <span>{user.name || '이름 없음'}</span> - {user.userCode && ( - <span className="text-muted-foreground text-sm"> - ({user.userCode}) - </span> - )} - {(user.isAbsent || user.isDeletedOnNonSap) && ( - <div className="flex gap-1"> - {user.isAbsent && ( - <Badge variant="outline" className="text-xs px-1 py-0"> - 휴직 - </Badge> - )} - {user.isDeletedOnNonSap && ( - <Badge variant="outline" className="text-xs px-1 py-0"> - 퇴직 - </Badge> - )} - </div> - )} - </div> - {user.deptName && ( - <span className="text-xs text-muted-foreground"> - {user.deptName} - </span> - )} - </div> - <Check - className={cn( - "h-4 w-4", - selectedUser?.id === user.id - ? "opacity-100" - : "opacity-0" - )} - /> - </CommandItem> - ))} - </CommandGroup> - </CommandList> - </Command> - </PopoverContent> - </Popover> - {selectedUser && ( - <div className="text-xs text-muted-foreground"> - <div className="flex items-center gap-2"> - <p> - 선택한 담당자: {selectedUser.name || '이름 없음'} - {selectedUser.userCode && ` (${selectedUser.userCode})`} - </p> - {(selectedUser.isAbsent || selectedUser.isDeletedOnNonSap) && ( - <div className="flex gap-1"> - {selectedUser.isAbsent && ( - <Badge variant="outline" className="text-xs px-1 py-0 border-orange-300 text-orange-700 bg-orange-50"> - 휴직 - </Badge> - )} - {selectedUser.isDeletedOnNonSap && ( - <Badge variant="outline" className="text-xs px-1 py-0 border-red-300 text-red-700 bg-red-50"> - 퇴직 - </Badge> - )} + {!selectedCode.user && ( + <div className="text-xs text-orange-600"> + ⚠️ 연결된 사용자가 없습니다 </div> )} </div> - {selectedUser.deptName && ( - <p>{selectedUser.deptName}</p> - )} - </div> + ) : ( + <span className="text-muted-foreground"> + 구매그룹코드를 선택하세요 + </span> + )} + </Button> + + {selectedCode && !selectedCode.user && ( + <Alert className="border-orange-200 bg-orange-50"> + <AlertDescription className="text-orange-800 text-xs"> + 선택한 구매그룹코드에 연결된 사용자가 없습니다. 다른 구매그룹코드를 선택해주세요. + </AlertDescription> + </Alert> )} </div> </div> + {/* 구매그룹코드 선택 다이얼로그 */} + <PurchaseGroupCodeSingleSelector + open={selectorOpen} + onOpenChange={setSelectorOpen} + selectedCode={selectedCode} + onCodeSelect={handleCodeSelect} + title="구매 담당자 선택" + description="ITB에 지정할 구매 담당자의 구매그룹코드를 선택하세요" + showConfirmButtons={false} + /> + <DialogFooter> <Button type="button" @@ -346,7 +227,7 @@ export function RfqAssignPicDialog({ <Button type="submit" onClick={handleAssign} - disabled={!selectedUser || itbCodes.length === 0 || isAssigning} + disabled={!selectedCode || !selectedCode.user || itbCodes.length === 0 || isAssigning} > {isAssigning ? ( <> @@ -361,4 +242,4 @@ export function RfqAssignPicDialog({ </DialogContent> </Dialog> ); -}
\ No newline at end of file +} |
