From 86b1fd1cc801f45642f84d24c0b5c84368454ff0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 9 Sep 2025 10:34:05 +0000 Subject: (최겸) 구매 입찰 사전견적, 입찰, 낙찰, 유찰, 재입찰 기능 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/pre-quote/service.ts | 15 +++- .../pre-quote/table/bidding-pre-quote-content.tsx | 8 +- .../table/bidding-pre-quote-invitation-dialog.tsx | 30 +++++++- .../table/bidding-pre-quote-vendor-columns.tsx | 86 ++++++++++++++++------ .../table/bidding-pre-quote-vendor-table.tsx | 17 +---- 5 files changed, 108 insertions(+), 48 deletions(-) (limited to 'lib/bidding/pre-quote') diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index 35bc8941..3f1b916c 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -133,6 +133,7 @@ export async function updatePreQuoteSelection(companyIds: number[], isSelected: await db.update(biddingCompanies) .set({ isPreQuoteSelected: isSelected, + invitationStatus: 'pending', // 초기 상태: 입찰생성 updatedAt: new Date() }) .where(inArray(biddingCompanies.id, companyIds)) @@ -194,7 +195,9 @@ export async function getBiddingCompanies(biddingId: number) { respondedAt: biddingCompanies.respondedAt, preQuoteAmount: biddingCompanies.preQuoteAmount, preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt, + preQuoteDeadline: biddingCompanies.preQuoteDeadline, isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, + isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated, isAttendingMeeting: biddingCompanies.isAttendingMeeting, notes: biddingCompanies.notes, contactPerson: biddingCompanies.contactPerson, @@ -217,6 +220,7 @@ export async function getBiddingCompanies(biddingId: number) { proposedShippingPort: companyConditionResponses.proposedShippingPort, proposedDestinationPort: companyConditionResponses.proposedDestinationPort, sparePartResponse: companyConditionResponses.sparePartResponse, + additionalProposals: companyConditionResponses.additionalProposals, }) .from(biddingCompanies) .leftJoin( @@ -243,7 +247,7 @@ export async function getBiddingCompanies(biddingId: number) { } // 선택된 업체들에게 사전견적 초대 발송 -export async function sendPreQuoteInvitations(companyIds: number[]) { +export async function sendPreQuoteInvitations(companyIds: number[], preQuoteDeadline?: Date | string) { try { if (companyIds.length === 0) { return { @@ -292,6 +296,7 @@ export async function sendPreQuoteInvitations(companyIds: number[]) { .set({ invitationStatus: 'sent', // 사전견적 초대 발송 상태 invitedAt: new Date(), + preQuoteDeadline: preQuoteDeadline ? new Date(preQuoteDeadline) : null, updatedAt: new Date() }) .where(eq(biddingCompanies.id, id)) @@ -406,7 +411,9 @@ export async function getBiddingCompaniesForPartners(biddingId: number, companyI invitationStatus: biddingCompanies.invitationStatus, preQuoteAmount: biddingCompanies.preQuoteAmount, preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt, + preQuoteDeadline: biddingCompanies.preQuoteDeadline, isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, + isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated, isAttendingMeeting: biddingCompanies.isAttendingMeeting, // company_condition_responses 정보 paymentTermsResponse: companyConditionResponses.paymentTermsResponse, @@ -443,7 +450,9 @@ export async function getBiddingCompaniesForPartners(biddingId: number, companyI invitationStatus: null, preQuoteAmount: null, preQuoteSubmittedAt: null, + preQuoteDeadline: null, isPreQuoteSelected: false, + isPreQuoteParticipated: null, isAttendingMeeting: null, paymentTermsResponse: null, taxConditionsResponse: null, @@ -666,7 +675,7 @@ export async function respondToPreQuoteInvitation( } } -// 벤더에서 사전견적 참여 여부 결정 (isPreQuoteSelected 사용) +// 벤더에서 사전견적 참여 여부 결정 (isPreQuoteSelected, isPreQuoteParticipated 사용) export async function setPreQuoteParticipation( biddingCompanyId: number, isParticipating: boolean, @@ -675,8 +684,8 @@ export async function setPreQuoteParticipation( try { await db.update(biddingCompanies) .set({ + isPreQuoteParticipated: isParticipating, isPreQuoteSelected: isParticipating, - invitationStatus: isParticipating ? 'accepted' : 'declined', respondedAt: new Date(), updatedAt: new Date() }) diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx index 692d12ea..91b80bd3 100644 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx +++ b/lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Bidding } from '@/db/schema' -import { QuotationDetails, QuotationVendor } from '@/lib/bidding/detail/service' +import { QuotationDetails } from '@/lib/bidding/detail/service' import { getBiddingCompanies } from '../service' import { BiddingPreQuoteVendorTableContent } from './bidding-pre-quote-vendor-table' @@ -10,7 +10,6 @@ import { BiddingPreQuoteVendorTableContent } from './bidding-pre-quote-vendor-ta interface BiddingPreQuoteContentProps { bidding: Bidding quotationDetails: QuotationDetails | null - quotationVendors: QuotationVendor[] biddingCompanies: any[] prItems: any[] } @@ -18,7 +17,6 @@ interface BiddingPreQuoteContentProps { export function BiddingPreQuoteContent({ bidding, quotationDetails, - quotationVendors, biddingCompanies: initialBiddingCompanies, prItems }: BiddingPreQuoteContentProps) { @@ -42,15 +40,11 @@ export function BiddingPreQuoteContent({ {}} onOpenTargetPriceDialog={() => {}} onOpenSelectionReasonDialog={() => {}} - onEdit={undefined} - onDelete={undefined} - onSelectWinner={undefined} /> ) diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx index 84824c1e..1b0598b7 100644 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx +++ b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx @@ -3,6 +3,8 @@ import * as React from 'react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' import { Dialog, DialogContent, @@ -16,7 +18,7 @@ import { BiddingCompany } from './bidding-pre-quote-vendor-columns' import { sendPreQuoteInvitations } from '../service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' -import { Mail, Building2 } from 'lucide-react' +import { Mail, Building2, Calendar } from 'lucide-react' interface BiddingPreQuoteInvitationDialogProps { open: boolean @@ -34,6 +36,7 @@ export function BiddingPreQuoteInvitationDialog({ const { toast } = useToast() const [isPending, startTransition] = useTransition() const [selectedCompanyIds, setSelectedCompanyIds] = React.useState([]) + const [preQuoteDeadline, setPreQuoteDeadline] = React.useState('') // 초대 가능한 업체들 (pending 상태인 업체들) const invitableCompanies = companies.filter(company => @@ -67,7 +70,10 @@ export function BiddingPreQuoteInvitationDialog({ } startTransition(async () => { - const response = await sendPreQuoteInvitations(selectedCompanyIds) + const response = await sendPreQuoteInvitations( + selectedCompanyIds, + preQuoteDeadline || undefined + ) if (response.success) { toast({ @@ -75,6 +81,7 @@ export function BiddingPreQuoteInvitationDialog({ description: response.message, }) setSelectedCompanyIds([]) + setPreQuoteDeadline('') onOpenChange(false) onSuccess() } else { @@ -91,6 +98,7 @@ export function BiddingPreQuoteInvitationDialog({ onOpenChange(open) if (!open) { setSelectedCompanyIds([]) + setPreQuoteDeadline('') } } @@ -114,6 +122,24 @@ export function BiddingPreQuoteInvitationDialog({ ) : ( <> + {/* 견적마감일 설정 */} +
+ + setPreQuoteDeadline(e.target.value)} + className="w-full" + /> +

+ 설정하지 않으면 마감일 없이 초대가 발송됩니다. +

+
+ {/* 전체 선택 */}
void onDelete: (company: BiddingCompany) => void - onInvite: (company: BiddingCompany) => void onViewPriceAdjustment?: (company: BiddingCompany) => void onViewItemDetails?: (company: BiddingCompany) => void onViewAttachments?: (company: BiddingCompany) => void @@ -65,7 +66,6 @@ interface GetBiddingCompanyColumnsProps { export function getBiddingPreQuoteVendorColumns({ onEdit, onDelete, - onInvite, onViewPriceAdjustment, onViewItemDetails, onViewAttachments @@ -109,11 +109,28 @@ export function getBiddingPreQuoteVendorColumns({ header: '초대 상태', cell: ({ row }) => { const status = row.original.invitationStatus - const variant = status === 'accepted' ? 'default' : - status === 'declined' ? 'destructive' : 'outline' + let variant: any + let label: string - const label = status === 'accepted' ? '수락' : - status === 'declined' ? '거절' : '대기중' + if (status === 'accepted') { + variant = 'default' + label = '수락' + } else if (status === 'declined') { + variant = 'destructive' + label = '거절' + } else if (status === 'pending') { + variant = 'outline' + label = '대기중' + } else if (status === 'sent') { + variant = 'outline' + label = '요청됨' + } else if (status === 'submitted') { + variant = 'outline' + label = '제출됨' + } else { + variant = 'outline' + label = status || '-' + } return {label} }, @@ -149,6 +166,31 @@ export function getBiddingPreQuoteVendorColumns({
), }, + { + accessorKey: 'preQuoteDeadline', + header: '사전견적 마감일', + cell: ({ row }) => { + const deadline = row.original.preQuoteDeadline + if (!deadline) { + return
-
+ } + + const now = new Date() + const deadlineDate = new Date(deadline) + const isExpired = deadlineDate < now + + return ( +
+
{deadlineDate.toLocaleDateString('ko-KR')}
+ {isExpired && ( + + 마감 + + )} +
+ ) + }, + }, { accessorKey: 'attachments', header: '첨부파일', @@ -173,6 +215,21 @@ export function getBiddingPreQuoteVendorColumns({ ) }, }, + { + accessorKey: 'isPreQuoteParticipated', + header: '사전견적 참여의사', + cell: ({ row }) => { + const participated = row.original.isPreQuoteParticipated + if (participated === null) { + return 미결정 + } + return ( + + {participated ? '참여' : '미참여'} + + ) + }, + }, { accessorKey: 'isPreQuoteSelected', header: '본입찰 선정', @@ -306,15 +363,6 @@ export function getBiddingPreQuoteVendorColumns({ ), }, - { - accessorKey: 'notes', - header: '특이사항', - cell: ({ row }) => ( -
- {row.original.notes || '-'} -
- ), - }, { id: 'actions', header: '액션', @@ -334,12 +382,6 @@ export function getBiddingPreQuoteVendorColumns({ 수정 */} - {company.invitationStatus === 'pending' && ( - onInvite(company)}> - - 초대 발송 - - )} onDelete(company)} className="text-destructive" diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx index a1319821..7ea05721 100644 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx +++ b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx @@ -23,7 +23,6 @@ import { getPrItemsForBidding } from '../service' interface BiddingPreQuoteVendorTableContentProps { biddingId: number bidding: Bidding - vendors: any[] // 사용하지 않음 biddingCompanies: BiddingCompany[] onRefresh: () => void onOpenItemsDialog: () => void @@ -31,7 +30,6 @@ interface BiddingPreQuoteVendorTableContentProps { onOpenSelectionReasonDialog: () => void onEdit?: (company: BiddingCompany) => void onDelete?: (company: BiddingCompany) => void - onSelectWinner?: (company: BiddingCompany) => void } const filterFields: DataTableFilterField[] = [ @@ -80,6 +78,7 @@ const advancedFilterFields: DataTableAdvancedFilterField[] = [ options: [ { label: '수락', value: 'accepted' }, { label: '거절', value: 'declined' }, + { label: '요청됨', value: 'sent' }, { label: '대기중', value: 'pending' }, ], }, @@ -88,15 +87,13 @@ const advancedFilterFields: DataTableAdvancedFilterField[] = [ export function BiddingPreQuoteVendorTableContent({ biddingId, bidding, - vendors, biddingCompanies, onRefresh, onOpenItemsDialog, onOpenTargetPriceDialog, onOpenSelectionReasonDialog, onEdit, - onDelete, - onSelectWinner + onDelete }: BiddingPreQuoteVendorTableContentProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() @@ -137,13 +134,6 @@ export function BiddingPreQuoteVendorTableContent({ setIsEditDialogOpen(true) } - const handleInvite = (company: BiddingCompany) => { - // TODO: 초대 발송 로직 구현 - toast({ - title: '알림', - description: `${company.companyName} 업체에 초대를 발송했습니다.`, - }) - } const handleViewPriceAdjustment = async (company: BiddingCompany) => { startTransition(async () => { @@ -190,12 +180,11 @@ export function BiddingPreQuoteVendorTableContent({ () => getBiddingPreQuoteVendorColumns({ onEdit: onEdit || handleEdit, onDelete: onDelete || handleDelete, - onInvite: handleInvite, onViewPriceAdjustment: handleViewPriceAdjustment, onViewItemDetails: handleViewItemDetails, onViewAttachments: handleViewAttachments }), - [onEdit, onDelete, handleEdit, handleDelete, handleInvite, handleViewPriceAdjustment, handleViewItemDetails, handleViewAttachments] + [onEdit, onDelete, handleEdit, handleDelete, handleViewPriceAdjustment, handleViewItemDetails, handleViewAttachments] ) const { table } = useDataTable({ -- cgit v1.2.3