From d47334639bd717aa860563ec1020a29827524fd4 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 5 Dec 2025 06:29:23 +0000 Subject: (최겸)구매 결재일 기준 공고 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/receive/biddings-receive-table.tsx | 593 +++++++++++++------------ 1 file changed, 297 insertions(+), 296 deletions(-) (limited to 'lib/bidding/receive/biddings-receive-table.tsx') diff --git a/lib/bidding/receive/biddings-receive-table.tsx b/lib/bidding/receive/biddings-receive-table.tsx index 2b141d5e..6a48fa79 100644 --- a/lib/bidding/receive/biddings-receive-table.tsx +++ b/lib/bidding/receive/biddings-receive-table.tsx @@ -1,296 +1,297 @@ -"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 { 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" -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { getBiddingsReceiveColumns } from "./biddings-receive-columns" -import { getBiddingsForReceive } from "@/lib/bidding/service" -import { - biddingStatusLabels, - contractTypeLabels, -} from "@/db/schema" -// import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs" -import { openBiddingAction } from "@/lib/bidding/actions" -import { BiddingParticipantsDialog } from "@/components/bidding/receive/bidding-participants-dialog" -import { getAllBiddingCompanies } from "@/lib/bidding/detail/service" - -type BiddingReceiveItem = { - id: number - biddingNumber: string - originalBiddingNumber: string | null - title: string - status: string - contractType: string - prNumber: string | null - submissionStartDate: Date | null - submissionEndDate: Date | null - bidPicName: string | null - supplyPicName: string | null - createdBy: string | null - createdAt: Date | null - updatedAt: Date | null - - // 참여 현황 - participantExpected: number - participantParticipated: number - participantDeclined: number - participantPending: number - participantFinalSubmitted: number - - // 개찰 정보 - openedAt: Date | null - openedBy: string | null -} - -interface BiddingsReceiveTableProps { - promises: Promise< - [ - Awaited> - ] - > -} - -export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { - 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 [isOpeningBidding, setIsOpeningBidding] = React.useState(false) - - // 협력사 다이얼로그 관련 상태 - const [participantsDialogOpen, setParticipantsDialogOpen] = React.useState(false) - const [selectedParticipantType, setSelectedParticipantType] = React.useState<'expected' | 'participated' | 'declined' | 'pending' | null>(null) - const [selectedBiddingId, setSelectedBiddingId] = React.useState(null) - const [participantCompanies, setParticipantCompanies] = React.useState([]) - const [isLoadingParticipants, setIsLoadingParticipants] = React.useState(false) - - const router = useRouter() - const { data: session } = useSession() - - // 협력사 클릭 핸들러 - const handleParticipantClick = React.useCallback(async (biddingId: number, participantType: 'expected' | 'participated' | 'declined' | 'pending') => { - setSelectedBiddingId(biddingId) - setSelectedParticipantType(participantType) - setIsLoadingParticipants(true) - setParticipantsDialogOpen(true) - - try { - // 협력사 데이터 로드 (모든 초대된 협력사) - const companies = await getAllBiddingCompanies(biddingId) - - console.log('Loaded companies:', companies) - - // 필터링 없이 모든 데이터 그대로 표시 - // invitationStatus가 그대로 다이얼로그에 표시됨 - setParticipantCompanies(companies) - } catch (error) { - console.error('Failed to load participant companies:', error) - toast.error('협력사 목록을 불러오는데 실패했습니다.') - setParticipantCompanies([]) - } finally { - setIsLoadingParticipants(false) - } - }, []) - - const columns = React.useMemo( - () => getBiddingsReceiveColumns({ setRowAction, onParticipantClick: handleParticipantClick }), - [setRowAction, handleParticipantClick] - ) - - // rowAction 변경 감지하여 해당 다이얼로그 열기 - React.useEffect(() => { - if (rowAction) { - setSelectedBidding(rowAction.row.original) - - switch (rowAction.type) { - case "view": - // 상세 페이지로 이동 - router.push(`/evcp/bid/${rowAction.row.original.id}`) - break - default: - break - } - } - }, [rowAction, router]) - - const filterFields: DataTableFilterField[] = [ - { - id: "biddingNumber", - label: "입찰번호", - placeholder: "입찰번호를 입력하세요", - }, - { - id: "prNumber", - label: "P/R번호", - placeholder: "P/R번호를 입력하세요", - }, - { - id: "title", - label: "입찰명", - 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: "submissionStartDate", label: "제출시작일", type: "date" }, - { id: "submissionEndDate", label: "제출마감일", type: "date" }, - ] - - const { table } = useDataTable({ - data, - columns, - pageCount, - filterFields, - enablePinning: true, - enableAdvancedFilter: true, - enableRowSelection: true, - initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => String(originalRow.id), - shallow: false, - clearOnDefault: true, - }) - - const handleCompactChange = React.useCallback((compact: boolean) => { - setIsCompact(compact) - }, []) - - - // 선택된 행 가져오기 - const selectedRows = table.getFilteredSelectedRowModel().rows - const selectedBiddingForAction = selectedRows.length > 0 ? selectedRows[0].original : null - - // 개찰 가능 여부 확인 - const canOpen = React.useMemo(() => { - if (!selectedBiddingForAction) return false - - const now = new Date() - const submissionEndDate = selectedBiddingForAction.submissionEndDate ? new Date(selectedBiddingForAction.submissionEndDate) : null - - // 1. 입찰 마감일이 지났으면 무조건 가능 - if (submissionEndDate && now > submissionEndDate) return true - - // 2. 입찰 기간 내 조기개찰 조건 확인 - // - 미제출 협력사가 0이어야 함 (참여예정 = 최종제출 + 포기) - const participatedOrDeclined = selectedBiddingForAction.participantFinalSubmitted + selectedBiddingForAction.participantDeclined - const isEarlyOpenPossible = participatedOrDeclined === selectedBiddingForAction.participantExpected - - return isEarlyOpenPossible - }, [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 ( - <> - - -
- -
-
-
- - {/* 사양설명회 다이얼로그 */} - {/* */} - - {/* PR 문서 다이얼로그 */} - {/* */} - - {/* 참여 협력사 다이얼로그 */} - - - ) -} +"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 { 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" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { getBiddingsReceiveColumns } from "./biddings-receive-columns" +import { getBiddingsForReceive } from "@/lib/bidding/service" +import { + biddingStatusLabels, + contractTypeLabels, +} from "@/db/schema" +// import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs" +import { openBiddingAction } from "@/lib/bidding/actions" +import { BiddingParticipantsDialog } from "@/components/bidding/receive/bidding-participants-dialog" +import { getAllBiddingCompanies } from "@/lib/bidding/detail/service" + +type BiddingReceiveItem = { + id: number + biddingNumber: string + originalBiddingNumber: string | null + title: string + status: string + contractType: string + prNumber: string | null + submissionStartDate: Date | null + submissionEndDate: Date | null + bidPicName: string | null + supplyPicName: string | null + createdBy: string | null + createdAt: Date | null + updatedAt: Date | null + + // 참여 현황 + participantExpected: number + participantParticipated: number + participantDeclined: number + participantPending: number + participantFinalSubmitted: number + + // 개찰 정보 + openedAt: Date | null + openedBy: string | null +} + +interface BiddingsReceiveTableProps { + promises: Promise< + [ + Awaited> + ] + > +} + +export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { + 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 [isOpeningBidding, setIsOpeningBidding] = React.useState(false) + + // 협력사 다이얼로그 관련 상태 + const [participantsDialogOpen, setParticipantsDialogOpen] = React.useState(false) + const [selectedParticipantType, setSelectedParticipantType] = React.useState<'expected' | 'participated' | 'declined' | 'pending' | null>(null) + const [selectedBiddingId, setSelectedBiddingId] = React.useState(null) + const [participantCompanies, setParticipantCompanies] = React.useState([]) + const [isLoadingParticipants, setIsLoadingParticipants] = React.useState(false) + + const router = useRouter() + const { data: session } = useSession() + + // 협력사 클릭 핸들러 + const handleParticipantClick = React.useCallback(async (biddingId: number, participantType: 'expected' | 'participated' | 'declined' | 'pending') => { + setSelectedBiddingId(biddingId) + setSelectedParticipantType(participantType) + setIsLoadingParticipants(true) + setParticipantsDialogOpen(true) + + try { + // 협력사 데이터 로드 (모든 초대된 협력사) + const companies = await getAllBiddingCompanies(biddingId) + + console.log('Loaded companies:', companies) + + // 필터링 없이 모든 데이터 그대로 표시 + // invitationStatus가 그대로 다이얼로그에 표시됨 + setParticipantCompanies(companies) + } catch (error) { + console.error('Failed to load participant companies:', error) + toast.error('협력사 목록을 불러오는데 실패했습니다.') + setParticipantCompanies([]) + } finally { + setIsLoadingParticipants(false) + } + }, []) + + const columns = React.useMemo( + () => getBiddingsReceiveColumns({ setRowAction, onParticipantClick: handleParticipantClick }), + [setRowAction, handleParticipantClick] + ) + + // rowAction 변경 감지하여 해당 다이얼로그 열기 + React.useEffect(() => { + if (rowAction) { + setSelectedBidding(rowAction.row.original) + + switch (rowAction.type) { + case "view": + // 상세 페이지로 이동 + router.push(`/evcp/bid/${rowAction.row.original.id}`) + break + default: + break + } + } + }, [rowAction, router]) + + const filterFields: DataTableFilterField[] = [ + { + id: "biddingNumber", + label: "입찰번호", + placeholder: "입찰번호를 입력하세요", + }, + { + id: "prNumber", + label: "P/R번호", + placeholder: "P/R번호를 입력하세요", + }, + { + id: "title", + label: "입찰명", + 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: "submissionStartDate", label: "제출시작일", type: "date" }, + { id: "submissionEndDate", label: "제출마감일", type: "date" }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + enableRowSelection: true, + enableMultiRowSelection: false, // 단일 선택만 가능 + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + const handleCompactChange = React.useCallback((compact: boolean) => { + setIsCompact(compact) + }, []) + + + // 선택된 행 가져오기 + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedBiddingForAction = selectedRows.length > 0 ? selectedRows[0].original : null + + // 개찰 가능 여부 확인 + const canOpen = React.useMemo(() => { + if (!selectedBiddingForAction) return false + + const now = new Date() + const submissionEndDate = selectedBiddingForAction.submissionEndDate ? new Date(selectedBiddingForAction.submissionEndDate) : null + + // 1. 입찰 마감일이 지났으면 무조건 가능 + if (submissionEndDate && now > submissionEndDate) return true + + // 2. 입찰 기간 내 조기개찰 조건 확인 + // - 미제출 협력사가 0이어야 함 (참여예정 = 최종제출 + 포기) + const participatedOrDeclined = selectedBiddingForAction.participantFinalSubmitted + selectedBiddingForAction.participantDeclined + const isEarlyOpenPossible = participatedOrDeclined === selectedBiddingForAction.participantExpected + + return isEarlyOpenPossible + }, [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 ( + <> + + +
+ +
+
+
+ + {/* 사양설명회 다이얼로그 */} + {/* */} + + {/* PR 문서 다이얼로그 */} + {/* */} + + {/* 참여 협력사 다이얼로그 */} + + + ) +} -- cgit v1.2.3