diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-18 10:30:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-18 10:30:31 +0000 |
| commit | c4f5472b961afb237dc819f9dd3f42a7b8f71075 (patch) | |
| tree | a1c0d00e46a005ff472bf1125e739bae73b0a53e /components/bidding/manage/bidding-schedule-editor.tsx | |
| parent | 1d1f6010704a1d655b3007887db0fe3ac866177a (diff) | |
(최겸) 구매 입찰 수정, 입찰초대 결재 등록, 재입찰, 차수증가, 폐찰, 유찰취소 로직 수정, readonly 추가 등
Diffstat (limited to 'components/bidding/manage/bidding-schedule-editor.tsx')
| -rw-r--r-- | components/bidding/manage/bidding-schedule-editor.tsx | 225 |
1 files changed, 141 insertions, 84 deletions
diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx index ce03c742..f2978f95 100644 --- a/components/bidding/manage/bidding-schedule-editor.tsx +++ b/components/bidding/manage/bidding-schedule-editor.tsx @@ -12,6 +12,9 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' +import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog' +import { requestBiddingInvitationWithApproval } from '@/lib/bidding/approval-actions' +import { prepareBiddingApprovalData } from '@/lib/bidding/approval-actions' import { BiddingInvitationDialog } from '@/lib/bidding/detail/table/bidding-invitation-dialog' import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from '@/lib/bidding/pre-quote/service' import { registerBidding } from '@/lib/bidding/detail/service' @@ -41,16 +44,17 @@ interface SpecificationMeetingInfo { interface BiddingScheduleEditorProps { biddingId: number + readonly?: boolean } interface VendorContractRequirement { vendorId: number vendorName: string - vendorCode?: string | null + vendorCode?: string vendorCountry?: string - vendorEmail?: string | null - contactPerson?: string | null - contactEmail?: string | null + vendorEmail?: string + contactPerson?: string + contactEmail?: string ndaYn?: boolean generalGtcYn?: boolean projectGtcYn?: boolean @@ -88,7 +92,7 @@ interface BiddingInvitationData { message?: string } -export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) { +export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingScheduleEditorProps) { const { data: session } = useSession() const router = useRouter() const { toast } = useToast() @@ -110,7 +114,11 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) const [isSubmitting, setIsSubmitting] = React.useState(false) const [biddingInfo, setBiddingInfo] = React.useState<{ title: string; projectName?: string; status: string } | null>(null) const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false) + const [isApprovalDialogOpen, setIsApprovalDialogOpen] = React.useState(false) const [selectedVendors, setSelectedVendors] = React.useState<VendorContractRequirement[]>([]) + const [approvalVariables, setApprovalVariables] = React.useState<Record<string, string>>({}) + const [approvalTitle, setApprovalTitle] = React.useState('') + const [invitationData, setInvitationData] = React.useState<BiddingInvitationData | null>(null) // 데이터 로딩 React.useEffect(() => { @@ -197,11 +205,11 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) return result.vendors.map((vendor): VendorContractRequirement => ({ vendorId: vendor.vendorId, vendorName: vendor.vendorName, - vendorCode: vendor.vendorCode ?? undefined, + vendorCode: vendor.vendorCode || undefined, vendorCountry: vendor.vendorCountry, - vendorEmail: vendor.vendorEmail ?? undefined, - contactPerson: vendor.contactPerson ?? undefined, - contactEmail: vendor.contactEmail ?? undefined, + vendorEmail: vendor.vendorEmail || undefined, + contactPerson: vendor.contactPerson || undefined, + contactEmail: vendor.contactEmail || undefined, ndaYn: vendor.ndaYn, generalGtcYn: vendor.generalGtcYn, projectGtcYn: vendor.projectGtcYn, @@ -219,7 +227,7 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) } }, [biddingId]) - // 본입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회 + // 입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회 React.useEffect(() => { if (isBiddingInvitationDialogOpen) { getSelectedVendors().then(vendors => { @@ -228,75 +236,96 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) } }, [isBiddingInvitationDialogOpen, getSelectedVendors]) - // 입찰 초대 발송 핸들러 - const handleBiddingInvitationSend = async (data: BiddingInvitationData) => { - try { - const userId = session?.user?.id?.toString() || '1' - - // 1. 기본계약 발송 - // sendBiddingBasicContracts에 필요한 형식으로 변환 - const vendorDataForContract = data.vendors.map(vendor => ({ - vendorId: vendor.vendorId, - vendorName: vendor.vendorName, - vendorCode: vendor.vendorCode || undefined, - vendorCountry: vendor.vendorCountry, - selectedMainEmail: vendor.selectedMainEmail, - additionalEmails: vendor.additionalEmails, - customEmails: vendor.customEmails, - contractRequirements: { - ndaYn: vendor.ndaYn || false, - generalGtcYn: vendor.generalGtcYn || false, - projectGtcYn: vendor.projectGtcYn || false, - agreementYn: vendor.agreementYn || false, - }, - biddingCompanyId: vendor.biddingCompanyId, - biddingId: vendor.biddingId, - hasExistingContracts: vendor.hasExistingContracts, - })) - - const contractResult = await sendBiddingBasicContracts( - biddingId, - vendorDataForContract, - data.generatedPdfs, - data.message - ) + // 입찰공고 버튼 클릭 핸들러 - 입찰 초대 다이얼로그 열기 + const handleBiddingInvitationClick = () => { + setIsBiddingInvitationDialogOpen(true) + } - if (!contractResult.success) { - const errorMessage = 'message' in contractResult - ? contractResult.message - : 'error' in contractResult - ? contractResult.error - : '기본계약 발송에 실패했습니다.' + // 결재 상신 핸들러 - 결재 완료 시 실제 입찰 등록 실행 + const handleApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => { + try { + if (!session?.user?.id || !session.user.epId || !invitationData) { toast({ - title: '기본계약 발송 실패', - description: errorMessage, + title: '오류', + description: '필요한 정보가 없습니다.', variant: 'destructive', }) return } - // 2. 입찰 등록 진행 - const registerResult = await registerBidding(biddingId, userId) + // 결재 상신 + const result = await requestBiddingInvitationWithApproval({ + biddingId, + vendors: selectedVendors, + message: invitationData.message || '', + currentUser: { + id: session.user.id, + epId: session.user.epId, + email: session.user.email || undefined, + }, + approvers, + }) - if (registerResult.success) { + if (result.status === 'pending_approval') { toast({ - title: '본입찰 초대 완료', - description: '기본계약 발송 및 본입찰 초대가 완료되었습니다.', + title: '입찰초대 결재 상신 완료', + description: `결재가 상신되었습니다. (ID: ${result.approvalId})`, }) + setIsApprovalDialogOpen(false) setIsBiddingInvitationDialogOpen(false) + setInvitationData(null) router.refresh() - } else { + } + } catch (error) { + console.error('결재 상신 중 오류 발생:', error) + toast({ + title: '오류', + description: '결재 상신 중 오류가 발생했습니다.', + variant: 'destructive', + }) + } + } + + // 입찰 초대 발송 핸들러 - 결재 준비 및 결재 다이얼로그 열기 + const handleBiddingInvitationSend = async (data: BiddingInvitationData) => { + try { + if (!session?.user?.id || !session.user.epId) { + toast({ + title: '오류', + description: '사용자 정보가 없습니다.', + variant: 'destructive', + }) + return + } + + // 선정된 업체들 조회 + const vendors = await getSelectedVendors() + if (vendors.length === 0) { toast({ title: '오류', - description: 'error' in registerResult ? registerResult.error : '입찰 등록에 실패했습니다.', + description: '선정된 업체가 없습니다.', variant: 'destructive', }) + return } + + // 결재 데이터 준비 (템플릿 변수, 제목 등) + const approvalData = await prepareBiddingApprovalData({ + biddingId, + vendors, + message: data.message || '', + }) + + // 결재 준비 완료 - invitationData와 결재 데이터 저장 및 결재 다이얼로그 열기 + setInvitationData(data) + setApprovalVariables(approvalData.variables) + setApprovalTitle(`입찰초대 - ${approvalData.bidding.title}`) + setIsApprovalDialogOpen(true) } catch (error) { - console.error('본입찰 초대 실패:', error) + console.error('결재 준비 중 오류 발생:', error) toast({ title: '오류', - description: '본입찰 초대에 실패했습니다.', + description: '결재 준비 중 오류가 발생했습니다.', variant: 'destructive', }) } @@ -614,36 +643,38 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) </Card> {/* 액션 버튼 */} - <div className="flex justify-between gap-4"> - <Button - variant="default" - onClick={() => setIsBiddingInvitationDialogOpen(true)} - disabled={!biddingInfo || biddingInfo.status !== 'bidding_generated'} - className="min-w-[120px]" - > - <Send className="w-4 h-4 mr-2" /> - 입찰공고 - </Button> - <div className="flex gap-4"> + {!readonly && ( + <div className="flex justify-between gap-4"> <Button - onClick={handleSave} - disabled={isSubmitting || !biddingInfo || biddingInfo.status !== 'bidding_generated'} + variant="default" + onClick={handleBiddingInvitationClick} + disabled={!biddingInfo || biddingInfo.status !== 'bidding_generated'} className="min-w-[120px]" > - {isSubmitting ? ( - <> - <RefreshCw className="w-4 h-4 mr-2 animate-spin" /> - 저장 중... - </> - ) : ( - <> - <Save className="w-4 h-4 mr-2" /> - 저장 - </> - )} + <Send className="w-4 h-4 mr-2" /> + 입찰공고 </Button> + <div className="flex gap-4"> + <Button + onClick={handleSave} + disabled={isSubmitting || !biddingInfo || biddingInfo.status !== 'bidding_generated'} + className="min-w-[120px]" + > + {isSubmitting ? ( + <> + <RefreshCw className="w-4 h-4 mr-2 animate-spin" /> + 저장 중... + </> + ) : ( + <> + <Save className="w-4 h-4 mr-2" /> + 저장 + </> + )} + </Button> + </div> </div> - </div> + )} {/* 입찰 초대 다이얼로그 */} {biddingInfo && ( @@ -656,6 +687,32 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) onSend={handleBiddingInvitationSend} /> )} + + {/* 입찰초대 결재 다이얼로그 */} + {session?.user && session.user.epId && biddingInfo && invitationData && Object.keys(approvalVariables).length > 0 && ( + <ApprovalPreviewDialog + open={isApprovalDialogOpen} + onOpenChange={(open) => { + setIsApprovalDialogOpen(open) + if (!open) { + // 다이얼로그가 닫히면 결재 변수 초기화 + setApprovalVariables({}) + setApprovalTitle('') + setInvitationData(null) + } + }} + templateName="입찰초대 결재" + variables={approvalVariables} + title={approvalTitle} + currentUser={{ + id: Number(session.user.id), + epId: session.user.epId, + name: session.user.name || undefined, + email: session.user.email || undefined + }} + onConfirm={handleApprovalSubmit} + /> + )} </div> ) } |
