diff options
Diffstat (limited to 'lib/bidding/selection')
| -rw-r--r-- | lib/bidding/selection/actions.ts | 69 | ||||
| -rw-r--r-- | lib/bidding/selection/bidding-info-card.tsx | 2 | ||||
| -rw-r--r-- | lib/bidding/selection/bidding-item-table.tsx | 192 | ||||
| -rw-r--r-- | lib/bidding/selection/bidding-selection-detail-content.tsx | 11 | ||||
| -rw-r--r-- | lib/bidding/selection/biddings-selection-table.tsx | 6 | ||||
| -rw-r--r-- | lib/bidding/selection/selection-result-form.tsx | 213 | ||||
| -rw-r--r-- | lib/bidding/selection/vendor-selection-table.tsx | 4 |
7 files changed, 447 insertions, 50 deletions
diff --git a/lib/bidding/selection/actions.ts b/lib/bidding/selection/actions.ts index f19fbe6d..91550960 100644 --- a/lib/bidding/selection/actions.ts +++ b/lib/bidding/selection/actions.ts @@ -131,6 +131,75 @@ export async function saveSelectionResult(data: SaveSelectionResultData) { } } +// 선정결과 조회 +export async function getSelectionResult(biddingId: number) { + try { + // 선정결과 조회 (selectedCompanyId가 null인 레코드) + const allResults = await db + .select() + .from(vendorSelectionResults) + .where(eq(vendorSelectionResults.biddingId, biddingId)) + + // @ts-ignore + const existingResult = allResults.filter((result: any) => result.selectedCompanyId === null).slice(0, 1) + + if (existingResult.length === 0) { + return { + success: true, + data: { + summary: '', + attachments: [] + } + } + } + + const result = existingResult[0] + + // 첨부파일 조회 + const documents = await db + .select({ + id: biddingDocuments.id, + fileName: biddingDocuments.fileName, + originalFileName: biddingDocuments.originalFileName, + fileSize: biddingDocuments.fileSize, + mimeType: biddingDocuments.mimeType, + filePath: biddingDocuments.filePath, + uploadedAt: biddingDocuments.uploadedAt + }) + .from(biddingDocuments) + .where(and( + eq(biddingDocuments.biddingId, biddingId), + eq(biddingDocuments.documentType, 'selection_result') + )) + + return { + success: true, + data: { + summary: result.evaluationSummary || '', + attachments: documents.map(doc => ({ + id: doc.id, + fileName: doc.fileName || doc.originalFileName || '', + originalFileName: doc.originalFileName || '', + fileSize: doc.fileSize || 0, + mimeType: doc.mimeType || '', + filePath: doc.filePath || '', + uploadedAt: doc.uploadedAt + })) + } + } + } catch (error) { + console.error('Failed to get selection result:', error) + return { + success: false, + error: '선정결과 조회 중 오류가 발생했습니다.', + data: { + summary: '', + attachments: [] + } + } + } +} + // 견적 히스토리 조회 export async function getQuotationHistory(biddingId: number, vendorId: number) { try { diff --git a/lib/bidding/selection/bidding-info-card.tsx b/lib/bidding/selection/bidding-info-card.tsx index 8864e7db..b363f538 100644 --- a/lib/bidding/selection/bidding-info-card.tsx +++ b/lib/bidding/selection/bidding-info-card.tsx @@ -56,7 +56,7 @@ export function BiddingInfoCard({ bidding }: BiddingInfoCardProps) { 입찰유형 </label> <div className="text-sm font-medium"> - {bidding.isPublic ? '공개입찰' : '비공개입찰'} + {bidding.biddingType} </div> </div> diff --git a/lib/bidding/selection/bidding-item-table.tsx b/lib/bidding/selection/bidding-item-table.tsx new file mode 100644 index 00000000..c101f7e7 --- /dev/null +++ b/lib/bidding/selection/bidding-item-table.tsx @@ -0,0 +1,192 @@ +'use client' + +import * as React from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + getPRItemsForBidding, + getVendorPricesForBidding +} from '@/lib/bidding/detail/service' +import { formatNumber } from '@/lib/utils' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' + +interface BiddingItemTableProps { + biddingId: number +} + +export function BiddingItemTable({ biddingId }: BiddingItemTableProps) { + const [data, setData] = React.useState<{ + prItems: any[] + vendorPrices: any[] + }>({ prItems: [], vendorPrices: [] }) + const [loading, setLoading] = React.useState(true) + + React.useEffect(() => { + const loadData = async () => { + try { + setLoading(true) + const [prItems, vendorPrices] = await Promise.all([ + getPRItemsForBidding(biddingId), + getVendorPricesForBidding(biddingId) + ]) + console.log('prItems', prItems) + console.log('vendorPrices', vendorPrices) + setData({ prItems, vendorPrices }) + } catch (error) { + console.error('Failed to load bidding items:', error) + } finally { + setLoading(false) + } + } + + loadData() + }, [biddingId]) + + if (loading) { + return ( + <Card> + <CardHeader> + <CardTitle>응찰품목</CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-center justify-center py-8"> + <div className="text-sm text-muted-foreground">로딩 중...</div> + </div> + </CardContent> + </Card> + ) + } + + const { prItems, vendorPrices } = data + + // Calculate Totals + const totalQuantity = prItems.reduce((sum, item) => sum + Number(item.quantity || 0), 0) + const totalWeight = prItems.reduce((sum, item) => sum + Number(item.totalWeight || 0), 0) + const totalTargetAmount = prItems.reduce((sum, item) => sum + Number(item.targetAmount || 0), 0) + + // Calculate Vendor Totals + const vendorTotals = vendorPrices.map(vendor => { + const total = vendor.itemPrices.reduce((sum: number, item: any) => sum + Number(item.amount || 0), 0) + return { + companyId: vendor.companyId, + totalAmount: total + } + }) + + return ( + <Card> + <CardHeader> + <CardTitle>응찰품목</CardTitle> + </CardHeader> + <CardContent> + <ScrollArea className="w-full whitespace-nowrap rounded-md border"> + <div className="w-max min-w-full"> + <table className="w-full caption-bottom text-sm"> + <thead className="[&_tr]:border-b"> + {/* Header Row 1: Base Info + Vendor Groups */} + <tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>자재번호</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>자재내역</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>자재내역상세</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>구매단위</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>수량</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>단위</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>총중량</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>중량단위</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>내정단가</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>내정액</th> + <th className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r" rowSpan={2}>통화</th> + + {vendorPrices.map((vendor) => ( + <th key={vendor.companyId} colSpan={4} className="h-12 px-4 text-center align-middle font-medium text-muted-foreground border-r bg-muted/20"> + {vendor.companyName} + </th> + ))} + </tr> + {/* Header Row 2: Vendor Sub-columns */} + <tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"> + {vendorPrices.map((vendor) => ( + <React.Fragment key={vendor.companyId}> + <th className="h-10 px-2 text-center align-middle font-medium text-muted-foreground border-r bg-muted/10">단가</th> + <th className="h-10 px-2 text-center align-middle font-medium text-muted-foreground border-r bg-muted/10">총액</th> + <th className="h-10 px-2 text-center align-middle font-medium text-muted-foreground border-r bg-muted/10">통화</th> + <th className="h-10 px-2 text-center align-middle font-medium text-muted-foreground border-r bg-muted/10">내정액(%)</th> + </React.Fragment> + ))} + </tr> + </thead> + <tbody className="[&_tr:last-child]:border-0"> + {/* Summary Row */} + <tr className="border-b transition-colors hover:bg-muted/50 bg-muted/30 font-semibold"> + <td className="p-4 align-middle text-center border-r" colSpan={4}>합계</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(totalQuantity)}</td> + <td className="p-4 align-middle text-center border-r">-</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(totalWeight)}</td> + <td className="p-4 align-middle text-center border-r">-</td> + <td className="p-4 align-middle text-center border-r">-</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(totalTargetAmount)}</td> + <td className="p-4 align-middle text-center border-r">KRW</td> + + {vendorPrices.map((vendor) => { + const vTotal = vendorTotals.find(t => t.companyId === vendor.companyId)?.totalAmount || 0 + const ratio = totalTargetAmount > 0 ? (vTotal / totalTargetAmount) * 100 : 0 + return ( + <React.Fragment key={vendor.companyId}> + <td className="p-4 align-middle text-center border-r">-</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(vTotal)}</td> + <td className="p-4 align-middle text-center border-r">{vendor.currency}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(ratio, 0)}%</td> + </React.Fragment> + ) + })} + </tr> + + {/* Data Rows */} + {prItems.map((item) => ( + <tr key={item.id} className="border-b transition-colors hover:bg-muted/50"> + <td className="p-4 align-middle border-r">{item.materialNumber}</td> + <td className="p-4 align-middle border-r min-w-[150px]">{item.materialInfo}</td> + <td className="p-4 align-middle border-r min-w-[150px]">{item.specification}</td> + <td className="p-4 align-middle text-center border-r">{item.purchaseUnit}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(item.quantity)}</td> + <td className="p-4 align-middle text-center border-r">{item.quantityUnit}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(item.totalWeight)}</td> + <td className="p-4 align-middle text-center border-r">{item.weightUnit}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(item.targetUnitPrice)}</td> + <td className="p-4 align-middle text-right border-r">{formatNumber(item.targetAmount)}</td> + <td className="p-4 align-middle text-center border-r">{item.currency}</td> + + {vendorPrices.map((vendor) => { + const bidItem = vendor.itemPrices.find((p: any) => p.prItemId === item.id) + const bidAmount = bidItem ? bidItem.amount : 0 + const targetAmt = Number(item.targetAmount || 0) + const ratio = targetAmt > 0 && bidAmount > 0 ? (bidAmount / targetAmt) * 100 : 0 + + return ( + <React.Fragment key={vendor.companyId}> + <td className="p-4 align-middle text-right border-r bg-muted/5"> + {bidItem ? formatNumber(bidItem.unitPrice) : '-'} + </td> + <td className="p-4 align-middle text-right border-r bg-muted/5"> + {bidItem ? formatNumber(bidItem.amount) : '-'} + </td> + <td className="p-4 align-middle text-center border-r bg-muted/5"> + {vendor.currency} + </td> + <td className="p-4 align-middle text-right border-r bg-muted/5"> + {bidItem && ratio > 0 ? `${formatNumber(ratio, 0)}%` : '-'} + </td> + </React.Fragment> + ) + })} + </tr> + ))} + </tbody> + </table> + </div> + <ScrollBar orientation="horizontal" /> + </ScrollArea> + </CardContent> + </Card> + ) +} + diff --git a/lib/bidding/selection/bidding-selection-detail-content.tsx b/lib/bidding/selection/bidding-selection-detail-content.tsx index 45d5d402..887498dc 100644 --- a/lib/bidding/selection/bidding-selection-detail-content.tsx +++ b/lib/bidding/selection/bidding-selection-detail-content.tsx @@ -5,6 +5,7 @@ import { Bidding } from '@/db/schema' import { BiddingInfoCard } from './bidding-info-card' import { SelectionResultForm } from './selection-result-form' import { VendorSelectionTable } from './vendor-selection-table' +import { BiddingItemTable } from './bidding-item-table' interface BiddingSelectionDetailContentProps { biddingId: number @@ -17,6 +18,9 @@ export function BiddingSelectionDetailContent({ }: BiddingSelectionDetailContentProps) { const [refreshKey, setRefreshKey] = React.useState(0) + // 입찰평가중 상태가 아니면 읽기 전용 + const isReadOnly = bidding.status !== 'evaluation_of_bidding' + const handleRefresh = React.useCallback(() => { setRefreshKey(prev => prev + 1) }, []) @@ -27,7 +31,7 @@ export function BiddingSelectionDetailContent({ <BiddingInfoCard bidding={bidding} /> {/* 선정결과 폼 */} - <SelectionResultForm biddingId={biddingId} onSuccess={handleRefresh} /> + <SelectionResultForm biddingId={biddingId} onSuccess={handleRefresh} readOnly={isReadOnly} /> {/* 업체선정 테이블 */} <VendorSelectionTable @@ -35,7 +39,12 @@ export function BiddingSelectionDetailContent({ biddingId={biddingId} bidding={bidding} onRefresh={handleRefresh} + readOnly={isReadOnly} /> + + {/* 응찰품목 테이블 */} + <BiddingItemTable biddingId={biddingId} /> + </div> ) } diff --git a/lib/bidding/selection/biddings-selection-table.tsx b/lib/bidding/selection/biddings-selection-table.tsx index c3990e7b..41225531 100644 --- a/lib/bidding/selection/biddings-selection-table.tsx +++ b/lib/bidding/selection/biddings-selection-table.tsx @@ -84,13 +84,13 @@ export function BiddingsSelectionTable({ promises }: BiddingsSelectionTableProps switch (rowAction.type) {
case "view":
// 상세 페이지로 이동
- // 입찰평가중일때만 상세보기 가능
- if (rowAction.row.original.status === 'evaluation_of_bidding') {
+ // 입찰평가중, 업체선정, 차수증가, 재입찰 상태일 때 상세보기 가능
+ if (['evaluation_of_bidding', 'vendor_selected', 'round_increase', 'rebidding'].includes(rowAction.row.original.status)) {
router.push(`/evcp/bid-selection/${rowAction.row.original.id}/detail`)
} else {
toast({
title: '접근 제한',
- description: '입찰평가중이 아닙니다.',
+ description: '상세보기가 불가능한 상태입니다.',
variant: 'destructive',
})
}
diff --git a/lib/bidding/selection/selection-result-form.tsx b/lib/bidding/selection/selection-result-form.tsx index 54687cc9..af6b8d43 100644 --- a/lib/bidding/selection/selection-result-form.tsx +++ b/lib/bidding/selection/selection-result-form.tsx @@ -9,8 +9,8 @@ import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { useToast } from '@/hooks/use-toast' -import { saveSelectionResult } from './actions' -import { Loader2, Save, FileText } from 'lucide-react' +import { saveSelectionResult, getSelectionResult } from './actions' +import { Loader2, Save, FileText, Download, X } from 'lucide-react' import { Dropzone, DropzoneZone, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription, DropzoneInput } from '@/components/ui/dropzone' const selectionResultSchema = z.object({ @@ -22,12 +22,25 @@ type SelectionResultFormData = z.infer<typeof selectionResultSchema> interface SelectionResultFormProps { biddingId: number onSuccess: () => void + readOnly?: boolean } -export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFormProps) { +interface AttachmentInfo { + id: number + fileName: string + originalFileName: string + fileSize: number + mimeType: string + filePath: string + uploadedAt: Date | null +} + +export function SelectionResultForm({ biddingId, onSuccess, readOnly = false }: SelectionResultFormProps) { const { toast } = useToast() const [isSubmitting, setIsSubmitting] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(true) const [attachmentFiles, setAttachmentFiles] = React.useState<File[]>([]) + const [existingAttachments, setExistingAttachments] = React.useState<AttachmentInfo[]>([]) const form = useForm<SelectionResultFormData>({ resolver: zodResolver(selectionResultSchema), @@ -36,10 +49,53 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor }, }) + // 기존 선정결과 로드 + React.useEffect(() => { + const loadSelectionResult = async () => { + setIsLoading(true) + try { + const result = await getSelectionResult(biddingId) + if (result.success && result.data) { + form.reset({ + summary: result.data.summary || '', + }) + if (result.data.attachments) { + setExistingAttachments(result.data.attachments) + } + } + } catch (error) { + console.error('Failed to load selection result:', error) + toast({ + title: '로드 실패', + description: '선정결과를 불러오는데 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsLoading(false) + } + } + + loadSelectionResult() + }, [biddingId, form, toast]) + const removeAttachmentFile = (index: number) => { setAttachmentFiles(prev => prev.filter((_, i) => i !== index)) } + const removeExistingAttachment = (id: number) => { + setExistingAttachments(prev => prev.filter(att => att.id !== id)) + } + + const downloadAttachment = (filePath: string, fileName: string) => { + // 파일 다운로드 (filePath가 절대 경로인 경우) + if (filePath.startsWith('http') || filePath.startsWith('/')) { + window.open(filePath, '_blank') + } else { + // 상대 경로인 경우 + window.open(`/api/files/${filePath}`, '_blank') + } + } + const onSubmit = async (data: SelectionResultFormData) => { setIsSubmitting(true) try { @@ -74,6 +130,22 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor } } + if (isLoading) { + return ( + <Card> + <CardHeader> + <CardTitle>선정결과</CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-center justify-center py-8"> + <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> + <span className="ml-2 text-sm text-muted-foreground">로딩 중...</span> + </div> + </CardContent> + </Card> + ) + } + return ( <Card> <CardHeader> @@ -94,6 +166,7 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor placeholder="선정결과에 대한 요약을 입력해주세요..." className="min-h-[120px]" {...field} + disabled={readOnly} /> </FormControl> <FormMessage /> @@ -104,35 +177,83 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor {/* 첨부파일 */} <div className="space-y-4"> <FormLabel>첨부파일</FormLabel> - <Dropzone - maxSize={10 * 1024 * 1024} // 10MB - onDropAccepted={(files) => { - const newFiles = Array.from(files) - setAttachmentFiles(prev => [...prev, ...newFiles]) - }} - onDropRejected={() => { - toast({ - title: "파일 업로드 거부", - description: "파일 크기 및 형식을 확인해주세요.", - variant: "destructive", - }) - }} - > - <DropzoneZone> - <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" /> - <DropzoneTitle className="text-lg font-medium"> - 파일을 드래그하거나 클릭하여 업로드 - </DropzoneTitle> - <DropzoneDescription className="text-sm text-muted-foreground"> - PDF, Word, Excel, 이미지 파일 (최대 10MB) - </DropzoneDescription> - </DropzoneZone> - <DropzoneInput /> - </Dropzone> + + {/* 기존 첨부파일 */} + {existingAttachments.length > 0 && ( + <div className="space-y-2"> + <h4 className="text-sm font-medium">기존 첨부파일</h4> + <div className="space-y-2"> + {existingAttachments.map((attachment) => ( + <div + key={attachment.id} + className="flex items-center justify-between p-3 bg-muted rounded-lg" + > + <div className="flex items-center gap-3"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <div> + <p className="text-sm font-medium">{attachment.originalFileName || attachment.fileName}</p> + <p className="text-xs text-muted-foreground"> + {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB + </p> + </div> + </div> + <div className="flex items-center gap-2"> + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => downloadAttachment(attachment.filePath, attachment.originalFileName || attachment.fileName)} + > + <Download className="h-4 w-4" /> + </Button> + {!readOnly && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => removeExistingAttachment(attachment.id)} + > + <X className="h-4 w-4" /> + </Button> + )} + </div> + </div> + ))} + </div> + </div> + )} + + {!readOnly && ( + <Dropzone + maxSize={10 * 1024 * 1024} // 10MB + onDropAccepted={(files) => { + const newFiles = Array.from(files) + setAttachmentFiles(prev => [...prev, ...newFiles]) + }} + onDropRejected={() => { + toast({ + title: "파일 업로드 거부", + description: "파일 크기 및 형식을 확인해주세요.", + variant: "destructive", + }) + }} + > + <DropzoneZone> + <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" /> + <DropzoneTitle className="text-lg font-medium"> + 파일을 드래그하거나 클릭하여 업로드 + </DropzoneTitle> + <DropzoneDescription className="text-sm text-muted-foreground"> + PDF, Word, Excel, 이미지 파일 (최대 10MB) + </DropzoneDescription> + </DropzoneZone> + <DropzoneInput /> + </Dropzone> + )} {attachmentFiles.length > 0 && ( <div className="space-y-2"> - <h4 className="text-sm font-medium">업로드된 파일</h4> + <h4 className="text-sm font-medium">새로 추가할 파일</h4> <div className="space-y-2"> {attachmentFiles.map((file, index) => ( <div @@ -148,14 +269,16 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor </p> </div> </div> - <Button - type="button" - variant="ghost" - size="sm" - onClick={() => removeAttachmentFile(index)} - > - 제거 - </Button> + {!readOnly && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => removeAttachmentFile(index)} + > + 제거 + </Button> + )} </div> ))} </div> @@ -164,13 +287,15 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor </div> {/* 저장 버튼 */} - <div className="flex justify-end"> - <Button type="submit" disabled={isSubmitting}> - {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - <Save className="mr-2 h-4 w-4" /> - 저장 - </Button> - </div> + {!readOnly && ( + <div className="flex justify-end"> + <Button type="submit" disabled={isSubmitting}> + {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + <Save className="mr-2 h-4 w-4" /> + 저장 + </Button> + </div> + )} </form> </Form> </CardContent> diff --git a/lib/bidding/selection/vendor-selection-table.tsx b/lib/bidding/selection/vendor-selection-table.tsx index 8570b5b6..40f13ec1 100644 --- a/lib/bidding/selection/vendor-selection-table.tsx +++ b/lib/bidding/selection/vendor-selection-table.tsx @@ -10,9 +10,10 @@ interface VendorSelectionTableProps { biddingId: number bidding: Bidding onRefresh: () => void + readOnly?: boolean } -export function VendorSelectionTable({ biddingId, bidding, onRefresh }: VendorSelectionTableProps) { +export function VendorSelectionTable({ biddingId, bidding, onRefresh, readOnly = false }: VendorSelectionTableProps) { const [vendors, setVendors] = React.useState<any[]>([]) const [loading, setLoading] = React.useState(true) @@ -59,6 +60,7 @@ export function VendorSelectionTable({ biddingId, bidding, onRefresh }: VendorSe vendors={vendors} onRefresh={onRefresh} onOpenSelectionReasonDialog={() => {}} + readOnly={readOnly} /> </CardContent> </Card> |
