summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/vendor-response
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/vendor-response')
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx88
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx115
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" />