diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response')
| -rw-r--r-- | lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx | 88 | ||||
| -rw-r--r-- | lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx | 115 |
2 files changed, 163 insertions, 40 deletions
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx index ddee2317..b89f8953 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx @@ -15,7 +15,8 @@ import { } from "@/components/ui/tooltip" import { TechSalesVendorQuotations, - TECH_SALES_QUOTATION_STATUS_CONFIG + TECH_SALES_QUOTATION_STATUS_CONFIG, + TECH_SALES_QUOTATION_STATUSES } from "@/db/schema" import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" @@ -70,14 +71,21 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge className="translate-y-0.5" /> ), - cell: ({ row }) => ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={(value) => row.toggleSelected(!!value)} + cell: ({ row }) => { + const isRejected = row.original.status === TECH_SALES_QUOTATION_STATUSES.REJECTED; + const isAccepted = row.original.status === TECH_SALES_QUOTATION_STATUSES.ACCEPTED; + const isDisabled = isRejected || isAccepted; + + return ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="행 선택" - className="translate-y-0.5" - /> - ), + className="translate-y-0.5" + disabled={isDisabled} + /> + ); + }, enableSorting: false, enableHiding: false, }, @@ -158,33 +166,33 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge // enableSorting: true, // enableHiding: true, // }, - { - accessorKey: "itemName", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="자재명" /> - ), - cell: ({ row }) => { - const itemName = row.getValue("itemName") as string; - return ( - <div className="min-w-48 max-w-64"> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <span className="truncate block text-sm"> - {itemName || "N/A"} - </span> - </TooltipTrigger> - <TooltipContent> - <p className="max-w-xs">{itemName || "N/A"}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - ); - }, - enableSorting: true, - enableHiding: true, - }, + // { + // accessorKey: "itemName", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="자재명" /> + // ), + // cell: ({ row }) => { + // const itemName = row.getValue("itemName") as string; + // return ( + // <div className="min-w-48 max-w-64"> + // <TooltipProvider> + // <Tooltip> + // <TooltipTrigger asChild> + // <span className="truncate block text-sm"> + // {itemName || "N/A"} + // </span> + // </TooltipTrigger> + // <TooltipContent> + // <p className="max-w-xs">{itemName || "N/A"}</p> + // </TooltipContent> + // </Tooltip> + // </TooltipProvider> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "projNm", header: ({ column }) => ( @@ -597,6 +605,9 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge const quotation = row.original; const rfqCode = quotation.rfqCode || "N/A"; const tooltipText = `${rfqCode} 견적서 작성`; + const isRejected = quotation.status === "Rejected"; + const isAccepted = quotation.status === "Accepted"; + const isDisabled = isRejected || isAccepted; return ( <div className="w-16"> @@ -607,16 +618,19 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge variant="ghost" size="icon" onClick={() => { - router.push(`/ko/partners/techsales/rfq-ship/${quotation.id}`); + if (!isDisabled) { + router.push(`/ko/partners/techsales/rfq-ship/${quotation.id}`); + } }} className="h-8 w-8" + disabled={isDisabled} > <Edit className="h-4 w-4" /> <span className="sr-only">견적서 작성</span> </Button> </TooltipTrigger> <TooltipContent> - <p>{tooltipText}</p> + <p>{isRejected ? "거절된 견적서는 편집할 수 없습니다" : isAccepted ? "승인된 견적서는 편집할 수 없습니다" : tooltipText}</p> </TooltipContent> </Tooltip> </TooltipProvider> 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" /> |
