diff options
Diffstat (limited to 'components/bidding/manage')
5 files changed, 254 insertions, 218 deletions
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index e92c39a5..f0d56689 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -45,6 +45,16 @@ import type { ProcurementManagerWithUser } from '@/components/common/selectors/p import { getBiddingDocuments, uploadBiddingDocument, deleteBiddingDocument } from '@/lib/bidding/detail/service' import { downloadFile } from '@/lib/file-download' +// Dropzone components +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone" + // 입찰 기본 정보 에디터 컴포넌트 interface BiddingBasicInfo { title?: string @@ -78,6 +88,7 @@ interface BiddingBasicInfo { interface BiddingBasicInfoEditorProps { biddingId: number + readonly?: boolean } interface UploadedDocument { @@ -95,7 +106,7 @@ interface UploadedDocument { uploadedBy: string } -export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProps) { +export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingBasicInfoEditorProps) { const [isLoading, setIsLoading] = React.useState(true) const [isSubmitting, setIsSubmitting] = React.useState(false) const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false) @@ -535,7 +546,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp <FormItem> <FormLabel>입찰명 <span className="text-red-500">*</span></FormLabel> <FormControl> - <Input placeholder="입찰명을 입력하세요" {...field} /> + <Input placeholder="입찰명을 입력하세요" {...field} disabled={readonly} /> </FormControl> <FormMessage /> </FormItem> @@ -883,7 +894,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp <FormItem> <FormLabel>입찰개요</FormLabel> <FormControl> - <Textarea placeholder="입찰에 대한 설명을 입력하세요" rows={2} {...field} /> + <Textarea placeholder="입찰에 대한 설명을 입력하세요" rows={2} {...field} readOnly={readonly} /> </FormControl> <FormMessage /> </FormItem> @@ -896,7 +907,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp <FormItem> <FormLabel>비고</FormLabel> <FormControl> - <Textarea placeholder="추가 사항이나 참고사항을 입력하세요" rows={3} {...field} /> + <Textarea placeholder="추가 사항이나 참고사항을 입력하세요" rows={3} {...field} readOnly={readonly} /> </FormControl> <FormMessage /> </FormItem> @@ -1123,6 +1134,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp })) }} rows={3} + readOnly={readonly} /> </div> </div> @@ -1138,7 +1150,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp <TiptapEditor content={field.value || noticeTemplate} setContent={field.onChange} - disabled={isLoadingTemplate} + disabled={isLoadingTemplate || readonly} height="300px" /> </div> @@ -1156,12 +1168,14 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp </div> {/* 액션 버튼 */} - <div className="flex justify-end gap-4 pt-4"> - <Button type="submit" disabled={isSubmitting} className="flex items-center gap-2"> - {isSubmitting ? '저장 중...' : '저장'} - <ChevronRight className="h-4 w-4" /> - </Button> - </div> + {!readonly && ( + <div className="flex justify-end gap-4 pt-4"> + <Button type="submit" disabled={isSubmitting} className="flex items-center gap-2"> + {isSubmitting ? '저장 중...' : '저장'} + <ChevronRight className="h-4 w-4" /> + </Button> + </div> + )} </form> </Form> </CardContent> @@ -1175,33 +1189,35 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp SHI용 첨부파일 </CardTitle> <p className="text-sm text-muted-foreground"> - SHI에서 제공하는 문서나 파일을 업로드하세요 + 내부 보관를 위해 필요한 문서나 파일을 업로드 하세요. </p> </CardHeader> <CardContent className="space-y-4"> - <div className="border-2 border-dashed border-gray-300 rounded-lg p-6"> - <div className="text-center"> - <Upload className="h-12 w-12 text-gray-400 mx-auto mb-4" /> - <div className="space-y-2"> - <p className="text-sm text-gray-600"> - 파일을 드래그 앤 드롭하거나{' '} - <label className="text-blue-600 hover:text-blue-500 cursor-pointer"> - <input - type="file" - multiple - className="hidden" - onChange={(e) => { - const files = Array.from(e.target.files || []) - setShiAttachmentFiles(prev => [...prev, ...files]) - }} - accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg" - /> - 찾아보세요 - </label> - </p> - </div> - </div> - </div> + <Dropzone + maxSize={6e8} // 600MB + onDropAccepted={(files) => { + const newFiles = Array.from(files) + setShiAttachmentFiles(prev => [...prev, ...newFiles]) + }} + onDropRejected={() => { + toast({ + title: "File upload rejected", + description: "Please check file size and type.", + variant: "destructive", + }) + }} + > + {() => ( + <DropzoneZone className="flex justify-center h-32"> + <div className="flex items-center gap-6"> + <DropzoneUploadIcon /> + <div className="grid gap-0.5"> + <DropzoneTitle>파일을 드래그하여 업로드</DropzoneTitle> + </div> + </div> + </DropzoneZone> + )} + </Dropzone> {shiAttachmentFiles.length > 0 && ( <div className="space-y-2"> @@ -1307,33 +1323,35 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp 협력업체용 첨부파일 </CardTitle> <p className="text-sm text-muted-foreground"> - 협력업체에서 제공하는 문서나 파일을 업로드하세요 + 협력사로 제공하는 문서나 파일을 업로드 하세요. </p> </CardHeader> <CardContent className="space-y-4"> - <div className="border-2 border-dashed border-gray-300 rounded-lg p-6"> - <div className="text-center"> - <Upload className="h-12 w-12 text-gray-400 mx-auto mb-4" /> - <div className="space-y-2"> - <p className="text-sm text-gray-600"> - 파일을 드래그 앤 드롭하거나{' '} - <label className="text-blue-600 hover:text-blue-500 cursor-pointer"> - <input - type="file" - multiple - className="hidden" - onChange={(e) => { - const files = Array.from(e.target.files || []) - setVendorAttachmentFiles(prev => [...prev, ...files]) - }} - accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg" - /> - 찾아보세요 - </label> - </p> - </div> - </div> - </div> + <Dropzone + maxSize={6e8} // 600MB + onDropAccepted={(files) => { + const newFiles = Array.from(files) + setVendorAttachmentFiles(prev => [...prev, ...newFiles]) + }} + onDropRejected={() => { + toast({ + title: "File upload rejected", + description: "Please check file size and type.", + variant: "destructive", + }) + }} + > + {() => ( + <DropzoneZone className="flex justify-center h-32"> + <div className="flex items-center gap-6"> + <DropzoneUploadIcon /> + <div className="grid gap-0.5"> + <DropzoneTitle>파일을 드래그하여 업로드</DropzoneTitle> + </div> + </div> + </DropzoneZone> + )} + </Dropzone> {vendorAttachmentFiles.length > 0 && ( <div className="space-y-2"> diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index 4992c2ab..a81f0063 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -11,8 +11,7 @@ import { createBiddingCompanyContact, deleteBiddingCompanyContact, getVendorContactsByVendorId, - updateBiddingCompanyPriceAdjustmentQuestion, - getBiddingById + updateBiddingCompanyPriceAdjustmentQuestion } from '@/lib/bidding/service' import { deleteBiddingCompany } from '@/lib/bidding/pre-quote/service' import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog' @@ -54,6 +53,7 @@ interface QuotationVendor { interface BiddingCompaniesEditorProps { biddingId: number + readonly?: boolean } interface VendorContact { @@ -79,7 +79,7 @@ interface BiddingCompanyContact { updatedAt: Date } -export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProps) { +export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingCompaniesEditorProps) { const [vendors, setVendors] = React.useState<QuotationVendor[]>([]) const [isLoading, setIsLoading] = React.useState(false) const [addVendorDialogOpen, setAddVendorDialogOpen] = React.useState(false) @@ -89,10 +89,6 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp // 각 업체별 첫 번째 담당자 정보 저장 (vendorId -> 첫 번째 담당자) const [vendorFirstContacts, setVendorFirstContacts] = React.useState<Map<number, BiddingCompanyContact>>(new Map()) - // 입찰 정보 (단수/복수 낙찰 확인용) - const [biddingInfo, setBiddingInfo] = React.useState<any>(null) - const [isLoadingBiddingInfo, setIsLoadingBiddingInfo] = React.useState(false) - // 담당자 추가 다이얼로그 const [addContactDialogOpen, setAddContactDialogOpen] = React.useState(false) const [newContact, setNewContact] = React.useState({ @@ -146,29 +142,6 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp } }, [biddingId]) - // 입찰 정보 로딩 - const loadBiddingInfo = React.useCallback(async () => { - setIsLoadingBiddingInfo(true) - try { - const result = await getBiddingById(biddingId) - if (result) { - setBiddingInfo(result) - } else { - console.error('Failed to load bidding info') - setBiddingInfo(null) - } - } catch (error) { - console.error('Failed to load bidding info:', error) - setBiddingInfo(null) - } finally { - setIsLoadingBiddingInfo(false) - } - }, [biddingId]) - - // 단수 입찰 여부 확인 및 업체 추가 제한 - const isSingleAwardBidding = biddingInfo?.awardCount === 'single' - const canAddVendor = !isSingleAwardBidding || vendors.length === 0 - // 데이터 로딩 React.useEffect(() => { const loadVendors = async () => { @@ -222,8 +195,7 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp } loadVendors() - loadBiddingInfo() - }, [biddingId, loadBiddingInfo]) + }, [biddingId]) // 업체 선택 핸들러 (단일 선택) const handleVendorSelect = async (vendor: QuotationVendor) => { @@ -513,10 +485,12 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp </p> <p className="text-sm text-muted-foreground mt-1"> 단수 입찰의 경우 1개 업체만 등록 가능합니다. </p> </div> - <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2"> - <Plus className="h-4 w-4" /> - 업체 추가 - </Button> + {!readonly && ( + <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2"> + <Plus className="h-4 w-4" /> + 업체 추가 + </Button> + )} </CardHeader> <CardContent> {vendors.length === 0 ? ( @@ -687,8 +661,6 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp open={addVendorDialogOpen} onOpenChange={setAddVendorDialogOpen} onSuccess={reloadVendors} - isSingleAwardBidding={isSingleAwardBidding} - currentVendorCount={vendors.length} /> {/* 담당자 추가 다이얼로그 (직접 입력) */} diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx index 205224b9..de813121 100644 --- a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx +++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx @@ -39,8 +39,6 @@ interface BiddingDetailVendorCreateDialogProps { open: boolean onOpenChange: (open: boolean) => void onSuccess: () => void - isSingleAwardBidding?: boolean - currentVendorCount?: number } interface Vendor { @@ -60,8 +58,6 @@ export function BiddingDetailVendorCreateDialog({ open, onOpenChange, onSuccess, - isSingleAwardBidding = false, - currentVendorCount = 0 }: BiddingDetailVendorCreateDialogProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() @@ -104,16 +100,6 @@ export function BiddingDetailVendorCreateDialog({ // 벤더 추가 const handleAddVendor = (vendor: Vendor) => { - // 단수 입찰이고 이미 업체가 선택되었거나 기존 업체가 있는 경우 제한 - if (isSingleAwardBidding && (selectedVendorsWithQuestion.length > 0 || currentVendorCount > 0)) { - toast({ - title: '제한 사항', - description: '단수 입찰의 경우 1개 업체만 등록 가능합니다.', - variant: 'destructive', - }) - return - } - if (!selectedVendorsWithQuestion.find(v => v.vendor.id === vendor.id)) { setSelectedVendorsWithQuestion([ ...selectedVendorsWithQuestion, diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index dc0aaeec..38113dfa 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -76,6 +76,7 @@ interface PRItemInfo { interface BiddingItemsEditorProps { biddingId: number + readonly?: boolean } import { removeBiddingItem, addPRItemForBidding, getBiddingById, getBiddingConditions } from '@/lib/bidding/service' @@ -84,7 +85,7 @@ import { ProcurementItemSelectorDialogSingle } from '@/components/common/selecto import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' -export function BiddingItemsEditor({ biddingId }: BiddingItemsEditorProps) { +export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItemsEditorProps) { const { data: session } = useSession() const [items, setItems] = React.useState<PRItemInfo[]>([]) const [isLoading, setIsLoading] = React.useState(false) @@ -620,7 +621,7 @@ export function BiddingItemsEditor({ biddingId }: BiddingItemsEditorProps) { /> </td> <td className="border-r px-3 py-2"> - {biddingType === 'equipment' ? ( + {biddingType !== 'equipment' ? ( <ProcurementItemSelectorDialogSingle triggerLabel={item.materialGroupNumber || "품목 선택"} triggerVariant="outline" @@ -642,8 +643,8 @@ export function BiddingItemsEditor({ biddingId }: BiddingItemsEditorProps) { }) } }} - title="품목 선택" - description="품목을 검색하고 선택해주세요." + title="1회성 품목 선택" + description="1회성 품목을 검색하고 선택해주세요." /> ) : ( <MaterialGroupSelectorDialogSingle @@ -1149,25 +1150,27 @@ export function BiddingItemsEditor({ biddingId }: BiddingItemsEditorProps) { </Card> {/* 액션 버튼 */} - <div className="flex justify-end gap-4"> - <Button - onClick={handleSave} - disabled={isSubmitting} - 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> + {!readonly && ( + <div className="flex justify-end gap-4"> + <Button + onClick={handleSave} + disabled={isSubmitting} + 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> + )} {/* 사전견적용 일반견적 생성 다이얼로그 */} <CreatePreQuoteRfqDialog 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> ) } |
