From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/general-contract-items-table.tsx | 602 +++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 lib/general-contracts_old/detail/general-contract-items-table.tsx (limited to 'lib/general-contracts_old/detail/general-contract-items-table.tsx') diff --git a/lib/general-contracts_old/detail/general-contract-items-table.tsx b/lib/general-contracts_old/detail/general-contract-items-table.tsx new file mode 100644 index 00000000..1b9a1a06 --- /dev/null +++ b/lib/general-contracts_old/detail/general-contract-items-table.tsx @@ -0,0 +1,602 @@ +'use client' + +import * as React from 'react' +import { Card, CardContent, CardHeader } from '@/components/ui/card' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Package, + Plus, + Trash2, +} from 'lucide-react' +import { toast } from 'sonner' +import { updateContractItems, getContractItems } from '../service' +import { Save, LoaderIcon } from 'lucide-react' + +interface ContractItem { + id?: number + itemCode: string + itemInfo: string + specification: string + quantity: number + quantityUnit: string + totalWeight: number + weightUnit: string + contractDeliveryDate: string + contractUnitPrice: number + contractAmount: number + contractCurrency: string + isSelected?: boolean + [key: string]: unknown +} + +interface ContractItemsTableProps { + contractId: number + items: ContractItem[] + onItemsChange: (items: ContractItem[]) => void + onTotalAmountChange: (total: number) => void + availableBudget?: number + readOnly?: boolean +} + +// 통화 목록 +const CURRENCIES = ["USD", "EUR", "KRW", "JPY", "CNY"]; + +// 수량 단위 목록 +const QUANTITY_UNITS = ["KG", "TON", "EA", "M", "M2", "M3", "L", "ML", "G", "SET", "PCS"]; + +// 중량 단위 목록 +const WEIGHT_UNITS = ["KG", "TON", "G", "LB", "OZ"]; + +export function ContractItemsTable({ + contractId, + items, + onItemsChange, + onTotalAmountChange, + availableBudget = 0, + readOnly = false +}: ContractItemsTableProps) { + const [localItems, setLocalItems] = React.useState(items) + const [isSaving, setIsSaving] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false) + const [isEnabled, setIsEnabled] = React.useState(true) + + // 초기 데이터 로드 + React.useEffect(() => { + const loadItems = async () => { + try { + setIsLoading(true) + const fetchedItems = await getContractItems(contractId) + const formattedItems = fetchedItems.map(item => ({ + id: item.id, + itemCode: item.itemCode || '', + itemInfo: item.itemInfo || '', + specification: item.specification || '', + quantity: Number(item.quantity) || 0, + quantityUnit: item.quantityUnit || 'EA', + totalWeight: Number(item.totalWeight) || 0, + weightUnit: item.weightUnit || 'KG', + contractDeliveryDate: item.contractDeliveryDate || '', + contractUnitPrice: Number(item.contractUnitPrice) || 0, + contractAmount: Number(item.contractAmount) || 0, + contractCurrency: item.contractCurrency || 'KRW', + isSelected: false + })) as ContractItem[] + setLocalItems(formattedItems as ContractItem[]) + onItemsChange(formattedItems as ContractItem[]) + } catch (error) { + console.error('Error loading contract items:', error) + // 기본 빈 배열로 설정 + setLocalItems([]) + onItemsChange([]) + } finally { + setIsLoading(false) + } + } + + loadItems() + }, [contractId, onItemsChange]) + + // 로컬 상태와 부모 상태 동기화 (초기 로드 후에는 부모 상태 우선) + React.useEffect(() => { + if (items.length > 0) { + setLocalItems(items) + } + }, [items]) + + const handleSaveItems = async () => { + try { + setIsSaving(true) + + // validation 체크 + const errors: string[] = [] + for (let index = 0; index < localItems.length; index++) { + const item = localItems[index] + if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`) + if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`) + if (!item.quantity || item.quantity <= 0) errors.push(`${index + 1}번째 품목의 수량`) + if (!item.contractUnitPrice || item.contractUnitPrice <= 0) errors.push(`${index + 1}번째 품목의 단가`) + if (!item.contractDeliveryDate) errors.push(`${index + 1}번째 품목의 납기일`) + } + + if (errors.length > 0) { + toast.error(`다음 항목을 입력해주세요: ${errors.join(', ')}`) + return + } + + await updateContractItems(contractId, localItems as any) + toast.success('품목정보가 저장되었습니다.') + } catch (error) { + console.error('Error saving contract items:', error) + toast.error('품목정보 저장 중 오류가 발생했습니다.') + } finally { + setIsSaving(false) + } + } + + // 총 금액 계산 + const totalAmount = localItems.reduce((sum, item) => sum + item.contractAmount, 0) + const totalQuantity = localItems.reduce((sum, item) => sum + item.quantity, 0) + const totalUnitPrice = localItems.reduce((sum, item) => sum + item.contractUnitPrice, 0) + const amountDifference = availableBudget - totalAmount + const budgetRatio = availableBudget > 0 ? (totalAmount / availableBudget) * 100 : 0 + + // 부모 컴포넌트에 총 금액 전달 + React.useEffect(() => { + onTotalAmountChange(totalAmount) + }, [totalAmount, onTotalAmountChange]) + + // 아이템 업데이트 + const updateItem = (index: number, field: keyof ContractItem, value: string | number | boolean | undefined) => { + const updatedItems = [...localItems] + updatedItems[index] = { ...updatedItems[index], [field]: value } + + // 단가나 수량이 변경되면 금액 자동 계산 + if (field === 'contractUnitPrice' || field === 'quantity') { + const item = updatedItems[index] + updatedItems[index].contractAmount = item.contractUnitPrice * item.quantity + } + + setLocalItems(updatedItems) + onItemsChange(updatedItems) + } + + // 행 추가 + const addRow = () => { + const newItem: ContractItem = { + itemCode: '', + itemInfo: '', + specification: '', + quantity: 0, + quantityUnit: 'EA', // 기본 수량 단위 + totalWeight: 0, + weightUnit: 'KG', // 기본 중량 단위 + contractDeliveryDate: '', + contractUnitPrice: 0, + contractAmount: 0, + contractCurrency: 'KRW', // 기본 통화 + isSelected: false + } + const updatedItems = [...localItems, newItem] + setLocalItems(updatedItems) + onItemsChange(updatedItems) + } + + // 선택된 행 삭제 + const deleteSelectedRows = () => { + const selectedIndices = localItems + .map((item, index) => item.isSelected ? index : -1) + .filter(index => index !== -1) + + if (selectedIndices.length === 0) { + toast.error("삭제할 행을 선택해주세요.") + return + } + + const updatedItems = localItems.filter((_, index) => !selectedIndices.includes(index)) + setLocalItems(updatedItems) + onItemsChange(updatedItems) + toast.success(`${selectedIndices.length}개 행이 삭제되었습니다.`) + } + + // 전체 선택/해제 + const toggleSelectAll = (checked: boolean) => { + const updatedItems = localItems.map(item => ({ ...item, isSelected: checked })) + setLocalItems(updatedItems) + onItemsChange(updatedItems) + } + + + // 통화 포맷팅 + const formatCurrency = (amount: number, currency: string = 'KRW') => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: currency, + }).format(amount) + } + + const allSelected = localItems.length > 0 && localItems.every(item => item.isSelected) + const someSelected = localItems.some(item => item.isSelected) + + if (isLoading) { + return ( + + + +
+ + 품목 정보 + (로딩 중...) +
+
+ +
+ + 품목 정보를 불러오는 중... +
+
+
+
+ ) + } + + return ( + + + +
+ + 품목 정보 + ({localItems.length}개 품목) +
+
+ + + + {/* 체크박스 */} +
+ setIsEnabled(checked as boolean)} + disabled={readOnly} + /> + 품목 정보 활성화 +
+ +
+
+ 총 금액: {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')} + 총 수량: {totalQuantity.toLocaleString()} +
+ {!readOnly && ( +
+ + + +
+ )} +
+ + {/* 요약 정보 */} +
+
+ +
+ {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')} +
+
+
+ +
+ {formatCurrency(availableBudget, localItems[0]?.contractCurrency || 'KRW')} +
+
+
+ +
= 0 ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(amountDifference, localItems[0]?.contractCurrency || 'KRW')} +
+
+
+ +
+ {budgetRatio.toFixed(1)}% +
+
+
+
+ + +
+ + + + + {!readOnly && ( + { + if (el) (el as HTMLInputElement & { indeterminate?: boolean }).indeterminate = someSelected && !allSelected + }} + onCheckedChange={toggleSelectAll} + disabled={!isEnabled} + /> + )} + + 품목코드 (PKG No.) + Item 정보 (자재그룹 / 자재코드) + 규격 + 수량 + 수량단위 + 총 중량 + 중량단위 + 계약납기일 + 계약단가 + 계약금액 + 계약통화 + + + + {localItems.map((item, index) => ( + + + {!readOnly && ( + + updateItem(index, 'isSelected', checked) + } + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.itemCode || '-'} + ) : ( + updateItem(index, 'itemCode', e.target.value)} + placeholder="품목코드" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.itemInfo || '-'} + ) : ( + updateItem(index, 'itemInfo', e.target.value)} + placeholder="Item 정보" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.specification || '-'} + ) : ( + updateItem(index, 'specification', e.target.value)} + placeholder="규격" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.quantity.toLocaleString()} + ) : ( + updateItem(index, 'quantity', parseFloat(e.target.value) || 0)} + className="h-8 text-sm text-right" + placeholder="0" + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.quantityUnit || '-'} + ) : ( + + )} + + + {readOnly ? ( + {item.totalWeight.toLocaleString()} + ) : ( + updateItem(index, 'totalWeight', parseFloat(e.target.value) || 0)} + className="h-8 text-sm text-right" + placeholder="0" + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.weightUnit || '-'} + ) : ( + + )} + + + {readOnly ? ( + {item.contractDeliveryDate || '-'} + ) : ( + updateItem(index, 'contractDeliveryDate', e.target.value)} + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + + + {readOnly ? ( + {item.contractUnitPrice.toLocaleString()} + ) : ( + updateItem(index, 'contractUnitPrice', parseFloat(e.target.value) || 0)} + className="h-8 text-sm text-right" + placeholder="0" + disabled={!isEnabled} + /> + )} + + +
+ {formatCurrency(item.contractAmount)} +
+
+ + {readOnly ? ( + {item.contractCurrency || '-'} + ) : ( + + )} + +
+ ))} +
+
+
+ + {/* 합계 정보 */} + {localItems.length > 0 && ( +
+ + +
+
+ 총 수량 + + {totalQuantity.toLocaleString()} {localItems[0]?.quantityUnit || 'KG'} + +
+
+ 총 단가 + + {formatCurrency(totalUnitPrice, localItems[0]?.contractCurrency || 'KRW')} + +
+
+
+ 합계 금액 + + {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')} + +
+
+
+
+
+
+ )} +
+
+
+
+
+ ) +} -- cgit v1.2.3