summaryrefslogtreecommitdiff
path: root/lib/bidding/failure/biddings-failure-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/failure/biddings-failure-table.tsx')
-rw-r--r--lib/bidding/failure/biddings-failure-table.tsx266
1 files changed, 233 insertions, 33 deletions
diff --git a/lib/bidding/failure/biddings-failure-table.tsx b/lib/bidding/failure/biddings-failure-table.tsx
index 901648d2..c80021ea 100644
--- a/lib/bidding/failure/biddings-failure-table.tsx
+++ b/lib/bidding/failure/biddings-failure-table.tsx
@@ -18,7 +18,12 @@ import {
biddingStatusLabels,
contractTypeLabels,
} from "@/db/schema"
-import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs"
+import { BiddingsClosureDialog } from "./biddings-closure-dialog"
+import { Button } from "@/components/ui/button"
+import { FileX, RefreshCw, Undo2 } from "lucide-react"
+import { bidClosureAction, cancelDisposalAction } from "@/lib/bidding/actions"
+import { increaseRoundOrRebid } from "@/lib/bidding/service"
+import { useToast } from "@/hooks/use-toast"
type BiddingFailureItem = {
id: number
@@ -30,7 +35,7 @@ type BiddingFailureItem = {
prNumber: string | null
// 가격 정보
- targetPrice: number | null
+ targetPrice: string | number | null
currency: string | null
// 일정 정보
@@ -47,6 +52,15 @@ type BiddingFailureItem = {
disposalUpdatedAt: Date | null // 폐찰수정일
disposalUpdatedBy: string | null // 폐찰수정자
+ // 폐찰 정보
+ closureReason: string | null // 폐찰사유
+ closureDocuments: {
+ id: number
+ fileName: string
+ originalFileName: string
+ filePath: string
+ }[] // 폐찰 첨부파일들
+
// 기타 정보
createdBy: string | null
createdAt: Date | null
@@ -69,9 +83,9 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
const { data, pageCount } = biddingsResult
const [isCompact, setIsCompact] = React.useState<boolean>(false)
- const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false)
- const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false)
+ const [biddingClosureDialogOpen, setBiddingClosureDialogOpen] = React.useState(false)
const [selectedBidding, setSelectedBidding] = React.useState<BiddingFailureItem | null>(null)
+ const { toast } = useToast()
const [rowAction, setRowAction] = React.useState<DataTableRowAction<BiddingFailureItem> | null>(null)
@@ -89,17 +103,18 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
setSelectedBidding(rowAction.row.original)
switch (rowAction.type) {
- case "view":
- // 상세 페이지로 이동
- router.push(`/evcp/bid/${rowAction.row.original.id}`)
+ case "rebid":
+ // 재입찰
+ handleRebid(rowAction.row.original)
break
- case "history":
- // 이력보기 (추후 구현)
- console.log('이력보기:', rowAction.row.original)
+ case "closure":
+ // 폐찰
+ setSelectedBidding(rowAction.row.original)
+ setBiddingClosureDialogOpen(true)
break
- case "rebid":
- // 재입찰 (추후 구현)
- console.log('재입찰:', rowAction.row.original)
+ case "cancelDisposal":
+ // 유찰취소
+ handleCancelDisposal(rowAction.row.original)
break
default:
break
@@ -163,6 +178,8 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
filterFields,
enablePinning: true,
enableAdvancedFilter: true,
+ enableRowSelection: true,
+ singleRowSelection: true,
initialState: {
sorting: [{ id: "disposalDate", desc: true }], // 유찰일 기준 최신순
columnPinning: { right: ["actions"] },
@@ -176,17 +193,85 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
setIsCompact(compact)
}, [])
- const handleSpecMeetingDialogClose = React.useCallback(() => {
- setSpecMeetingDialogOpen(false)
+ const handleBiddingClosureDialogClose = React.useCallback(() => {
+ setBiddingClosureDialogOpen(false)
setRowAction(null)
setSelectedBidding(null)
}, [])
- const handlePrDocumentsDialogClose = React.useCallback(() => {
- setPrDocumentsDialogOpen(false)
- setRowAction(null)
- setSelectedBidding(null)
- }, [])
+ const handleRebid = React.useCallback(async (bidding: BiddingFailureItem) => {
+ if (!session?.user?.id) {
+ toast({
+ title: "오류",
+ description: "사용자 정보를 찾을 수 없습니다.",
+ variant: "destructive",
+ })
+ return
+ }
+
+ try {
+ const result = await increaseRoundOrRebid(bidding.id, session.user.id, 'rebidding')
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: result.message,
+ })
+ // 페이지 새로고침
+ router.refresh()
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "재입찰 중 오류가 발생했습니다.",
+ variant: "destructive",
+ })
+ }
+ } catch (error) {
+ console.error('재입찰 실패:', error)
+ toast({
+ title: "오류",
+ description: "재입찰 중 오류가 발생했습니다.",
+ variant: "destructive",
+ })
+ }
+ }, [session?.user?.id, toast, router])
+
+ const handleCancelDisposal = React.useCallback(async (bidding: BiddingFailureItem) => {
+ if (!session?.user?.id) {
+ toast({
+ title: "오류",
+ description: "사용자 정보를 찾을 수 없습니다.",
+ variant: "destructive",
+ })
+ return
+ }
+
+ try {
+ const result = await cancelDisposalAction(bidding.id, session.user.id)
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: result.message,
+ })
+ // 페이지 새로고침
+ router.refresh()
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "유찰취소 중 오류가 발생했습니다.",
+ variant: "destructive",
+ })
+ }
+ } catch (error) {
+ console.error('유찰취소 실패:', error)
+ toast({
+ title: "오류",
+ description: "유찰취소 중 오류가 발생했습니다.",
+ variant: "destructive",
+ })
+ }
+ }, [session?.user?.id, toast, router])
return (
<>
@@ -202,22 +287,137 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
compactStorageKey="biddingsFailureTableCompact"
onCompactChange={handleCompactChange}
>
+ {/* Toolbar 액션 버튼들 */}
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ if (selectedRows.length === 0) {
+ toast({
+ title: "선택 필요",
+ description: "폐찰할 입찰을 선택해주세요.",
+ variant: "destructive",
+ })
+ return
+ }
+ if (selectedRows.length > 1) {
+ toast({
+ title: "하나만 선택",
+ description: "폐찰은 한 개의 입찰만 선택해주세요.",
+ variant: "destructive",
+ })
+ return
+ }
+ const bidding = selectedRows[0].original
+ if (bidding.status !== 'bidding_disposal') {
+ toast({
+ title: "유찰 상태만 가능",
+ description: "유찰 상태인 입찰만 폐찰할 수 있습니다.",
+ variant: "destructive",
+ })
+ return
+ }
+ setSelectedBidding(bidding)
+ setBiddingClosureDialogOpen(true)
+ }}
+ disabled={table.getFilteredSelectedRowModel().rows.length !== 1 ||
+ (table.getFilteredSelectedRowModel().rows.length === 1 &&
+ table.getFilteredSelectedRowModel().rows[0].original.status === 'bid_closure')}
+ >
+ <FileX className="mr-2 h-4 w-4" />
+ 폐찰
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ if (selectedRows.length === 0) {
+ toast({
+ title: "선택 필요",
+ description: "재입찰할 입찰을 선택해주세요.",
+ variant: "destructive",
+ })
+ return
+ }
+ if (selectedRows.length > 1) {
+ toast({
+ title: "하나만 선택",
+ description: "재입찰은 한 개의 입찰만 선택해주세요.",
+ variant: "destructive",
+ })
+ return
+ }
+ const bidding = selectedRows[0].original
+ handleRebid(bidding)
+ }}
+ disabled={table.getFilteredSelectedRowModel().rows.length !== 1 ||
+ (table.getFilteredSelectedRowModel().rows.length === 1 &&
+ table.getFilteredSelectedRowModel().rows[0].original.status === 'bid_closure')}
+ >
+ <RefreshCw className="mr-2 h-4 w-4" />
+ 재입찰
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ if (selectedRows.length === 0) {
+ toast({
+ title: "선택 필요",
+ description: "유찰취소할 입찰을 선택해주세요.",
+ variant: "destructive",
+ })
+ return
+ }
+ if (selectedRows.length > 1) {
+ toast({
+ title: "하나만 선택",
+ description: "유찰취소는 한 개의 입찰만 선택해주세요.",
+ variant: "destructive",
+ })
+ return
+ }
+ const bidding = selectedRows[0].original
+ if (bidding.status !== 'bidding_disposal' && bidding.status !== 'bid_closure') {
+ toast({
+ title: "유찰/폐찰 상태만 가능",
+ description: "유찰 또는 폐찰 상태인 입찰만 취소할 수 있습니다.",
+ variant: "destructive",
+ })
+ return
+ }
+ handleCancelDisposal(bidding)
+ }}
+ disabled={table.getFilteredSelectedRowModel().rows.length !== 1 ||
+ (table.getFilteredSelectedRowModel().rows.length === 1 &&
+ table.getFilteredSelectedRowModel().rows[0].original.status === 'bid_closure')}
+ >
+ <Undo2 className="mr-2 h-4 w-4" />
+ 유찰취소
+ </Button>
+ </div>
</DataTableAdvancedToolbar>
</DataTable>
- {/* 사양설명회 다이얼로그 */}
- <SpecificationMeetingDialog
- open={specMeetingDialogOpen}
- onOpenChange={handleSpecMeetingDialogClose}
- bidding={selectedBidding}
- />
-
- {/* PR 문서 다이얼로그 */}
- <PrDocumentsDialog
- open={prDocumentsDialogOpen}
- onOpenChange={handlePrDocumentsDialogClose}
- bidding={selectedBidding}
- />
+ {/* 폐찰 다이얼로그 */}
+ {selectedBidding && session?.user?.id && (
+ <BidClosureDialog
+ open={biddingClosureDialogOpen}
+ onOpenChange={handleBiddingClosureDialogClose}
+ bidding={selectedBidding}
+ userId={session.user.id}
+ onSuccess={() => {
+ router.refresh()
+ handleBiddingClosureDialogClose()
+ }}
+ />
+ )}
</>
)
}