diff options
Diffstat (limited to 'lib/rfq-last/table')
| -rw-r--r-- | lib/rfq-last/table/delete-rfq-dialog.tsx | 254 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-columns.tsx | 1 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-toolbar-actions.tsx | 56 |
3 files changed, 310 insertions, 1 deletions
diff --git a/lib/rfq-last/table/delete-rfq-dialog.tsx b/lib/rfq-last/table/delete-rfq-dialog.tsx new file mode 100644 index 00000000..01af5453 --- /dev/null +++ b/lib/rfq-last/table/delete-rfq-dialog.tsx @@ -0,0 +1,254 @@ +"use client";
+
+import * as React from "react";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { RfqsLastView } from "@/db/schema";
+import { deleteRfq } from "@/lib/rfq-last/delete-action";
+import { Loader2, AlertTriangle } from "lucide-react";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { toast } from "sonner";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+
+interface DeleteRfqDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ selectedRfqs: RfqsLastView[];
+ onSuccess?: () => void;
+}
+
+export function DeleteRfqDialog({
+ open,
+ onOpenChange,
+ selectedRfqs,
+ onSuccess,
+}: DeleteRfqDialogProps) {
+ const [isDeleting, setIsDeleting] = React.useState(false);
+ const [deleteReason, setDeleteReason] = React.useState("");
+
+ // ANFNR이 있는 RFQ만 필터링
+ const rfqsWithAnfnr = React.useMemo(() => {
+ return selectedRfqs.filter(rfq => rfq.ANFNR && rfq.ANFNR.trim() !== "");
+ }, [selectedRfqs]);
+
+ const handleDelete = async () => {
+ if (rfqsWithAnfnr.length === 0) {
+ toast.error("ANFNR이 있는 RFQ가 선택되지 않았습니다.");
+ return;
+ }
+
+ if (!deleteReason || deleteReason.trim() === "") {
+ toast.error("삭제 사유를 입력해주세요.");
+ return;
+ }
+
+ setIsDeleting(true);
+
+ try {
+ const rfqIds = rfqsWithAnfnr.map(rfq => rfq.id);
+ const result = await deleteRfq(rfqIds, deleteReason.trim());
+
+ if (result.results) {
+ const successCount = result.results.filter(r => r.success).length;
+ const failCount = result.results.length - successCount;
+
+ if (result.success) {
+ // 성공한 RFQ 목록
+ const successRfqs = result.results
+ .filter(r => r.success)
+ .map(r => {
+ const rfq = rfqsWithAnfnr.find(rf => rf.id === r.rfqId);
+ return rfq?.rfqCode || `RFQ ID: ${r.rfqId}`;
+ });
+
+ if (successCount > 0) {
+ toast.success(
+ `RFQ 삭제가 완료되었습니다. (${successCount}건)`,
+ {
+ description: successRfqs.length <= 3
+ ? successRfqs.join(", ")
+ : `${successRfqs.slice(0, 3).join(", ")} 외 ${successRfqs.length - 3}건`,
+ duration: 5000,
+ }
+ );
+ }
+
+ // 실패한 RFQ가 있는 경우
+ if (failCount > 0) {
+ const failRfqs = result.results
+ .filter(r => !r.success)
+ .map(r => {
+ const rfq = rfqsWithAnfnr.find(rf => rf.id === r.rfqId);
+ return `${rfq?.rfqCode || r.rfqId}: ${r.error || "알 수 없는 오류"}`;
+ });
+
+ toast.error(
+ `${failCount}건의 RFQ 삭제가 실패했습니다.`,
+ {
+ description: failRfqs.length <= 3
+ ? failRfqs.join(", ")
+ : `${failRfqs.slice(0, 3).join(", ")} 외 ${failRfqs.length - 3}건`,
+ duration: 7000,
+ }
+ );
+ }
+ } else {
+ // 전체 실패
+ toast.error(result.message || "RFQ 삭제에 실패했습니다.");
+ }
+ } else {
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error(result.message);
+ }
+ }
+
+ // 성공 여부와 관계없이 다이얼로그 닫기 및 콜백 호출
+ if (result.success) {
+ setDeleteReason(""); // 성공 시 입력 필드 초기화
+ onOpenChange(false);
+ onSuccess?.();
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : "RFQ 삭제 중 오류가 발생했습니다.");
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!isDeleting) {
+ setDeleteReason(""); // 다이얼로그 닫을 때 입력 필드 초기화
+ onOpenChange(false);
+ }
+ };
+
+ // ANFNR이 없는 RFQ가 포함된 경우 경고 표시
+ const rfqsWithoutAnfnr = selectedRfqs.filter(rfq => !rfq.ANFNR || rfq.ANFNR.trim() === "");
+ const hasWarning = rfqsWithoutAnfnr.length > 0;
+
+ return (
+ <AlertDialog open={open} onOpenChange={handleClose}>
+ <AlertDialogContent className="max-w-2xl">
+ <AlertDialogHeader>
+ <AlertDialogTitle>RFQ 삭제</AlertDialogTitle>
+ <AlertDialogDescription className="space-y-4">
+ {isDeleting ? (
+ /* 로딩 중 상태 - 다른 내용 숨김 */
+ <div className="flex items-center justify-center gap-3 py-8">
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
+ <div className="flex flex-col">
+ <span className="text-base font-medium">RFQ 삭제 처리 중...</span>
+ <span className="text-sm text-muted-foreground mt-1">
+ ECC로 취소 요청을 전송하고 있습니다.
+ </span>
+ </div>
+ </div>
+ ) : (
+ <>
+ <div>
+ 선택된 RFQ 중 ANFNR이 있는 RFQ만 삭제됩니다.
+ </div>
+
+ {/* 삭제 대상 RFQ 목록 */}
+ {rfqsWithAnfnr.length > 0 && (
+ <div className="space-y-2">
+ <p className="font-medium text-sm">삭제 대상 RFQ ({rfqsWithAnfnr.length}건):</p>
+ <div className="max-h-40 overflow-y-auto border rounded-md p-3 space-y-1">
+ {rfqsWithAnfnr.map((rfq) => (
+ <div key={rfq.id} className="text-sm">
+ <span className="font-mono font-medium">{rfq.rfqCode}</span>
+ {rfq.rfqTitle && (
+ <span className="text-muted-foreground ml-2">
+ - {rfq.rfqTitle}
+ </span>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* ANFNR이 없는 RFQ 경고 */}
+ {hasWarning && (
+ <Alert variant="destructive">
+ <AlertTriangle className="h-4 w-4" />
+ <AlertDescription>
+ <div className="space-y-2">
+ <p className="font-medium">ANFNR이 없는 RFQ는 삭제할 수 없습니다 ({rfqsWithoutAnfnr.length}건):</p>
+ <div className="max-h-32 overflow-y-auto space-y-1">
+ {rfqsWithoutAnfnr.map((rfq) => (
+ <div key={rfq.id} className="text-sm">
+ <span className="font-mono">{rfq.rfqCode}</span>
+ {rfq.rfqTitle && (
+ <span className="text-muted-foreground ml-2">
+ - {rfq.rfqTitle}
+ </span>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* 삭제 사유 입력 */}
+ <div className="space-y-2">
+ <Label htmlFor="delete-reason" className="text-sm font-medium">
+ 삭제 사유 <span className="text-destructive">*</span>
+ </Label>
+ <Textarea
+ id="delete-reason"
+ placeholder="RFQ 삭제 사유를 입력해주세요..."
+ value={deleteReason}
+ onChange={(e) => setDeleteReason(e.target.value)}
+ disabled={isDeleting}
+ className="min-h-[100px] resize-none"
+ required
+ />
+ </div>
+
+ {/* 안내 메시지 */}
+ <div className="text-sm text-muted-foreground space-y-1">
+ <p>• ANFNR이 있는 RFQ만 삭제됩니다.</p>
+ <p>• ECC로 SOAP 취소 요청이 전송됩니다.</p>
+ <p>• 성공 시 RFQ 상태가 RFQ 삭제로 변경됩니다.</p>
+ <p>• 연결된 TBE 세션도 취소 상태로 변경됩니다.</p>
+ </div>
+ </>
+ )}
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleDelete}
+ disabled={isDeleting || rfqsWithAnfnr.length === 0 || !deleteReason || deleteReason.trim() === ""}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {isDeleting ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 삭제 중...
+ </>
+ ) : (
+ "RFQ 삭제"
+ )}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ );
+}
+
diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx index d0a9ee1e..e8a5ba94 100644 --- a/lib/rfq-last/table/rfq-table-columns.tsx +++ b/lib/rfq-last/table/rfq-table-columns.tsx @@ -39,6 +39,7 @@ const getStatusBadgeVariant = (status: string) => { case "RFQ 발송": return "default"; case "견적접수": return "default"; case "최종업체선정": return "default"; + case "RFQ 삭제": return "destructive"; default: return "outline"; } }; diff --git a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx index 148336fb..a6dc1ad4 100644 --- a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx +++ b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx @@ -3,11 +3,12 @@ import * as React from "react"; import { Table } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; -import { Users, RefreshCw, FileDown, Plus, Edit } from "lucide-react"; +import { Users, RefreshCw, FileDown, Plus, Edit, Trash2 } from "lucide-react"; import { RfqsLastView } from "@/db/schema"; import { RfqAssignPicDialog } from "./rfq-assign-pic-dialog"; import { CreateGeneralRfqDialog } from "./create-general-rfq-dialog"; // 추가 import { UpdateGeneralRfqDialog } from "./update-general-rfq-dialog"; // 수정용 +import { DeleteRfqDialog } from "./delete-rfq-dialog"; import { Badge } from "@/components/ui/badge"; import { Tooltip, @@ -29,6 +30,7 @@ export function RfqTableToolbarActions<TData>({ }: RfqTableToolbarActionsProps<TData>) { const [showAssignDialog, setShowAssignDialog] = React.useState(false); const [showUpdateDialog, setShowUpdateDialog] = React.useState(false); + const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); const [selectedRfqForUpdate, setSelectedRfqForUpdate] = React.useState<number | null>(null); console.log(rfqCategory) @@ -47,6 +49,9 @@ export function RfqTableToolbarActions<TData>({ // 수정 가능한 RFQ (general 카테고리에서 RFQ 생성 상태인 항목, 단일 선택만) const updatableRfq = rfqCategory === "general" && rows.length === 1 && rows[0].status === "RFQ 생성" ? rows[0] : null; + // ANFNR이 있는 RFQ만 필터링 (삭제 가능한 RFQ) + const deletableRows = rows.filter(row => row.ANFNR && row.ANFNR.trim() !== ""); + return { ids: rows.map(row => row.id), codes: rows.map(row => row.rfqCode || ""), @@ -61,6 +66,10 @@ export function RfqTableToolbarActions<TData>({ // 수정 가능한 RFQ 정보 updatableRfq: updatableRfq, canUpdate: updatableRfq !== null, + // 삭제 가능한 RFQ 정보 + deletableRows: deletableRows, + deletableCount: deletableRows.length, + canDelete: deletableRows.length > 0, }; }, [selectedRows, rfqCategory]); @@ -92,6 +101,13 @@ export function RfqTableToolbarActions<TData>({ } }; + const handleDeleteSuccess = () => { + // 테이블 선택 초기화 + table.toggleAllPageRowsSelected(false); + // 데이터 새로고침 + onRefresh?.(); + }; + return ( <> <div className="flex items-center gap-2"> @@ -125,6 +141,36 @@ export function RfqTableToolbarActions<TData>({ </TooltipProvider> )} + {/* RFQ 삭제 버튼 - ANFNR이 있는 RFQ가 선택된 경우에만 활성화 */} + {selectedRfqData.totalCount > 0 && selectedRfqData.canDelete && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="destructive" + size="sm" + onClick={() => setShowDeleteDialog(true)} + className="flex items-center gap-2" + > + <Trash2 className="h-4 w-4" /> + RFQ 삭제 + <Badge variant="secondary" className="ml-1"> + {selectedRfqData.deletableCount}건 + </Badge> + </Button> + </TooltipTrigger> + <TooltipContent> + <p>선택한 RFQ를 삭제합니다 (ANFNR이 있는 RFQ만 삭제 가능)</p> + {selectedRfqData.deletableCount !== selectedRfqData.totalCount && ( + <p className="text-xs text-muted-foreground mt-1"> + 전체 {selectedRfqData.totalCount}건 중 {selectedRfqData.deletableCount}건만 삭제 가능합니다 + </p> + )} + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + {/* 선택된 항목 표시 */} {selectedRfqData.totalCount > 0 && ( <div className="flex items-center gap-2 px-3 py-1.5 bg-muted rounded-md"> @@ -198,6 +244,14 @@ export function RfqTableToolbarActions<TData>({ rfqId={selectedRfqForUpdate || 0} onSuccess={handleUpdateGeneralRfqSuccess} /> + + {/* RFQ 삭제 다이얼로그 */} + <DeleteRfqDialog + open={showDeleteDialog} + onOpenChange={setShowDeleteDialog} + selectedRfqs={selectedRfqData.deletableRows} + onSuccess={handleDeleteSuccess} + /> </> ); }
\ No newline at end of file |
