"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 { BiddingsClosureDialog } from "./biddings-closure-dialog" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { FileX, RefreshCw, Undo2 } from "lucide-react" import { bidClosureAction, cancelDisposalAction } from "@/lib/bidding/actions" import { increaseRoundOrRebid } from "@/lib/bidding/service" import { ApprovalPreviewDialog } from "@/lib/approval/approval-preview-dialog" import { requestBiddingClosureWithApproval } from "@/lib/bidding/approval-actions" import { useToast } from "@/hooks/use-toast" type BiddingFailureItem = { id: number biddingNumber: string originalBiddingNumber: string | null title: string status: string contractType: string prNumber: string | null // 가격 정보 targetPrice: string | 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 // 폐찰수정자 // 폐찰 정보 closureReason: string | null // 폐찰사유 closureDocuments: { id: number fileName: string originalFileName: string filePath: string }[] // 폐찰 첨부파일들 // 기타 정보 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 [biddingClosureDialogOpen, setBiddingClosureDialogOpen] = React.useState(false) const [selectedBidding, setSelectedBidding] = React.useState(null) const [isRebidDialogOpen, setIsRebidDialogOpen] = React.useState(false) const [selectedBiddingForRebid, setSelectedBiddingForRebid] = React.useState(null) const [approvalPreviewData, setApprovalPreviewData] = React.useState<{ templateName: string variables: Record title: string description: string files?: File[] } | null>(null) const [isApprovalPreviewDialogOpen, setIsApprovalPreviewDialogOpen] = React.useState(false) const { toast } = useToast() 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}/info`) break case "rebid": // 재입찰 팝업 열기 setSelectedBiddingForRebid(rowAction.row.original) setIsRebidDialogOpen(true) break case "closure": // 폐찰 setSelectedBidding(rowAction.row.original) setBiddingClosureDialogOpen(true) break case "cancelDisposal": // 유찰취소 handleCancelDisposal(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, enableRowSelection: true, singleRowSelection: 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 handleBiddingClosureDialogClose = React.useCallback(() => { setBiddingClosureDialogOpen(false) setRowAction(null) setSelectedBidding(null) }, []) const handleRebidWithNavigation = 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 as any).message || "재입찰이 완료되었습니다.", }) // 새로 생성된 입찰의 상세 페이지로 이동 if ((result as any).biddingId) { router.push(`/evcp/bid/${(result as any).biddingId}`) } else { 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 ( <> {/* Toolbar 액션 버튼들 */}
{/* 폐찰 다이얼로그 */} {selectedBidding && session?.user?.id && ( { router.refresh() handleBiddingClosureDialogClose() }} onApprovalPreview={(data) => { setApprovalPreviewData(data) setIsApprovalPreviewDialogOpen(true) }} /> )} {/* 재입찰 확인 다이얼로그 */} 재입찰 확인 입찰을 재입찰 처리하시겠습니까? 재입찰 후 새로운 입찰 화면으로 이동합니다. {/* 폐찰 결재 미리보기 다이얼로그 */} {session?.user && session.user.epId && approvalPreviewData && ( { setIsApprovalPreviewDialogOpen(open) if (!open) { setApprovalPreviewData(null) } }} templateName={approvalPreviewData.templateName} variables={approvalPreviewData.variables} title={approvalPreviewData.title} currentUser={{ id: Number(session.user.id), epId: session.user.epId, name: session.user.name || undefined, email: session.user.email || undefined }} onConfirm={handleClosureApprovalConfirm} /> )} ) // 폐찰 결재 상신 핸들러 const handleClosureApprovalConfirm = async (data: { approvers: string[]; title: string; attachments?: File[] }) => { if (!session?.user?.id || !approvalPreviewData || !selectedBidding) return try { const result = await requestBiddingClosureWithApproval({ biddingId: selectedBidding.id, description: approvalPreviewData.description, files: approvalPreviewData.files, currentUser: { id: Number(session.user.id), epId: session.user.epId || null, email: session.user.email || undefined }, approvers: data.approvers, }) if (result.status === 'pending_approval') { toast({ title: '성공', description: `폐찰 결재가 상신되었습니다. (ID: ${result.approvalId})`, }) setIsApprovalPreviewDialogOpen(false) setApprovalPreviewData(null) handleBiddingClosureDialogClose() router.refresh() } else { toast({ title: '오류', description: '폐찰 결재 상신 중 오류가 발생했습니다.', variant: 'destructive', }) } } catch (error) { console.error('폐찰 결재 상신 실패:', error) toast({ title: '오류', description: '폐찰 결재 상신 중 오류가 발생했습니다.', variant: 'destructive', }) } } }