diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx')
| -rw-r--r-- | lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx | 115 |
1 files changed, 112 insertions, 3 deletions
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx index 55dcad92..5e5d4f39 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx @@ -12,9 +12,24 @@ import { useRouter } from "next/navigation" import { getColumns } from "./vendor-quotations-table-columns" import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet" import { RfqItemsViewDialog } from "../../table/rfq-items-view-dialog" -import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service" +import { getTechSalesRfqAttachments, getVendorQuotations, rejectTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" +import { Button } from "@/components/ui/button" +import { X } from "lucide-react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" interface QuotationWithRfqCode extends TechSalesVendorQuotations { rfqCode?: string | null; @@ -95,8 +110,6 @@ function TableLoadingSkeleton() { ) } - - export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTableProps) { const searchParams = useSearchParams() const router = useRouter() @@ -110,6 +123,11 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) const [selectedRfqForItems, setSelectedRfqForItems] = React.useState<{ id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; } | null>(null) + // 거절 다이얼로그 상태 + const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false) + const [rejectionReason, setRejectionReason] = React.useState("") + const [isRejecting, setIsRejecting] = React.useState(false) + // 데이터 로딩 상태 const [data, setData] = React.useState<QuotationWithRfqCode[]>([]) const [pageCount, setPageCount] = React.useState(0) @@ -248,6 +266,54 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab setSelectedRfqForItems(rfq) setItemsDialogOpen(true) }, []) + + // 거절 처리 함수 + const handleRejectQuotations = React.useCallback(async () => { + if (!table) return; + + const selectedRows = table.getFilteredSelectedRowModel().rows; + const quotationIds = selectedRows.map(row => row.original.id); + + if (quotationIds.length === 0) { + toast.error("거절할 견적서를 선택해주세요."); + return; + } + + // 거절할 수 없는 상태의 견적서가 있는지 확인 + const invalidStatuses = selectedRows.filter(row => + row.original.status === "Accepted" || row.original.status === "Rejected" + ); + + if (invalidStatuses.length > 0) { + toast.error("이미 승인되었거나 거절된 견적서는 거절할 수 없습니다."); + return; + } + + setIsRejecting(true); + + try { + const result = await rejectTechSalesVendorQuotations({ + quotationIds, + rejectionReason: rejectionReason.trim() || undefined, + }); + + if (result.success) { + toast.success(result.message); + setRejectDialogOpen(false); + setRejectionReason(""); + table.resetRowSelection(); + // 데이터 다시 로드 + await loadData(); + } else { + toast.error(result.error || "견적서 거절 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("견적서 거절 오류:", error); + toast.error("견적서 거절 중 오류가 발생했습니다."); + } finally { + setIsRejecting(false); + } + }, [rejectionReason, loadData]); // 테이블 컬럼 정의 const columns = React.useMemo(() => getColumns({ @@ -322,6 +388,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab enableAdvancedFilter: true, enableColumnResizing: true, columnResizeMode: 'onChange', + enableRowSelection: true, // 행 선택 활성화 initialState: { sorting: initialSettings.sort, columnPinning: { right: ["actions"] }, @@ -366,6 +433,48 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab filterFields={advancedFilterFields} shallow={false} > + {/* 선택된 행이 있을 때 거절 버튼 표시 */} + {table && table.getFilteredSelectedRowModel().rows.length > 0 && ( + <AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}> + <AlertDialogTrigger asChild> + <Button variant="destructive" size="sm"> + <X className="mr-2 h-4 w-4" /> + 선택한 견적서 거절 ({table.getFilteredSelectedRowModel().rows.length}개) + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>견적서 거절</AlertDialogTitle> + <AlertDialogDescription> + 선택한 {table.getFilteredSelectedRowModel().rows.length}개의 견적서를 거절하시겠습니까? + 거절된 견적서는 다시 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <div className="grid gap-4 py-4"> + <div className="grid gap-2"> + <Label htmlFor="rejection-reason">거절 사유 (선택사항)</Label> + <Textarea + id="rejection-reason" + placeholder="거절 사유를 입력하세요..." + value={rejectionReason} + onChange={(e) => setRejectionReason(e.target.value)} + /> + </div> + </div> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleRejectQuotations} + disabled={isRejecting} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {isRejecting ? "처리 중..." : "거절"} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + )} + {!isInitialLoad && isLoading && ( <div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" /> |
