summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/bidding/manage/bidding-companies-editor.tsx1
-rw-r--r--db/schema/bidding.ts1
-rw-r--r--lib/bidding/actions.ts6
-rw-r--r--lib/bidding/detail/service.ts34
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx25
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-table.tsx24
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx89
-rw-r--r--lib/general-contracts/detail/general-contract-basic-info.tsx161
-rw-r--r--lib/general-contracts/detail/general-contract-items-table.tsx12
9 files changed, 225 insertions, 128 deletions
diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx
index a81f0063..a5ce1349 100644
--- a/components/bidding/manage/bidding-companies-editor.tsx
+++ b/components/bidding/manage/bidding-companies-editor.tsx
@@ -483,7 +483,6 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC
<p className="text-sm text-muted-foreground mt-1">
입찰에 참여하는 업체들을 관리합니다. 업체를 선택하면 하단에 담당자 목록이 표시됩니다.
</p>
- <p className="text-sm text-muted-foreground mt-1"> 단수 입찰의 경우 1개 업체만 등록 가능합니다. </p>
</div>
{!readonly && (
<Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2">
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts
index 8f9f8d84..ab9b3373 100644
--- a/db/schema/bidding.ts
+++ b/db/schema/bidding.ts
@@ -692,6 +692,7 @@ export const biddingStatusLabels = {
set_target_price: '내정가 산정',
bidding_opened: '입찰공고',
bidding_closed: '입찰마감',
+ approval_pending: '결재 진행중',
evaluation_of_bidding: '입찰평가중',
bidding_disposal: '유찰',
vendor_selected: '업체선정',
diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts
index c4c543d9..d0017413 100644
--- a/lib/bidding/actions.ts
+++ b/lib/bidding/actions.ts
@@ -613,16 +613,16 @@ export async function cancelDisposalAction(
}
}
- // 3. 입찰 상태를 입찰생성으로 변경
+ // 3. 입찰 상태를 입찰평가중으로 변경
await tx
.update(biddings)
.set({
- status: 'bidding_generated',
+ status: 'evaluation_of_bidding',
updatedAt: new Date(),
updatedBy: userName,
})
.where(eq(biddings.id, biddingId))
-
+
return {
success: true,
message: '유찰 취소가 완료되었습니다.'
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index b5a3cce8..6ab9270e 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -610,6 +610,40 @@ export async function updateBiddingDetailVendor(
}
}
+// 발주비율 취소 (발주비율을 null로 리셋하고 낙찰 상태 해제)
+export async function cancelAwardRatio(biddingCompanyId: number) {
+ try {
+ const result = await db.update(biddingCompanies)
+ .set({
+ awardRatio: null,
+ isWinner: false,
+ updatedAt: new Date(),
+ })
+ .where(eq(biddingCompanies.id, biddingCompanyId))
+ .returning({ biddingId: biddingCompanies.biddingId })
+
+ // 캐시 무효화
+ if (result.length > 0) {
+ const biddingId = result[0].biddingId
+ revalidateTag(`bidding-${biddingId}`)
+ revalidateTag('quotation-vendors')
+ revalidateTag('quotation-details')
+ revalidatePath(`/evcp/bid/${biddingId}`)
+ }
+
+ return {
+ success: true,
+ message: '발주비율이 성공적으로 취소되었습니다.',
+ }
+ } catch (error) {
+ console.error('Failed to cancel award ratio:', error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '발주비율 취소에 실패했습니다.'
+ }
+ }
+}
+
// 본입찰용 업체 추가
export async function createBiddingDetailVendor(
biddingId: number,
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
index 1a1b331e..6e5481f4 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx
@@ -24,6 +24,7 @@ interface BiddingDetailVendorEditDialogProps {
onSuccess: () => void
biddingAwardCount?: string // 낙찰수 정보 추가
biddingStatus?: string // 입찰 상태 정보 추가
+ allVendors?: QuotationVendor[] // 전체 벤더 목록 추가
}
export function BiddingDetailVendorEditDialog({
@@ -32,7 +33,8 @@ export function BiddingDetailVendorEditDialog({
onOpenChange,
onSuccess,
biddingAwardCount,
- biddingStatus
+ biddingStatus,
+ allVendors = []
}: BiddingDetailVendorEditDialogProps) {
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
@@ -42,6 +44,14 @@ export function BiddingDetailVendorEditDialog({
awardRatio: 0,
})
+ // 단수낙찰의 경우 이미 100%인 벤더가 있는지 확인
+ const hasWinnerWith100Percent = React.useMemo(() => {
+ if (biddingAwardCount === 'single') {
+ return allVendors.some(v => v.awardRatio === 100 && v.id !== vendor?.id)
+ }
+ return false
+ }, [allVendors, biddingAwardCount, vendor?.id])
+
// vendor가 변경되면 폼 데이터 업데이트
React.useEffect(() => {
if (vendor) {
@@ -135,7 +145,7 @@ export function BiddingDetailVendorEditDialog({
value={formData.awardRatio}
onChange={(e) => setFormData({ ...formData, awardRatio: Number(e.target.value) })}
placeholder="발주비율을 입력하세요"
- disabled={vendor?.isBiddingParticipated !== true || biddingAwardCount === 'single' || biddingStatus === 'vendor_selected'}
+ disabled={vendor?.isBiddingParticipated !== true || biddingAwardCount === 'single' || biddingStatus === 'vendor_selected' || hasWinnerWith100Percent}
/>
{vendor?.isBiddingParticipated !== true && (
<p className="text-sm text-muted-foreground">
@@ -152,15 +162,20 @@ export function BiddingDetailVendorEditDialog({
낙찰이 완료되어 발주비율을 수정할 수 없습니다.
</p>
)}
+ {hasWinnerWith100Percent && (
+ <p className="text-sm text-orange-600">
+ 단수 낙찰의 경우 이미 100% 발주비율이 설정된 업체가 있어 다른 업체의 발주비율을 수정할 수 없습니다.
+ </p>
+ )}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
취소
</Button>
- <Button
- onClick={handleEdit}
- disabled={isPending || vendor?.isBiddingParticipated !== true}
+ <Button
+ onClick={handleEdit}
+ disabled={isPending || vendor?.isBiddingParticipated !== true || hasWinnerWith100Percent}
>
산정
</Button>
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
index cfdab9c6..1fa116ab 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
@@ -66,16 +66,16 @@ const advancedFilterFields: DataTableAdvancedFilterField<QuotationVendor>[] = [
label: '견적금액',
type: 'number',
},
- {
- id: 'status',
- label: '상태',
- type: 'multi-select',
- options: [
- { label: '제출완료', value: 'submitted' },
- { label: '선정완료', value: 'selected' },
- { label: '미제출', value: 'pending' },
- ],
- },
+ {
+ id: 'invitationStatus',
+ label: '상태',
+ type: 'multi-select',
+ options: [
+ { label: '제출완료', value: 'bidding_submitted' },
+ { label: '선정완료', value: 'bidding_accepted' },
+ { label: '미제출', value: 'pending' },
+ ],
+ },
]
export function BiddingDetailVendorTableContent({
@@ -201,6 +201,7 @@ export function BiddingDetailVendorTableContent({
userId={userId}
onOpenAwardDialog={() => setIsAwardDialogOpen(true)}
onSuccess={onRefresh}
+ winnerVendor={vendors.find(v => v.awardRatio === 100)}
/>
</DataTableAdvancedToolbar>
</DataTable>
@@ -210,8 +211,9 @@ export function BiddingDetailVendorTableContent({
open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
onSuccess={onRefresh}
- biddingAwardCount={bidding.awardCount}
+ biddingAwardCount={bidding.awardCount || undefined}
biddingStatus={bidding.status}
+ allVendors={vendors}
/>
<BiddingAwardDialog
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
index f2c23de9..c1d59677 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -6,7 +6,7 @@ import { useTransition } from "react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Plus, Send, RotateCcw, XCircle, Trophy, FileText, DollarSign, RotateCw } from "lucide-react"
-import { registerBidding, markAsDisposal, createRebidding } from "@/lib/bidding/detail/service"
+import { registerBidding, markAsDisposal, createRebidding, cancelAwardRatio } from "@/lib/bidding/detail/service"
import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from "@/lib/bidding/pre-quote/service"
import { increaseRoundOrRebid } from "@/lib/bidding/service"
@@ -14,6 +14,7 @@ import { BiddingDetailVendorCreateDialog } from "../../../../components/bidding/
import { BiddingDocumentUploadDialog } from "./bidding-document-upload-dialog"
import { Bidding } from "@/db/schema"
import { useToast } from "@/hooks/use-toast"
+import { QuotationVendor } from "@/lib/bidding/detail/service"
interface BiddingDetailVendorToolbarActionsProps {
biddingId: number
@@ -21,6 +22,7 @@ interface BiddingDetailVendorToolbarActionsProps {
userId: string
onOpenAwardDialog: () => void
onSuccess: () => void
+ winnerVendor?: QuotationVendor | null // 100% 낙찰된 벤더
}
export function BiddingDetailVendorToolbarActions({
@@ -28,7 +30,8 @@ export function BiddingDetailVendorToolbarActions({
bidding,
userId,
onOpenAwardDialog,
- onSuccess
+ onSuccess,
+ winnerVendor
}: BiddingDetailVendorToolbarActionsProps) {
const router = useRouter()
const { toast } = useToast()
@@ -39,6 +42,7 @@ export function BiddingDetailVendorToolbarActions({
const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false)
const [selectedVendors, setSelectedVendors] = React.useState<any[]>([])
const [isRoundIncreaseDialogOpen, setIsRoundIncreaseDialogOpen] = React.useState(false)
+ const [isCancelAwardDialogOpen, setIsCancelAwardDialogOpen] = React.useState(false)
// 본입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회
React.useEffect(() => {
@@ -178,26 +182,51 @@ export function BiddingDetailVendorToolbarActions({
})
}
+ const handleCancelAward = () => {
+ if (!winnerVendor) return
+
+ startTransition(async () => {
+ const result = await cancelAwardRatio(winnerVendor.id)
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: result.message,
+ })
+ setIsCancelAwardDialogOpen(false)
+ onSuccess()
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "발주비율 취소 중 오류가 발생했습니다.",
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
const handleRoundIncreaseWithNavigation = () => {
startTransition(async () => {
const result = await increaseRoundOrRebid(bidding.id, userId, 'round_increase')
if (result.success) {
+ const successResult = result as { success: true; message: string; biddingId: number; biddingNumber: string }
toast({
title: "성공",
- description: result.message,
+ description: successResult.message,
})
// 새로 생성된 입찰의 상세 페이지로 이동
- if (result.biddingId) {
- router.push(`/evcp/bid/${result.biddingId}`)
+ if (successResult.biddingId) {
+ router.push(`/evcp/bid/${successResult.biddingId}/info`)
} else {
- router.push(`/evcp/bid`)
+ router.push(`/evcp/bid/${biddingId}/info`)
}
onSuccess()
} else {
+ const errorResult = result as { success: false; error: string }
toast({
title: "오류",
- description: result.error || "차수증가 중 오류가 발생했습니다.",
+ description: errorResult.error || "차수증가 중 오류가 발생했습니다.",
variant: 'destructive',
})
}
@@ -244,6 +273,19 @@ export function BiddingDetailVendorToolbarActions({
</Button>
</>
)}
+
+ {/* 발주비율 취소: 100% 낙찰된 벤더가 있는 경우 */}
+ {winnerVendor && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsCancelAwardDialogOpen(true)}
+ disabled={isPending}
+ >
+ <RotateCcw className="mr-2 h-4 w-4" />
+ 발주비율 취소
+ </Button>
+ )}
{/* 구분선 */}
{(bidding.status === 'bidding_generated' ||
bidding.status === 'bidding_disposal') && (
@@ -307,6 +349,39 @@ export function BiddingDetailVendorToolbarActions({
</DialogContent>
</Dialog>
+ {/* 발주비율 취소 확인 다이얼로그 */}
+ <Dialog open={isCancelAwardDialogOpen} onOpenChange={setIsCancelAwardDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>발주비율 취소 확인</DialogTitle>
+ <DialogDescription>
+ {winnerVendor && (
+ <>
+ <strong>{winnerVendor.vendorName}</strong> 업체의 발주비율(100%)을 취소하시겠습니까?
+ <br />
+ 취소 후 다른 업체의 발주비율을 설정할 수 있습니다.
+ </>
+ )}
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setIsCancelAwardDialogOpen(false)}
+ >
+ 아니오
+ </Button>
+ <Button
+ variant="destructive"
+ onClick={handleCancelAward}
+ disabled={isPending}
+ >
+ 취소하기
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
</>
)
}
diff --git a/lib/general-contracts/detail/general-contract-basic-info.tsx b/lib/general-contracts/detail/general-contract-basic-info.tsx
index 54c083ff..fc147b59 100644
--- a/lib/general-contracts/detail/general-contract-basic-info.tsx
+++ b/lib/general-contracts/detail/general-contract-basic-info.tsx
@@ -1385,9 +1385,25 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
</div>
{/* 사외업체 야드투입 */}
- <div className="space-y-4 col-span-2">
- <Label className="text-base font-medium">사외업체 야드투입</Label>
+ <div className="space-y-4 grid grid-cols-2 col-span-2">
+ {/* 연동제적용 */}
+ <div className="space-y-4 flex-1">
+ <Label className="text-base font-medium">연동제적용</Label>
+ <div className="space-y-2">
+ <Select value={formData.interlockingSystem} onValueChange={(value) => setFormData(prev => ({ ...prev, interlockingSystem: value }))}>
+ <SelectTrigger>
+ <SelectValue placeholder="연동제적용을 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="Y">Y</SelectItem>
+ <SelectItem value="N">N</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
<div className="flex items-center space-x-4">
+ <div className="space-y-4 flex-1">
+ <Label className="text-base font-medium">사외업체 야드투입</Label>
<div className="flex items-center space-x-2">
<input
type="radio"
@@ -1456,12 +1472,14 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
</div>
</DialogContent>
</Dialog>
+ </div>
</div>
{/* 계약성립조건 */}
- <div className="space-y-4 col-span-2">
- <Label className="text-base font-medium">계약성립조건</Label>
+ <div className="space-y-4 grid grid-cols-2 col-span-2">
<div className="space-y-3">
+ <Label className="text-base font-medium">계약성립조건</Label>
+
<div className="flex items-center space-x-2">
<input
type="checkbox"
@@ -1503,106 +1521,49 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="establishmentOther">기타</Label>
</div>
</div>
- </div>
-
- {/* 연동제적용 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">연동제적용</Label>
- <div className="space-y-2">
- <Select value={formData.interlockingSystem} onValueChange={(value) => setFormData(prev => ({ ...prev, interlockingSystem: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="연동제적용을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="Y">Y</SelectItem>
- <SelectItem value="N">N</SelectItem>
- </SelectContent>
- </Select>
+ {/* 계약해지조건 */}
+ <div className="space-y-4 flex-1">
+ <Label className="text-base font-medium">계약해지조건</Label>
+ <div className="space-y-3">
+ <div className="flex items-center space-x-2">
+ <input
+ type="checkbox"
+ id="standardTermination"
+ checked={formData.contractTerminationConditions.standardTermination}
+ onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, standardTermination: e.target.checked } }))}
+ className="rounded"
+ />
+ <Label htmlFor="standardTermination">표준 계약해지조건</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <input
+ type="checkbox"
+ id="projectNotAwarded"
+ checked={formData.contractTerminationConditions.projectNotAwarded}
+ onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, projectNotAwarded: e.target.checked } }))}
+ className="rounded"
+ />
+ <Label htmlFor="projectNotAwarded">프로젝트 미수주 시</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <input
+ type="checkbox"
+ id="terminationOther"
+ checked={formData.contractTerminationConditions.other}
+ onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, other: e.target.checked } }))}
+ className="rounded"
+ />
+ <Label htmlFor="terminationOther">기타</Label>
+ </div>
+ </div>
</div>
</div>
- {/* 필수문서동의 */}
- {/* <div className="space-y-4">
- <Label className="text-base font-medium">필수문서동의</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="technicalDataAgreement"
- checked={formData.mandatoryDocuments.technicalDataAgreement}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, technicalDataAgreement: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="technicalDataAgreement">기술자료제공동의서</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="nda"
- checked={formData.mandatoryDocuments.nda}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, nda: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="nda">비밀유지계약서(NDA)</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="basicCompliance"
- checked={formData.mandatoryDocuments.basicCompliance}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, basicCompliance: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="basicCompliance">기본준수서약서</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="safetyHealthAgreement"
- checked={formData.mandatoryDocuments.safetyHealthAgreement}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, safetyHealthAgreement: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="safetyHealthAgreement">안전보건관리 약정서</Label>
- </div>
- </div>
- </div> */}
+ {/* 연동제적용과 계약해지조건을 같은 줄에 배치 */}
+ <div className="flex gap-8">
+
+
- {/* 계약해지조건 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">계약해지조건</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="standardTermination"
- checked={formData.contractTerminationConditions.standardTermination}
- onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, standardTermination: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="standardTermination">표준 계약해지조건</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="projectNotAwarded"
- checked={formData.contractTerminationConditions.projectNotAwarded}
- onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, projectNotAwarded: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="projectNotAwarded">프로젝트 미수주 시</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="terminationOther"
- checked={formData.contractTerminationConditions.other}
- onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, other: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="terminationOther">기타</Label>
- </div>
- </div>
</div>
<div className="space-y-2 col-span-2">
diff --git a/lib/general-contracts/detail/general-contract-items-table.tsx b/lib/general-contracts/detail/general-contract-items-table.tsx
index ed1e5afb..bda2901e 100644
--- a/lib/general-contracts/detail/general-contract-items-table.tsx
+++ b/lib/general-contracts/detail/general-contract-items-table.tsx
@@ -664,7 +664,7 @@ export function ContractItemsTable({
/>
)}
</TableCell>
- <TableCell className="px-3 py-3">
+ {/* <TableCell className="px-3 py-3">
{readOnly ? (
<span className="text-sm text-right">{item.quantity.toLocaleString()}</span>
) : (
@@ -677,6 +677,16 @@ export function ContractItemsTable({
disabled={!isEnabled || isQuantityDisabled}
/>
)}
+ </TableCell> */}
+ <TableCell className="px-3 py-3">
+ <Input
+ type="number"
+ value={item.quantity}
+ onChange={(e) => updateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
+ className="h-8 text-sm text-right"
+ placeholder="0"
+ disabled={!isEnabled}
+ />
</TableCell>
<TableCell className="px-3 py-3">
{readOnly ? (