diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
| commit | a5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch) | |
| tree | 667ed8c5d6ec35b109190e9f976d66ae54def4ce /lib/bidding/list/biddings-table-columns.tsx | |
| parent | b0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff) | |
| parent | f8a38907911d940cb2e8e6c9aa49488d05b2b578 (diff) | |
Merge remote-tracking branch 'origin/dujinkim' into master_homemaster
Diffstat (limited to 'lib/bidding/list/biddings-table-columns.tsx')
| -rw-r--r-- | lib/bidding/list/biddings-table-columns.tsx | 649 |
1 files changed, 224 insertions, 425 deletions
diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index d6044e93..10966e0e 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -5,18 +5,10 @@ import { type ColumnDef } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { getUserCodeByEmail } from "@/lib/bidding/service" import { - Eye, Edit, MoreHorizontal, FileText, Users, Calendar, - Building, Package, DollarSign, Clock, CheckCircle, XCircle, - AlertTriangle + Eye, Edit, MoreHorizontal, FileX } from "lucide-react" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" + import { DropdownMenu, DropdownMenuContent, @@ -30,14 +22,13 @@ import { DataTableRowAction } from "@/types/table" // BiddingListItem에 manager 정보 추가 type BiddingListItemWithManagerCode = BiddingListItem & { - managerName?: string | null - managerCode?: string | null + bidPicName?: string | null + supplyPicName?: string | null } import { biddingStatusLabels, contractTypeLabels, biddingTypeLabels, - awardCountLabels } from "@/db/schema" import { formatDate } from "@/lib/utils" @@ -68,23 +59,6 @@ const getStatusBadgeVariant = (status: string) => { } } -// 금액 포맷팅 -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 getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef<BiddingListItemWithManagerCode>[] { return [ @@ -132,442 +106,256 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef meta: { excelHeader: "입찰 No." }, }, + // ░░░ 원입찰번호 ░░░ + { + accessorKey: "originalBiddingNumber", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="원입찰번호" />, + cell: ({ row }) => ( + <div className="font-mono text-sm"> + {row.original.originalBiddingNumber || '-'} + </div> + ), + size: 120, + meta: { excelHeader: "원입찰번호" }, + }, + // ░░░ 프로젝트명 ░░░ + { + accessorKey: "projectName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="프로젝트명" />, + cell: ({ row }) => ( + <div className="truncate max-w-[150px]" title={row.original.projectName || ''}> + {row.original.projectName || '-'} + </div> + ), + size: 150, + meta: { excelHeader: "프로젝트명" }, + }, + // ░░░ 입찰명 ░░░ + { + accessorKey: "title", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰명" />, + cell: ({ row }) => ( + <div className="truncate max-w-[200px]" title={row.original.title}> + <Button + variant="link" + className="p-0 h-auto text-left justify-start font-bold underline" + onClick={() => setRowAction({ row, type: "view" })} + > + <div className="whitespace-pre-line"> + {row.original.title} + </div> + </Button> + </div> + ), + size: 200, + meta: { excelHeader: "입찰명" }, + }, + // ░░░ 계약구분 ░░░ + { + accessorKey: "contractType", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약구분" />, + cell: ({ row }) => ( + <Badge variant="outline"> + {contractTypeLabels[row.original.contractType]} + </Badge> + ), + size: 100, + meta: { excelHeader: "계약구분" }, + }, // ░░░ 입찰상태 ░░░ { accessorKey: "status", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰상태" />, + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="진행상태" />, cell: ({ row }) => ( <Badge variant={getStatusBadgeVariant(row.original.status)}> {biddingStatusLabels[row.original.status]} </Badge> ), size: 120, - meta: { excelHeader: "입찰상태" }, + meta: { excelHeader: "진행상태" }, }, - - // ░░░ 긴급여부 ░░░ + // ░░░ 입찰유형 ░░░ { - accessorKey: "isUrgent", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="긴급여부" />, - cell: ({ row }) => { - const isUrgent = row.original.isUrgent + accessorKey: "biddingType", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰유형" />, + cell: ({ row }) => ( + <Badge variant="secondary"> + {biddingTypeLabels[row.original.biddingType]} + </Badge> + ), + size: 100, + meta: { excelHeader: "입찰유형" }, + }, - return isUrgent ? ( - <div className="flex items-center gap-1"> - <AlertTriangle className="h-4 w-4 text-red-600" /> - <Badge variant="destructive" className="text-xs"> - 긴급 - </Badge> - </div> - ) : ( - <div className="flex items-center gap-1"> - <CheckCircle className="h-4 w-4 text-green-600" /> - <span className="text-xs text-muted-foreground">일반</span> - </div> - ) - }, - size: 90, - meta: { excelHeader: "긴급여부" }, + // ░░░ 통화 ░░░ + { + accessorKey: "currency", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="통화" />, + cell: ({ row }) => ( + <span className="font-mono text-sm">{row.original.currency}</span> + ), + size: 60, + meta: { excelHeader: "통화" }, }, - // ░░░ 사전견적 ░░░ + // ░░░ 예산 ░░░ { - id: "preQuote", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="사전견적" />, - cell: ({ row }) => { - const hasPreQuote = ['request_for_quotation', 'received_quotation'].includes(row.original.status) - const preQuoteDate = row.original.preQuoteDate - - return hasPreQuote ? ( - <div className="flex items-center gap-1"> - <CheckCircle className="h-4 w-4 text-green-600" /> - {preQuoteDate && ( - <span className="text-xs text-muted-foreground"> - {formatDate(preQuoteDate, "KR")} - </span> - )} - </div> - ) : ( - <XCircle className="h-4 w-4 text-gray-400" /> - ) - }, - size: 90, - meta: { excelHeader: "사전견적" }, + accessorKey: "budget", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="예산" />, + cell: ({ row }) => ( + <span className="text-sm font-medium"> + {row.original.budget} + </span> + ), + size: 120, + meta: { excelHeader: "예산" }, }, + // ░░░ 내정가 ░░░ + { + accessorKey: "targetPrice", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내정가" />, + cell: ({ row }) => ( + <span className="text-sm font-medium text-orange-600"> + {row.original.targetPrice} + </span> + ), + size: 120, + meta: { excelHeader: "내정가" }, + }, // ░░░ 입찰담당자 ░░░ { - accessorKey: "managerName", + accessorKey: "bidPicName", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰담당자" />, cell: ({ row }) => { - const name = row.original.managerName || "-"; - const managerCode = row.original.managerCode || ""; - return name === "-" ? "-" : `${name}(${managerCode})`; + const name = row.original.bidPicName || "-"; + return name; }, size: 100, meta: { excelHeader: "입찰담당자" }, }, - - // ═══════════════════════════════════════════════════════════════ - // 프로젝트 정보 - // ═══════════════════════════════════════════════════════════════ + + // ░░░ 입찰등록일 ░░░ { - header: "프로젝트 정보", - columns: [ - { - accessorKey: "projectName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="프로젝트명" />, - cell: ({ row }) => ( - <div className="truncate max-w-[150px]" title={row.original.projectName || ''}> - {row.original.projectName || '-'} - </div> - ), - size: 150, - meta: { excelHeader: "프로젝트명" }, - }, - - { - accessorKey: "itemName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="품목명" />, - cell: ({ row }) => ( - <div className="truncate max-w-[150px]" title={row.original.itemName || ''}> - {row.original.itemName || '-'} - </div> - ), - size: 150, - meta: { excelHeader: "품목명" }, - }, - - { - accessorKey: "title", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰명" />, - cell: ({ row }) => ( - <div className="truncate max-w-[200px]" title={row.original.title}> - <Button - variant="link" - className="p-0 h-auto text-left justify-start font-bold underline" - onClick={() => setRowAction({ row, type: "view" })} - > - <div className="whitespace-pre-line"> - {row.original.title} - </div> - </Button> - </div> - ), - size: 200, - meta: { excelHeader: "입찰명" }, - }, - ] + accessorKey: "biddingRegistrationDate", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰등록일" />, + cell: ({ row }) => ( + <span className="text-sm">{formatDate(row.original.biddingRegistrationDate , "KR")}</span> + ), + size: 100, + meta: { excelHeader: "입찰등록일" }, }, - // ═══════════════════════════════════════════════════════════════ - // 계약 정보 - // ═══════════════════════════════════════════════════════════════ { - header: "계약 정보", - columns: [ - { - accessorKey: "contractType", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약구분" />, - cell: ({ row }) => ( - <Badge variant="outline"> - {contractTypeLabels[row.original.contractType]} - </Badge> - ), - size: 100, - meta: { excelHeader: "계약구분" }, - }, - - { - accessorKey: "biddingType", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰유형" />, - cell: ({ row }) => ( - <Badge variant="secondary"> - {biddingTypeLabels[row.original.biddingType]} - </Badge> - ), - size: 100, - meta: { excelHeader: "입찰유형" }, - }, - - { - accessorKey: "awardCount", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="낙찰수" />, - cell: ({ row }) => ( - <Badge variant="outline"> - {awardCountLabels[row.original.awardCount]} - </Badge> - ), - size: 80, - meta: { excelHeader: "낙찰수" }, - }, - - { - id: "contractPeriod", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약기간" />, - cell: ({ row }) => { - const startDate = row.original.contractStartDate - const endDate = row.original.contractEndDate - - if (!startDate || !endDate) { - return <span className="text-muted-foreground">-</span> - } - - return ( - <div className="text-xs max-w-[120px] truncate" title={`${formatDate(startDate, "KR")} ~ ${formatDate(endDate, "KR")}`}> - {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")} - </div> - ) - }, - size: 120, - meta: { excelHeader: "계약기간" }, - }, - ] + id: "submissionPeriod", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰서제출기간" />, + cell: ({ row }) => { + const startDate = row.original.submissionStartDate + const endDate = row.original.submissionEndDate + + if (!startDate || !endDate) return <span className="text-muted-foreground">-</span> + + const now = new Date() + const isActive = now >= new Date(startDate) && now <= new Date(endDate) + const isPast = now > new Date(endDate) + + return ( + <div className="text-xs"> + <div className={`${isActive ? 'text-green-600 font-medium' : isPast ? 'text-red-600' : 'text-gray-600'}`}> + {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")} + </div> + {isActive && ( + <Badge variant="default" className="text-xs mt-1">진행중</Badge> + )} + </div> + ) + }, + size: 140, + meta: { excelHeader: "입찰서제출기간" }, }, - - // ═══════════════════════════════════════════════════════════════ - // 일정 정보 - // ═══════════════════════════════════════════════════════════════ + // ░░░ 사양설명회 ░░░ { - header: "일정 정보", - columns: [ - { - id: "submissionPeriod", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰서제출기간" />, - cell: ({ row }) => { - const startDate = row.original.submissionStartDate - const endDate = row.original.submissionEndDate - - if (!startDate || !endDate) return <span className="text-muted-foreground">-</span> - - const now = new Date() - const isActive = now >= new Date(startDate) && now <= new Date(endDate) - const isPast = now > new Date(endDate) - - return ( - <div className="text-xs"> - <div className={`${isActive ? 'text-green-600 font-medium' : isPast ? 'text-red-600' : 'text-gray-600'}`}> - {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")} - </div> - {isActive && ( - <Badge variant="default" className="text-xs mt-1">진행중</Badge> - )} - </div> - ) - }, - size: 140, - meta: { excelHeader: "입찰서제출기간" }, - }, - - { - accessorKey: "hasSpecificationMeeting", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="사양설명회" />, - cell: ({ row }) => { - const hasMeeting = row.original.hasSpecificationMeeting - - return ( - <Button - variant="ghost" - size="sm" - className={`p-1 h-auto ${hasMeeting ? 'text-blue-600' : 'text-gray-400'}`} - onClick={() => hasMeeting && setRowAction({ row, type: "specification_meeting" })} - disabled={!hasMeeting} - > - {hasMeeting ? 'Yes' : 'No'} - </Button> - ) - }, - size: 100, - meta: { excelHeader: "사양설명회" }, - }, - ] + accessorKey: "hasSpecificationMeeting", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="사양설명회" />, + cell: ({ row }) => { + const hasMeeting = row.original.hasSpecificationMeeting + + return ( + <Button + variant="ghost" + size="sm" + className={`p-1 h-auto ${hasMeeting ? 'text-blue-600' : 'text-gray-400'}`} + onClick={() => hasMeeting && setRowAction({ row, type: "specification_meeting" })} + disabled={!hasMeeting} + > + {hasMeeting ? 'Yes' : 'No'} + </Button> + ) + }, + size: 100, + meta: { excelHeader: "사양설명회" }, }, - // ═══════════════════════════════════════════════════════════════ - // 가격 정보 - // ═══════════════════════════════════════════════════════════════ - { - header: "가격 정보", - columns: [ - { - accessorKey: "currency", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="통화" />, - cell: ({ row }) => ( - <span className="font-mono text-sm">{row.original.currency}</span> - ), - size: 60, - meta: { excelHeader: "통화" }, - }, + // ░░░ 등록자 ░░░ - { - accessorKey: "budget", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="예산" />, - cell: ({ row }) => ( - <span className="text-sm font-medium"> - {row.original.budget} - </span> - ), - size: 120, - meta: { excelHeader: "예산" }, - }, - - { - accessorKey: "targetPrice", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내정가" />, - cell: ({ row }) => ( - <span className="text-sm font-medium text-orange-600"> - {row.original.targetPrice} - </span> - ), - size: 120, - meta: { excelHeader: "내정가" }, - }, - - { - accessorKey: "finalBidPrice", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종입찰가" />, - cell: ({ row }) => ( - <span className="text-sm font-medium text-green-600"> - {row.original.finalBidPrice} - </span> - ), - size: 120, - meta: { excelHeader: "최종입찰가" }, - }, - ] + { + accessorKey: "updatedBy", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록자" />, + cell: ({ row }) => ( + <span className="text-sm">{row.original.updatedBy || '-'}</span> + ), + size: 100, + meta: { excelHeader: "등록자" }, }, - - // ═══════════════════════════════════════════════════════════════ - // 참여 현황 - // ═══════════════════════════════════════════════════════════════ + // 등록일시 { - header: "참여 현황", - columns: [ - { - id: "participantExpected", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여예정" />, - cell: ({ row }) => ( - <Badge variant="outline" className="font-mono"> - {row.original.participantExpected} - </Badge> - ), - size: 80, - meta: { excelHeader: "참여예정" }, - }, - - { - id: "participantParticipated", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="참여" />, - cell: ({ row }) => ( - <Badge variant="default" className="font-mono"> - {row.original.participantParticipated} - </Badge> - ), - size: 60, - meta: { excelHeader: "참여" }, - }, - - { - id: "participantDeclined", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="포기" />, - cell: ({ row }) => ( - <Badge variant="destructive" className="font-mono"> - {row.original.participantDeclined} - </Badge> - ), - size: 60, - meta: { excelHeader: "포기" }, - }, - ] + accessorKey: "updatedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록일시" />, + cell: ({ row }) => ( + <span className="text-sm">{formatDate(row.original.updatedAt, "KR")}</span> + ), + size: 100, + meta: { excelHeader: "등록일시" }, }, - // ═══════════════════════════════════════════════════════════════ // PR 정보 // ═══════════════════════════════════════════════════════════════ - { - header: "PR 정보", - columns: [ - { - accessorKey: "prNumber", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PR No." />, - cell: ({ row }) => ( - <span className="font-mono text-sm">{row.original.prNumber || '-'}</span> - ), - size: 100, - meta: { excelHeader: "PR No." }, - }, - - { - accessorKey: "hasPrDocument", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PR 문서" />, - cell: ({ row }) => { - const hasPrDoc = row.original.hasPrDocument + // { + // header: "PR 정보", + // columns: [ + // { + // accessorKey: "prNumber", + // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PR No." />, + // cell: ({ row }) => ( + // <span className="font-mono text-sm">{row.original.prNumber || '-'}</span> + // ), + // size: 100, + // meta: { excelHeader: "PR No." }, + // }, + + // { + // accessorKey: "hasPrDocument", + // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PR 문서" />, + // cell: ({ row }) => { + // const hasPrDoc = row.original.hasPrDocument - return ( - <Button - variant="ghost" - size="sm" - className={`p-1 h-auto ${hasPrDoc ? 'text-blue-600' : 'text-gray-400'}`} - onClick={() => hasPrDoc && setRowAction({ row, type: "pr_documents" })} - disabled={!hasPrDoc} - > - {hasPrDoc ? 'Yes' : 'No'} - </Button> - ) - }, - size: 80, - meta: { excelHeader: "PR 문서" }, - }, - ] - }, - - // ═══════════════════════════════════════════════════════════════ - // 메타 정보 - // ═══════════════════════════════════════════════════════════════ - { - header: "메타 정보", - columns: [ - { - accessorKey: "preQuoteDate", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="사전견적일" />, - cell: ({ row }) => ( - <span className="text-sm">{formatDate(row.original.preQuoteDate, "KR")}</span> - ), - size: 90, - meta: { excelHeader: "사전견적일" }, - }, - - { - accessorKey: "biddingRegistrationDate", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰등록일" />, - cell: ({ row }) => ( - <span className="text-sm">{formatDate(row.original.biddingRegistrationDate , "KR")}</span> - ), - size: 100, - meta: { excelHeader: "입찰등록일" }, - }, - - { - accessorKey: "updatedAt", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종수정일" />, - cell: ({ row }) => ( - <span className="text-sm">{formatDate(row.original.updatedAt, "KR")}</span> - ), - size: 100, - meta: { excelHeader: "최종수정일" }, - }, - - { - accessorKey: "updatedBy", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종수정자" />, - cell: ({ row }) => ( - <span className="text-sm">{row.original.updatedBy || '-'}</span> - ), - size: 100, - meta: { excelHeader: "최종수정자" }, - }, - ] - }, + // return ( + // <Button + // variant="ghost" + // size="sm" + // className={`p-1 h-auto ${hasPrDoc ? 'text-blue-600' : 'text-gray-400'}`} + // onClick={() => hasPrDoc && setRowAction({ row, type: "pr_documents" })} + // disabled={!hasPrDoc} + // > + // {hasPrDoc ? 'Yes' : 'No'} + // </Button> + // ) + // }, + // size: 80, + // meta: { excelHeader: "PR 문서" }, + // }, + // ] + // }, // ░░░ 비고 ░░░ { @@ -611,6 +399,17 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef <span className="text-xs text-muted-foreground ml-2">(수정 불가)</span> )} </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "bid_closure" })} + disabled={row.original.status !== 'bidding_disposal'} + > + <FileX className="mr-2 h-4 w-4" /> + 폐찰하기 + {row.original.status !== 'bidding_disposal' && ( + <span className="text-xs text-muted-foreground ml-2">(유찰 시에만 가능)</span> + )} + </DropdownMenuItem> {/* <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => setRowAction({ row, type: "copy" })}> <Package className="mr-2 h-4 w-4" /> |
