summaryrefslogtreecommitdiff
path: root/components/bidding/manage
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/manage')
-rw-r--r--components/bidding/manage/bidding-basic-info-editor.tsx136
-rw-r--r--components/bidding/manage/bidding-companies-editor.tsx48
-rw-r--r--components/bidding/manage/bidding-detail-vendor-create-dialog.tsx14
-rw-r--r--components/bidding/manage/bidding-items-editor.tsx49
-rw-r--r--components/bidding/manage/bidding-schedule-editor.tsx225
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>
)
}