summaryrefslogtreecommitdiff
path: root/lib/general-contracts/detail/general-contract-items-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/general-contracts/detail/general-contract-items-table.tsx')
-rw-r--r--lib/general-contracts/detail/general-contract-items-table.tsx292
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>