diff options
Diffstat (limited to 'lib/bidding/receive')
| -rw-r--r-- | lib/bidding/receive/biddings-receive-columns.tsx | 86 | ||||
| -rw-r--r-- | lib/bidding/receive/biddings-receive-table.tsx | 149 |
2 files changed, 173 insertions, 62 deletions
diff --git a/lib/bidding/receive/biddings-receive-columns.tsx b/lib/bidding/receive/biddings-receive-columns.tsx index 724a7396..d5798782 100644 --- a/lib/bidding/receive/biddings-receive-columns.tsx +++ b/lib/bidding/receive/biddings-receive-columns.tsx @@ -4,6 +4,7 @@ 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 { Checkbox } from "@/components/ui/checkbox"
import {
Eye, Calendar, Users, CheckCircle, XCircle, Clock, AlertTriangle
} from "lucide-react"
@@ -91,6 +92,25 @@ const formatCurrency = (amount: string | number | null, currency = 'KRW') => { export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): ColumnDef<BiddingReceiveItem>[] {
return [
+ // ░░░ 선택 ░░░
+ {
+ id: "select",
+ header: "",
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => {
+ // single select 모드에서는 다른 행들의 선택을 해제
+ row.toggleSelected(!!value)
+ }}
+ aria-label="행 선택"
+ />
+ ),
+ size: 50,
+ enableSorting: false,
+ enableHiding: false,
+ },
+
// ░░░ 입찰번호 ░░░
{
accessorKey: "biddingNumber",
@@ -110,7 +130,7 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰명" />,
cell: ({ row }) => (
<div className="truncate max-w-[200px]" title={row.original.title}>
- <Button
+ {/* <Button
variant="link"
className="p-0 h-auto text-left justify-start font-bold underline"
onClick={() => setRowAction({ row, type: "view" })}
@@ -118,7 +138,8 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co <div className="whitespace-pre-line">
{row.original.title}
</div>
- </Button>
+ </Button> */}
+ {row.original.title}
</div>
),
size: 200,
@@ -175,8 +196,8 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co 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)
+ const isActive = now >= startDate && now <= endDate
+ const isPast = now > endDate
return (
<div className="text-xs">
@@ -315,7 +336,7 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co accessorKey: "createdAt",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등록일시" />,
cell: ({ row }) => (
- <span className="text-sm">{formatDate(row.original.createdAt, "KR")}</span>
+ <span className="text-sm">{row.original.createdAt ? formatDate(row.original.createdAt, "KR") : '-'}</span>
),
size: 100,
meta: { excelHeader: "등록일시" },
@@ -324,37 +345,28 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co // ═══════════════════════════════════════════════════════════════
// 액션
// ═══════════════════════════════════════════════════════════════
- {
- id: "actions",
- header: "액션",
- cell: ({ row }) => (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" className="h-8 w-8 p-0">
- <span className="sr-only">메뉴 열기</span>
- <AlertTriangle className="h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={() => setRowAction({ row, type: "view" })}>
- <Eye className="mr-2 h-4 w-4" />
- 상세보기
- </DropdownMenuItem>
- {row.original.status === 'bidding_closed' && (
- <>
- <DropdownMenuSeparator />
- <DropdownMenuItem onClick={() => setRowAction({ row, type: "open_bidding" })}>
- <Calendar className="mr-2 h-4 w-4" />
- 개찰하기
- </DropdownMenuItem>
- </>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- ),
- size: 50,
- enableSorting: false,
- enableHiding: false,
- },
+ // {
+ // id: "actions",
+ // header: "액션",
+ // cell: ({ row }) => (
+ // <DropdownMenu>
+ // <DropdownMenuTrigger asChild>
+ // <Button variant="ghost" className="h-8 w-8 p-0">
+ // <span className="sr-only">메뉴 열기</span>
+ // <AlertTriangle className="h-4 w-4" />
+ // </Button>
+ // </DropdownMenuTrigger>
+ // <DropdownMenuContent align="end">
+ // <DropdownMenuItem onClick={() => setRowAction({ row, type: "view" })}>
+ // <Eye className="mr-2 h-4 w-4" />
+ // 상세보기
+ // </DropdownMenuItem>
+ // </DropdownMenuContent>
+ // </DropdownMenu>
+ // ),
+ // size: 50,
+ // enableSorting: false,
+ // enableHiding: false,
+ // },
]
}
diff --git a/lib/bidding/receive/biddings-receive-table.tsx b/lib/bidding/receive/biddings-receive-table.tsx index 88fade40..873f3fa4 100644 --- a/lib/bidding/receive/biddings-receive-table.tsx +++ b/lib/bidding/receive/biddings-receive-table.tsx @@ -8,6 +8,9 @@ import type { DataTableFilterField,
DataTableRowAction,
} from "@/types/table"
+import { Button } from "@/components/ui/button"
+import { Loader2 } from "lucide-react"
+import { toast } from "sonner"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
@@ -18,7 +21,8 @@ import { biddingStatusLabels,
contractTypeLabels,
} from "@/db/schema"
-import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs"
+// import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs"
+import { openBiddingAction, earlyOpenBiddingAction } from "@/lib/bidding/actions"
type BiddingReceiveItem = {
id: number
@@ -62,11 +66,13 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { const { data, pageCount } = biddingsResult
const [isCompact, setIsCompact] = React.useState<boolean>(false)
- const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false)
- const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false)
- const [selectedBidding, setSelectedBidding] = React.useState<BiddingReceiveItem | null>(null)
+ // const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false)
+ // const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false)
+ // const [selectedBidding, setSelectedBidding] = React.useState<BiddingReceiveItem | null>(null)
const [rowAction, setRowAction] = React.useState<DataTableRowAction<BiddingReceiveItem> | null>(null)
+ const [isOpeningBidding, setIsOpeningBidding] = React.useState(false)
+ const [isEarlyOpeningBidding, setIsEarlyOpeningBidding] = React.useState(false)
const router = useRouter()
const { data: session } = useSession()
@@ -86,10 +92,6 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { // 상세 페이지로 이동
router.push(`/evcp/bid/${rowAction.row.original.id}`)
break
- case "open_bidding":
- // 개찰하기 (추후 구현)
- console.log('개찰하기:', rowAction.row.original)
- break
default:
break
}
@@ -100,19 +102,16 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { {
id: "biddingNumber",
label: "입찰번호",
- type: "text",
placeholder: "입찰번호를 입력하세요",
},
{
id: "prNumber",
label: "P/R번호",
- type: "text",
placeholder: "P/R번호를 입력하세요",
},
{
id: "title",
label: "입찰명",
- type: "text",
placeholder: "입찰명을 입력하세요",
},
]
@@ -151,6 +150,7 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { filterFields,
enablePinning: true,
enableAdvancedFilter: true,
+ enableRowSelection: true,
initialState: {
sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
@@ -164,17 +164,96 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { setIsCompact(compact)
}, [])
- const handleSpecMeetingDialogClose = React.useCallback(() => {
- setSpecMeetingDialogOpen(false)
- setRowAction(null)
- setSelectedBidding(null)
- }, [])
+ // const handleSpecMeetingDialogClose = React.useCallback(() => {
+ // setSpecMeetingDialogOpen(false)
+ // setRowAction(null)
+ // setSelectedBidding(null)
+ // }, [])
- const handlePrDocumentsDialogClose = React.useCallback(() => {
- setPrDocumentsDialogOpen(false)
- setRowAction(null)
- setSelectedBidding(null)
- }, [])
+ // const handlePrDocumentsDialogClose = React.useCallback(() => {
+ // setPrDocumentsDialogOpen(false)
+ // setRowAction(null)
+ // setSelectedBidding(null)
+ // }, [])
+
+ // 선택된 행 가져오기
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ const selectedBiddingForAction = selectedRows.length > 0 ? selectedRows[0].original : null
+
+ // 조기개찰 가능 여부 확인
+ const canEarlyOpen = React.useMemo(() => {
+ if (!selectedBiddingForAction) return false
+
+ const now = new Date()
+ const submissionEndDate = selectedBiddingForAction.submissionEndDate
+
+ // 참여협력사가 1명 이상이어야 함
+ if (selectedBiddingForAction.participantParticipated < 1) return false
+
+ // 입찰서 제출기간 내여야 함
+ if (!submissionEndDate || now > submissionEndDate) return false
+
+ // 미제출 협력사가 0이어야 함
+ if (selectedBiddingForAction.participantPending > 0) return false
+
+ // 참여협력사 + 포기협력사 = 참여예정협력사 여야 함
+ const participatedOrDeclined = selectedBiddingForAction.participantParticipated + selectedBiddingForAction.participantDeclined
+ return participatedOrDeclined === selectedBiddingForAction.participantExpected
+ }, [selectedBiddingForAction])
+
+ // 개찰 가능 여부 확인
+ const canOpen = React.useMemo(() => {
+ if (!selectedBiddingForAction) return false
+
+ // 참여협력사가 1명 이상이어야 함
+ if (selectedBiddingForAction.participantParticipated < 1) return false
+
+ const now = new Date()
+ const submissionEndDate = selectedBiddingForAction.submissionEndDate
+
+ // 입찰서 제출기간이 종료되어야 함
+ return submissionEndDate && now > submissionEndDate
+ }, [selectedBiddingForAction])
+
+ const handleEarlyOpenBidding = React.useCallback(async () => {
+ if (!selectedBiddingForAction) return
+
+ setIsEarlyOpeningBidding(true)
+ try {
+ const result = await earlyOpenBiddingAction(selectedBiddingForAction.id)
+ if (result.success) {
+ toast.success("조기개찰이 완료되었습니다.")
+ // 데이터 리프레시
+ window.location.reload()
+ } else {
+ toast.error(result.message || "조기개찰에 실패했습니다.")
+ }
+ } catch (error) {
+ toast.error("조기개찰 중 오류가 발생했습니다.")
+ } finally {
+ setIsEarlyOpeningBidding(false)
+ }
+ }, [selectedBiddingForAction])
+
+ const handleOpenBidding = React.useCallback(async () => {
+ if (!selectedBiddingForAction) return
+
+ setIsOpeningBidding(true)
+ try {
+ const result = await openBiddingAction(selectedBiddingForAction.id)
+ if (result.success) {
+ toast.success("개찰이 완료되었습니다.")
+ // 데이터 리프레시
+ window.location.reload()
+ } else {
+ toast.error(result.message || "개찰에 실패했습니다.")
+ }
+ } catch (error) {
+ toast.error("개찰 중 오류가 발생했습니다.")
+ } finally {
+ setIsOpeningBidding(false)
+ }
+ }, [selectedBiddingForAction])
return (
<>
@@ -190,22 +269,42 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { compactStorageKey="biddingsReceiveTableCompact"
onCompactChange={handleCompactChange}
>
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleEarlyOpenBidding}
+ disabled={!selectedBiddingForAction || !canEarlyOpen || isEarlyOpeningBidding}
+ >
+ {isEarlyOpeningBidding && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 조기개찰
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleOpenBidding}
+ disabled={!selectedBiddingForAction || !canOpen || isOpeningBidding}
+ >
+ {isOpeningBidding && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 개찰
+ </Button>
+ </div>
</DataTableAdvancedToolbar>
</DataTable>
{/* 사양설명회 다이얼로그 */}
- <SpecificationMeetingDialog
+ {/* <SpecificationMeetingDialog
open={specMeetingDialogOpen}
onOpenChange={handleSpecMeetingDialogClose}
bidding={selectedBidding}
- />
+ /> */}
{/* PR 문서 다이얼로그 */}
- <PrDocumentsDialog
+ {/* <PrDocumentsDialog
open={prDocumentsDialogOpen}
onOpenChange={handlePrDocumentsDialogClose}
bidding={selectedBidding}
- />
+ /> */}
</>
)
}
|
