summaryrefslogtreecommitdiff
path: root/lib/rfq-last/table/rfq-assign-pic-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-18 00:23:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-18 00:23:40 +0000
commitcf8dac0c6490469dab88a560004b0c07dbd48612 (patch)
treeb9e76061e80d868331e6b4277deecb9086f845f3 /lib/rfq-last/table/rfq-assign-pic-dialog.tsx
parente5745fc0268bbb5770bc14a55fd58a0ec30b466e (diff)
(대표님) rfq, 계약, 서명 등
Diffstat (limited to 'lib/rfq-last/table/rfq-assign-pic-dialog.tsx')
-rw-r--r--lib/rfq-last/table/rfq-assign-pic-dialog.tsx311
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