From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/failure/biddings-failure-columns.tsx | 320 +++++++++++++++++++++++ lib/bidding/failure/biddings-failure-table.tsx | 223 ++++++++++++++++ 2 files changed, 543 insertions(+) create mode 100644 lib/bidding/failure/biddings-failure-columns.tsx create mode 100644 lib/bidding/failure/biddings-failure-table.tsx (limited to 'lib/bidding/failure') diff --git a/lib/bidding/failure/biddings-failure-columns.tsx b/lib/bidding/failure/biddings-failure-columns.tsx new file mode 100644 index 00000000..8a888079 --- /dev/null +++ b/lib/bidding/failure/biddings-failure-columns.tsx @@ -0,0 +1,320 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Eye, Calendar, FileX, DollarSign, AlertTriangle, RefreshCw +} from "lucide-react" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { + biddingStatusLabels, + contractTypeLabels, +} from "@/db/schema" +import { formatDate } from "@/lib/utils" +import { DataTableRowAction } from "@/types/table" + +type BiddingFailureItem = { + id: number + biddingNumber: string + originalBiddingNumber: string | null + title: string + status: string + contractType: string + prNumber: string | null + + // 가격 정보 + targetPrice: number | null + currency: string | null + + // 일정 정보 + biddingRegistrationDate: Date | null + submissionStartDate: Date | null + submissionEndDate: Date | null + + // 담당자 정보 + bidPicName: string | null + supplyPicName: string | null + + // 유찰 정보 + disposalDate: Date | null // 유찰일 + disposalUpdatedAt: Date | null // 폐찰수정일 + disposalUpdatedBy: string | null // 폐찰수정자 + + // 기타 정보 + createdBy: string | null + createdAt: Date | null + updatedAt: Date | null + updatedBy: string | null +} + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +// 상태별 배지 색상 +const getStatusBadgeVariant = (status: string) => { + switch (status) { + case 'bidding_disposal': + return 'destructive' + default: + return 'outline' + } +} + +// 금액 포맷팅 +const formatCurrency = (amount: string | number | null, currency = 'KRW') => { + if (!amount) return '-' + + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount + if (isNaN(numAmount)) return '-' + + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: currency, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(numAmount) +} + +export function getBiddingsFailureColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + + return [ + // ░░░ 입찰번호 ░░░ + { + accessorKey: "biddingNumber", + header: ({ column }) => , + cell: ({ row }) => ( +
+ {row.original.biddingNumber} +
+ ), + size: 120, + meta: { excelHeader: "입찰번호" }, + }, + + // ░░░ 입찰명 ░░░ + { + accessorKey: "title", + header: ({ column }) => , + cell: ({ row }) => ( +
+ +
+ ), + size: 200, + meta: { excelHeader: "입찰명" }, + }, + + // ░░░ 진행상태 ░░░ + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => ( + + {biddingStatusLabels[row.original.status]} + + ), + size: 120, + meta: { excelHeader: "진행상태" }, + }, + + // ░░░ 계약구분 ░░░ + { + accessorKey: "contractType", + header: ({ column }) => , + cell: ({ row }) => ( + + {contractTypeLabels[row.original.contractType]} + + ), + size: 100, + meta: { excelHeader: "계약구분" }, + }, + + // ░░░ 내정가 ░░░ + { + accessorKey: "targetPrice", + header: ({ column }) => , + cell: ({ row }) => { + const price = row.original.targetPrice + const currency = row.original.currency || 'KRW' + + return ( +
+ {price ? formatCurrency(price, currency) : '-'} +
+ ) + }, + size: 120, + meta: { excelHeader: "내정가" }, + }, + + // ░░░ 통화 ░░░ + { + accessorKey: "currency", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.currency || 'KRW'} + ), + size: 60, + meta: { excelHeader: "통화" }, + }, + + // ░░░ 입찰등록일 ░░░ + { + accessorKey: "biddingRegistrationDate", + header: ({ column }) => , + cell: ({ row }) => ( + {formatDate(row.original.biddingRegistrationDate, "KR")} + ), + size: 100, + meta: { excelHeader: "입찰등록일" }, + }, + + // ░░░ 입찰담당자 ░░░ + { + accessorKey: "bidPicName", + header: ({ column }) => , + cell: ({ row }) => { + const bidPic = row.original.bidPicName + const supplyPic = row.original.supplyPicName + + const displayName = bidPic || supplyPic || "-" + return {displayName} + }, + size: 100, + meta: { excelHeader: "입찰담당자" }, + }, + + // ░░░ 유찰일 ░░░ + { + id: "disposalDate", + header: ({ column }) => , + cell: ({ row }) => ( +
+ + {formatDate(row.original.disposalDate, "KR")} +
+ ), + size: 100, + meta: { excelHeader: "유찰일" }, + }, + + // ░░░ 폐찰일 ░░░ + { + id: "disposalUpdatedAt", + header: ({ column }) => , + cell: ({ row }) => ( +
+ + {formatDate(row.original.disposalUpdatedAt, "KR")} +
+ ), + size: 100, + meta: { excelHeader: "폐찰일" }, + }, + + // ░░░ 폐찰수정자 ░░░ + { + id: "disposalUpdatedBy", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.disposalUpdatedBy || '-'} + ), + size: 100, + meta: { excelHeader: "폐찰수정자" }, + }, + + // ░░░ P/R번호 ░░░ + { + accessorKey: "prNumber", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.prNumber || '-'} + ), + size: 100, + meta: { excelHeader: "P/R번호" }, + }, + + // ░░░ 등록자 ░░░ + { + accessorKey: "createdBy", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.createdBy || '-'} + ), + size: 100, + meta: { excelHeader: "등록자" }, + }, + + // ░░░ 등록일시 ░░░ + { + accessorKey: "createdAt", + header: ({ column }) => , + cell: ({ row }) => ( + {formatDate(row.original.createdAt, "KR")} + ), + size: 100, + meta: { excelHeader: "등록일시" }, + }, + + // ═══════════════════════════════════════════════════════════════ + // 액션 + // ═══════════════════════════════════════════════════════════════ + { + id: "actions", + header: "액션", + cell: ({ row }) => ( + + + + + + setRowAction({ row, type: "view" })}> + + 상세보기 + + setRowAction({ row, type: "history" })}> + + 이력보기 + + + setRowAction({ row, type: "rebid" })}> + + 재입찰 + + + + ), + size: 50, + enableSorting: false, + enableHiding: false, + }, + ] +} diff --git a/lib/bidding/failure/biddings-failure-table.tsx b/lib/bidding/failure/biddings-failure-table.tsx new file mode 100644 index 00000000..901648d2 --- /dev/null +++ b/lib/bidding/failure/biddings-failure-table.tsx @@ -0,0 +1,223 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useSession } from "next-auth/react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { getBiddingsFailureColumns } from "./biddings-failure-columns" +import { getBiddingsForFailure } from "@/lib/bidding/service" +import { + biddingStatusLabels, + contractTypeLabels, +} from "@/db/schema" +import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs" + +type BiddingFailureItem = { + id: number + biddingNumber: string + originalBiddingNumber: string | null + title: string + status: string + contractType: string + prNumber: string | null + + // 가격 정보 + targetPrice: number | null + currency: string | null + + // 일정 정보 + biddingRegistrationDate: Date | null + submissionStartDate: Date | null + submissionEndDate: Date | null + + // 담당자 정보 + bidPicName: string | null + supplyPicName: string | null + + // 유찰 정보 + disposalDate: Date | null // 유찰일 + disposalUpdatedAt: Date | null // 폐찰수정일 + disposalUpdatedBy: string | null // 폐찰수정자 + + // 기타 정보 + createdBy: string | null + createdAt: Date | null + updatedAt: Date | null + updatedBy: string | null +} + +interface BiddingsFailureTableProps { + promises: Promise< + [ + Awaited> + ] + > +} + +export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) { + const [biddingsResult] = React.use(promises) + + // biddingsResult에서 data와 pageCount 추출 + const { data, pageCount } = biddingsResult + + const [isCompact, setIsCompact] = React.useState(false) + const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false) + const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false) + const [selectedBidding, setSelectedBidding] = React.useState(null) + + const [rowAction, setRowAction] = React.useState | null>(null) + + const router = useRouter() + const { data: session } = useSession() + + const columns = React.useMemo( + () => getBiddingsFailureColumns({ setRowAction }), + [setRowAction] + ) + + // rowAction 변경 감지하여 해당 다이얼로그 열기 + React.useEffect(() => { + if (rowAction) { + setSelectedBidding(rowAction.row.original) + + switch (rowAction.type) { + case "view": + // 상세 페이지로 이동 + router.push(`/evcp/bid/${rowAction.row.original.id}`) + break + case "history": + // 이력보기 (추후 구현) + console.log('이력보기:', rowAction.row.original) + break + case "rebid": + // 재입찰 (추후 구현) + console.log('재입찰:', rowAction.row.original) + break + default: + break + } + } + }, [rowAction]) + + const filterFields: DataTableFilterField[] = [ + { + id: "biddingNumber", + label: "입찰번호", + type: "text", + placeholder: "입찰번호를 입력하세요", + }, + { + id: "prNumber", + label: "P/R번호", + type: "text", + placeholder: "P/R번호를 입력하세요", + }, + { + id: "title", + label: "입찰명", + type: "text", + placeholder: "입찰명을 입력하세요", + }, + ] + + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "title", label: "입찰명", type: "text" }, + { id: "biddingNumber", label: "입찰번호", type: "text" }, + { id: "bidPicName", label: "입찰담당자", type: "text" }, + { + id: "status", + label: "진행상태", + type: "multi-select", + options: Object.entries(biddingStatusLabels).map(([value, label]) => ({ + label, + value, + })), + }, + { + id: "contractType", + label: "계약구분", + type: "select", + options: Object.entries(contractTypeLabels).map(([value, label]) => ({ + label, + value, + })), + }, + { id: "createdAt", label: "등록일", type: "date" }, + { id: "biddingRegistrationDate", label: "입찰등록일", type: "date" }, + { id: "disposalDate", label: "유찰일", type: "date" }, + { id: "disposalUpdatedAt", label: "폐찰일", type: "date" }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "disposalDate", desc: true }], // 유찰일 기준 최신순 + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + const handleCompactChange = React.useCallback((compact: boolean) => { + setIsCompact(compact) + }, []) + + const handleSpecMeetingDialogClose = React.useCallback(() => { + setSpecMeetingDialogOpen(false) + setRowAction(null) + setSelectedBidding(null) + }, []) + + const handlePrDocumentsDialogClose = React.useCallback(() => { + setPrDocumentsDialogOpen(false) + setRowAction(null) + setSelectedBidding(null) + }, []) + + return ( + <> + + + + + + {/* 사양설명회 다이얼로그 */} + + + {/* PR 문서 다이얼로그 */} + + + ) +} -- cgit v1.2.3