diff options
Diffstat (limited to 'lib/general-contracts/detail/general-contract-items-table.tsx')
| -rw-r--r-- | lib/general-contracts/detail/general-contract-items-table.tsx | 292 |
1 files changed, 263 insertions, 29 deletions
diff --git a/lib/general-contracts/detail/general-contract-items-table.tsx b/lib/general-contracts/detail/general-contract-items-table.tsx index 1b9a1a06..ed1e5afb 100644 --- a/lib/general-contracts/detail/general-contract-items-table.tsx +++ b/lib/general-contracts/detail/general-contract-items-table.tsx @@ -20,15 +20,26 @@ import { Package, Plus, Trash2, + FileSpreadsheet, + Save, + LoaderIcon } from 'lucide-react' import { toast } from 'sonner' import { updateContractItems, getContractItems } from '../service' -import { Save, LoaderIcon } from 'lucide-react' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { ProjectSelector } from '@/components/ProjectSelector' +import { MaterialGroupSelectorDialogSingle } from '@/components/common/material/material-group-selector-dialog-single' +import { MaterialSearchItem } from '@/lib/material/material-group-service' interface ContractItem { id?: number + projectId?: number | null + projectName?: string + projectCode?: string itemCode: string itemInfo: string + materialGroupCode?: string + materialGroupDescription?: string specification: string quantity: number quantityUnit: string @@ -49,6 +60,9 @@ interface ContractItemsTableProps { onTotalAmountChange: (total: number) => void availableBudget?: number readOnly?: boolean + contractScope?: string // 계약확정범위 (단가/금액/물량) + deliveryType?: string // 납기종류 (단일납기/분할납기) + contractDeliveryDate?: string // 기본정보의 계약납기일 } // 통화 목록 @@ -66,12 +80,28 @@ export function ContractItemsTable({ onItemsChange, onTotalAmountChange, availableBudget = 0, - readOnly = false + readOnly = false, + contractScope = '', + deliveryType = '', + contractDeliveryDate = '' }: ContractItemsTableProps) { + // 계약확정범위에 따른 필드 활성화/비활성화 + const isQuantityDisabled = contractScope === '단가' || contractScope === '물량' + const isTotalAmountDisabled = contractScope === '단가' || contractScope === '물량' + // 단일납기인 경우 납기일 필드 비활성화 및 기본값 설정 + const isDeliveryDateDisabled = deliveryType === '단일납기' const [localItems, setLocalItems] = React.useState<ContractItem[]>(items) const [isSaving, setIsSaving] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) const [isEnabled, setIsEnabled] = React.useState(true) + const [showBatchInputDialog, setShowBatchInputDialog] = React.useState(false) + const [batchInputData, setBatchInputData] = React.useState({ + quantity: '', + quantityUnit: 'EA', + contractDeliveryDate: '', + contractCurrency: 'KRW', + contractUnitPrice: '' + }) // 초기 데이터 로드 React.useEffect(() => { @@ -79,21 +109,41 @@ export function ContractItemsTable({ 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[] + const formattedItems = fetchedItems.map(item => { + // itemInfo에서 자재그룹 정보 파싱 (형식: "자재그룹코드 / 자재그룹명") + let materialGroupCode = '' + let materialGroupDescription = '' + if (item.itemInfo) { + const parts = item.itemInfo.split(' / ') + if (parts.length >= 2) { + materialGroupCode = parts[0].trim() + materialGroupDescription = parts.slice(1).join(' / ').trim() + } else if (parts.length === 1) { + materialGroupCode = parts[0].trim() + } + } + + return { + id: item.id, + projectId: item.projectId || null, + projectName: item.projectName || undefined, + projectCode: item.projectCode || undefined, + itemCode: item.itemCode || '', + itemInfo: item.itemInfo || '', + materialGroupCode: materialGroupCode || undefined, + materialGroupDescription: materialGroupDescription || undefined, + 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) { @@ -176,8 +226,11 @@ export function ContractItemsTable({ // 행 추가 const addRow = () => { const newItem: ContractItem = { + projectId: null, itemCode: '', itemInfo: '', + materialGroupCode: '', + materialGroupDescription: '', specification: '', quantity: 0, quantityUnit: 'EA', // 기본 수량 단위 @@ -218,6 +271,43 @@ export function ContractItemsTable({ onItemsChange(updatedItems) } + // 일괄입력 적용 + const applyBatchInput = () => { + if (localItems.length === 0) { + toast.error('품목이 없습니다. 먼저 품목을 추가해주세요.') + return + } + + const updatedItems = localItems.map(item => { + const updatedItem = { ...item } + + if (batchInputData.quantity) { + updatedItem.quantity = parseFloat(batchInputData.quantity) || 0 + } + if (batchInputData.quantityUnit) { + updatedItem.quantityUnit = batchInputData.quantityUnit + } + if (batchInputData.contractDeliveryDate) { + updatedItem.contractDeliveryDate = batchInputData.contractDeliveryDate + } + if (batchInputData.contractCurrency) { + updatedItem.contractCurrency = batchInputData.contractCurrency + } + if (batchInputData.contractUnitPrice) { + updatedItem.contractUnitPrice = parseFloat(batchInputData.contractUnitPrice) || 0 + // 단가가 변경되면 계약금액도 재계산 + updatedItem.contractAmount = updatedItem.contractUnitPrice * updatedItem.quantity + } + + return updatedItem + }) + + setLocalItems(updatedItems) + onItemsChange(updatedItems) + setShowBatchInputDialog(false) + toast.success('일괄입력이 적용되었습니다.') + } + // 통화 포맷팅 const formatCurrency = (amount: number, currency: string = 'KRW') => { @@ -292,6 +382,104 @@ export function ContractItemsTable({ <Plus className="w-4 h-4" /> 행 추가 </Button> + <Dialog open={showBatchInputDialog} onOpenChange={setShowBatchInputDialog}> + <DialogTrigger asChild> + <Button + variant="outline" + size="sm" + disabled={!isEnabled || localItems.length === 0} + className="flex items-center gap-2" + > + <FileSpreadsheet className="w-4 h-4" /> + 일괄입력 + </Button> + </DialogTrigger> + <DialogContent className="max-w-md"> + <DialogHeader> + <DialogTitle>품목 정보 일괄입력</DialogTitle> + </DialogHeader> + <div className="space-y-4 py-4"> + <div className="flex flex-col gap-2"> + <Label htmlFor="batch-quantity">수량</Label> + <Input + id="batch-quantity" + type="number" + value={batchInputData.quantity} + onChange={(e) => setBatchInputData(prev => ({ ...prev, quantity: e.target.value }))} + placeholder="수량 입력 (선택사항)" + /> + </div> + <div className="flex flex-col gap-2"> + <Label htmlFor="batch-quantity-unit">수량단위</Label> + <Select + value={batchInputData.quantityUnit} + onValueChange={(value) => setBatchInputData(prev => ({ ...prev, quantityUnit: value }))} + > + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {QUANTITY_UNITS.map((unit) => ( + <SelectItem key={unit} value={unit}> + {unit} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + <div className="flex flex-col gap-2"> + <Label htmlFor="batch-delivery-date">계약납기일</Label> + <Input + id="batch-delivery-date" + type="date" + value={batchInputData.contractDeliveryDate} + onChange={(e) => setBatchInputData(prev => ({ ...prev, contractDeliveryDate: e.target.value }))} + /> + </div> + <div className="flex flex-col gap-2"> + <Label htmlFor="batch-currency">계약통화</Label> + <Select + value={batchInputData.contractCurrency} + onValueChange={(value) => setBatchInputData(prev => ({ ...prev, contractCurrency: value }))} + > + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {CURRENCIES.map((currency) => ( + <SelectItem key={currency} value={currency}> + {currency} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + <div className="flex flex-col gap-2"> + <Label htmlFor="batch-unit-price">계약단가</Label> + <Input + id="batch-unit-price" + type="number" + value={batchInputData.contractUnitPrice} + onChange={(e) => setBatchInputData(prev => ({ ...prev, contractUnitPrice: e.target.value }))} + placeholder="계약단가 입력 (선택사항)" + /> + </div> + <div className="flex justify-end gap-2 pt-4"> + <Button + variant="outline" + onClick={() => setShowBatchInputDialog(false)} + > + 취소 + </Button> + <Button + onClick={applyBatchInput} + > + 적용 + </Button> + </div> + </div> + </DialogContent> + </Dialog> <Button variant="outline" size="sm" @@ -322,8 +510,8 @@ export function ContractItemsTable({ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4"> <div className="space-y-1"> <Label className="text-sm font-medium">총 계약금액</Label> - <div className="text-lg font-bold text-primary"> - {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')} + <div className={`text-lg font-bold ${isTotalAmountDisabled ? 'text-gray-400' : 'text-primary'}`}> + {isTotalAmountDisabled ? '-' : formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')} </div> </div> <div className="space-y-1"> @@ -364,8 +552,10 @@ export function ContractItemsTable({ /> )} </TableHead> + <TableHead className="px-3 py-3 font-semibold">프로젝트</TableHead> <TableHead className="px-3 py-3 font-semibold">품목코드 (PKG No.)</TableHead> - <TableHead className="px-3 py-3 font-semibold">Item 정보 (자재그룹 / 자재코드)</TableHead> + <TableHead className="px-3 py-3 font-semibold">자재그룹</TableHead> + <TableHead className="px-3 py-3 font-semibold">자재내역(자재그룹명)</TableHead> <TableHead className="px-3 py-3 font-semibold">규격</TableHead> <TableHead className="px-3 py-3 font-semibold text-right">수량</TableHead> <TableHead className="px-3 py-3 font-semibold">수량단위</TableHead> @@ -393,6 +583,21 @@ export function ContractItemsTable({ </TableCell> <TableCell className="px-3 py-3"> {readOnly ? ( + <span className="text-sm">{item.projectCode && item.projectName ? `${item.projectCode} - ${item.projectName}` : '-'}</span> + ) : ( + <ProjectSelector + selectedProjectId={item.projectId || undefined} + onProjectSelect={(project) => { + updateItem(index, 'projectId', project.id) + updateItem(index, 'projectName', project.projectName) + updateItem(index, 'projectCode', project.projectCode) + }} + placeholder="프로젝트 선택" + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( <span className="text-sm">{item.itemCode || '-'}</span> ) : ( <Input @@ -406,13 +611,42 @@ export function ContractItemsTable({ </TableCell> <TableCell className="px-3 py-3"> {readOnly ? ( - <span className="text-sm">{item.itemInfo || '-'}</span> + <span className="text-sm">{item.materialGroupCode || '-'}</span> + ) : ( + <MaterialGroupSelectorDialogSingle + triggerLabel={item.materialGroupCode || "자재그룹 선택"} + triggerVariant="outline" + selectedMaterial={item.materialGroupCode ? { + materialGroupCode: item.materialGroupCode, + materialGroupDescription: item.materialGroupDescription || '', + displayText: `${item.materialGroupCode} - ${item.materialGroupDescription || ''}` + } : null} + onMaterialSelect={(material) => { + if (material) { + updateItem(index, 'materialGroupCode', material.materialGroupCode) + updateItem(index, 'materialGroupDescription', material.materialGroupDescription) + updateItem(index, 'itemInfo', `${material.materialGroupCode} / ${material.materialGroupDescription}`) + } else { + updateItem(index, 'materialGroupCode', '') + updateItem(index, 'materialGroupDescription', '') + updateItem(index, 'itemInfo', '') + } + }} + title="자재그룹 선택" + description="자재그룹을 검색하고 선택해주세요." + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.materialGroupDescription || item.itemInfo || '-'}</span> ) : ( <Input - value={item.itemInfo} - onChange={(e) => updateItem(index, 'itemInfo', e.target.value)} - placeholder="Item 정보" - className="h-8 text-sm" + value={item.materialGroupDescription || item.itemInfo || ''} + onChange={(e) => updateItem(index, 'materialGroupDescription', e.target.value)} + placeholder="자재그룹명" + className="h-8 text-sm bg-muted/50" + readOnly disabled={!isEnabled} /> )} @@ -440,7 +674,7 @@ export function ContractItemsTable({ onChange={(e) => updateItem(index, 'quantity', parseFloat(e.target.value) || 0)} className="h-8 text-sm text-right" placeholder="0" - disabled={!isEnabled} + disabled={!isEnabled || isQuantityDisabled} /> )} </TableCell> @@ -451,7 +685,7 @@ export function ContractItemsTable({ <Select value={item.quantityUnit} onValueChange={(value) => updateItem(index, 'quantityUnit', value)} - disabled={!isEnabled} + disabled={!isEnabled || isQuantityDisabled} > <SelectTrigger className="h-8 text-sm w-20"> <SelectValue /> @@ -476,7 +710,7 @@ export function ContractItemsTable({ onChange={(e) => updateItem(index, 'totalWeight', parseFloat(e.target.value) || 0)} className="h-8 text-sm text-right" placeholder="0" - disabled={!isEnabled} + disabled={!isEnabled || isQuantityDisabled} /> )} </TableCell> @@ -511,7 +745,7 @@ export function ContractItemsTable({ value={item.contractDeliveryDate} onChange={(e) => updateItem(index, 'contractDeliveryDate', e.target.value)} className="h-8 text-sm" - disabled={!isEnabled} + disabled={!isEnabled || isDeliveryDateDisabled} /> )} </TableCell> |
