summaryrefslogtreecommitdiff
path: root/lib/itb/table/create-rfq-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/itb/table/create-rfq-dialog.tsx')
-rw-r--r--lib/itb/table/create-rfq-dialog.tsx380
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