diff options
Diffstat (limited to 'components/bidding/manage/bidding-items-editor.tsx')
| -rw-r--r-- | components/bidding/manage/bidding-items-editor.tsx | 271 |
1 files changed, 240 insertions, 31 deletions
diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index f0287ae4..208cf040 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -441,8 +441,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems materialGroupInfo: null, materialNumber: null, materialInfo: null, - priceUnit: null, - purchaseUnit: '1', + priceUnit: 1, + purchaseUnit: 'EA', materialWeight: null, wbsCode: null, wbsName: null, @@ -495,8 +495,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems prev.map((item) => { if (item.id === id) { const updatedItem = { ...item, ...updates } - // 내정단가, 수량, 중량, 구매단위가 변경되면 내정금액 재계산 - if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.purchaseUnit) { + // 내정단가, 수량, 중량, 가격단위가 변경되면 내정금액 재계산 + if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.priceUnit) { updatedItem.targetAmount = calculateTargetAmount(updatedItem) } return updatedItem @@ -533,20 +533,66 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems const calculateTargetAmount = (item: PRItemInfo): string => { const unitPrice = parseFloat(item.targetUnitPrice?.replace(/,/g, '') || '0') || 0 - const purchaseUnit = parseFloat(item.purchaseUnit || '1') || 1 + const priceUnit = parseFloat(item.priceUnit || '1') || 1 let amount = 0 if (quantityWeightMode === 'quantity') { const quantity = parseFloat(item.quantity || '0') || 0 - amount = (quantity / purchaseUnit) * unitPrice + amount = (quantity / priceUnit) * unitPrice } else { const weight = parseFloat(item.totalWeight || '0') || 0 - amount = (weight / purchaseUnit) * unitPrice + amount = (weight / priceUnit) * unitPrice } return Math.floor(amount).toString() } + // 합계 계산 함수들 + const calculateTotals = () => { + let quantityTotal = 0 + let weightTotal = 0 + let targetAmountTotal = 0 + let budgetAmountTotal = 0 + let actualAmountTotal = 0 + + items.forEach((item) => { + // 수량 합계 + if (item.quantity) { + quantityTotal += parseFloat(item.quantity) || 0 + } + + // 중량 합계 + if (item.totalWeight) { + weightTotal += parseFloat(item.totalWeight) || 0 + } + + // 내정금액 합계 + if (item.targetAmount) { + targetAmountTotal += parseFloat(item.targetAmount.replace(/,/g, '')) || 0 + } + + // 예산금액 합계 + if (item.budgetAmount) { + budgetAmountTotal += parseFloat(item.budgetAmount.replace(/,/g, '')) || 0 + } + + // 실적금액 합계 + if (item.actualAmount) { + actualAmountTotal += parseFloat(item.actualAmount.replace(/,/g, '')) || 0 + } + }) + + return { + quantityTotal, + weightTotal, + targetAmountTotal, + budgetAmountTotal, + actualAmountTotal, + } + } + + const totals = calculateTotals() + if (isLoading) { return ( <div className="flex items-center justify-center p-8"> @@ -572,14 +618,16 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">프로젝트코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">프로젝트명</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재그룹코드 <span className="text-red-500">*</span></th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재그룹명 <span className="text-red-500">*</span></th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재명</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량(중량) <span className="text-red-500">*</span></th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위 <span className="text-red-500">*</span></th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">가격단위</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">구매단위</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[100px]">자재순중량</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정단가</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정금액</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">내정통화</th> @@ -587,19 +635,120 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">예산통화</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">실적금액</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">실적통화</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">WBS코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">WBS명</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">코스트센터코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">코스트센터명</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">GL계정코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">GL계정명</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th> <th className="sticky right-0 z-10 bg-muted/50 border-l px-3 py-3 text-center text-xs font-medium min-w-[100px]"> 액션 </th> </tr> </thead> <tbody> + {/* 합계 행 */} + <tr className="bg-blue-50 border-y-2 border-blue-200 font-semibold"> + <td className="sticky left-0 z-10 bg-blue-50 border-r px-2 py-3 text-center"> + <span className="text-xs">합계</span> + </td> + <td className="sticky left-[50px] z-10 bg-blue-50 border-r px-3 py-3 text-center"> + <span className="text-xs">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs"> + {quantityWeightMode === 'quantity' + ? `${formatNumberWithCommas(totals.quantityTotal.toString())}` + : `${formatNumberWithCommas(totals.weightTotal.toString())}` + } + </span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs"> + {quantityWeightMode === 'quantity' + ? `${items[0]?.quantityUnit || 'EA'}` + : `${items[0]?.weightUnit || 'KG'}` + } + </span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs">{formatNumberWithCommas(totals.targetAmountTotal.toString())}</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs">{formatNumberWithCommas(totals.budgetAmountTotal.toString())}</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs">{formatNumberWithCommas(totals.actualAmountTotal.toString())}</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="sticky right-0 z-10 bg-blue-50 border-l px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + </tr> {items.map((item, index) => ( <tr key={item.id} className="border-t hover:bg-muted/30"> <td className="sticky left-0 z-10 bg-background border-r px-2 py-2 text-center"> @@ -617,10 +766,17 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <ProjectSelector selectedProjectId={item.projectId || null} onProjectSelect={(project) => { - updatePRItem(item.id, { - projectId: project.id, - projectInfo: project.projectName - }) + if (project) { + updatePRItem(item.id, { + projectId: project.id, + projectInfo: project.projectName + }) + } else { + updatePRItem(item.id, { + projectId: null, + projectInfo: null + }) + } }} placeholder="프로젝트 선택" /> @@ -633,14 +789,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems className="h-8 text-xs bg-muted/50" /> </td> - <td className="border-r px-3 py-2"> - <Input - placeholder="PR 번호" - value={item.prNumber || ''} - readOnly - className="h-8 text-xs bg-muted/50" - /> - </td> + <td className="border-r px-3 py-2"> {biddingType !== 'equipment' ? ( <ProcurementItemSelectorDialogSingle @@ -745,6 +894,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.quantity || ''} onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })} className="h-8 text-xs" + required /> ) : ( <Input @@ -754,6 +904,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.totalWeight || ''} onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })} className="h-8 text-xs" + required /> )} </td> @@ -762,6 +913,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Select value={item.quantityUnit || 'EA'} onValueChange={(value) => updatePRItem(item.id, { quantityUnit: value })} + required > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -779,6 +931,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Select value={item.weightUnit || 'KG'} onValueChange={(value) => updatePRItem(item.id, { weightUnit: value })} + required > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -797,9 +950,42 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems type="number" min="1" step="1" - placeholder="구매단위" - value={item.purchaseUnit || ''} - onChange={(e) => updatePRItem(item.id, { purchaseUnit: e.target.value })} + placeholder="가격단위" + value={item.priceUnit || ''} + onChange={(e) => updatePRItem(item.id, { priceUnit: e.target.value })} + className="h-8 text-xs" + /> + </td> + <td className="border-r px-3 py-2"> + <Select + value={item.purchaseUnit || 'EA'} + onValueChange={(value) => updatePRItem(item.id, { purchaseUnit: value })} + > + <SelectTrigger className="h-8 text-xs"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="EA">EA</SelectItem> + <SelectItem value="SET">SET</SelectItem> + <SelectItem value="LOT">LOT</SelectItem> + <SelectItem value="M">M</SelectItem> + <SelectItem value="M2">M²</SelectItem> + <SelectItem value="M3">M³</SelectItem> + <SelectItem value="KG">KG</SelectItem> + <SelectItem value="TON">TON</SelectItem> + <SelectItem value="G">G</SelectItem> + <SelectItem value="LB">LB</SelectItem> + </SelectContent> + </Select> + </td> + <td className="border-r px-3 py-2"> + <Input + type="number" + min="0" + step="0.01" + placeholder="자재순중량" + value={item.materialWeight || ''} + onChange={(e) => updatePRItem(item.id, { materialWeight: e.target.value })} className="h-8 text-xs" /> </td> @@ -888,9 +1074,23 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </Select> </td> <td className="border-r px-3 py-2"> + <Input + type="date" + value={item.requestedDeliveryDate || ''} + onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} + className="h-8 text-xs" + required + /> + </td> + <td className="border-r px-3 py-2"> <Button variant="outline" onClick={() => { + // 재클릭 시 기존 데이터 클리어 + updatePRItem(item.id, { + wbsCode: null, + wbsName: null + }) setSelectedItemForWbs(item.id) setWbsCodeDialogOpen(true) }} @@ -941,6 +1141,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Button variant="outline" onClick={() => { + // 재클릭 시 기존 데이터 클리어 + updatePRItem(item.id, { + costCenterCode: null, + costCenterName: null + }) setSelectedItemForCostCenter(item.id) setCostCenterDialogOpen(true) }} @@ -992,6 +1197,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Button variant="outline" onClick={() => { + // 재클릭 시 기존 데이터 클리어 + updatePRItem(item.id, { + glAccountCode: null, + glAccountName: null + }) setSelectedItemForGlAccount(item.id) setGlAccountDialogOpen(true) }} @@ -1039,11 +1249,10 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </td> <td className="border-r px-3 py-2"> <Input - type="date" - value={item.requestedDeliveryDate || ''} - onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} - className="h-8 text-xs" - required + placeholder="PR 번호" + value={item.prNumber || ''} + readOnly + className="h-8 text-xs bg-muted/50" /> </td> <td className="sticky right-0 z-10 bg-background border-l px-3 py-2"> |
