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 | 1102 |
1 files changed, 553 insertions, 549 deletions
diff --git a/lib/general-contracts/detail/general-contract-items-table.tsx b/lib/general-contracts/detail/general-contract-items-table.tsx index 23057cb7..5176c6ce 100644 --- a/lib/general-contracts/detail/general-contract-items-table.tsx +++ b/lib/general-contracts/detail/general-contract-items-table.tsx @@ -1,549 +1,553 @@ -'use client'
-
-import * as React from 'react'
-import { Card, CardContent, CardHeader, CardTitle } 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 {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@/components/ui/table'
-import {
- Package,
- Plus,
- Trash2,
- Calculator
-} from 'lucide-react'
-import { toast } from 'sonner'
-import { updateContractItems, getContractItems } from '../service'
-import { Save, LoaderIcon } from 'lucide-react'
-
-interface ContractItem {
- id?: number
- project: string
- itemCode: string
- itemInfo: string
- specification: string
- quantity: number
- quantityUnit: string
- contractDeliveryDate: string
- contractUnitPrice: number
- contractAmount: number
- contractCurrency: string
- isSelected?: boolean
-}
-
-interface ContractItemsTableProps {
- contractId: number
- items: ContractItem[]
- onItemsChange: (items: ContractItem[]) => void
- onTotalAmountChange: (total: number) => void
- currency?: string
- availableBudget?: number
- readOnly?: boolean
-}
-
-export function ContractItemsTable({
- contractId,
- items,
- onItemsChange,
- onTotalAmountChange,
- currency = 'USD',
- availableBudget = 0,
- readOnly = false
-}: ContractItemsTableProps) {
- 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)
-
- // 초기 데이터 로드
- React.useEffect(() => {
- const loadItems = async () => {
- try {
- setIsLoading(true)
- const fetchedItems = await getContractItems(contractId)
- const formattedItems = fetchedItems.map(item => ({
- id: item.id,
- project: item.project || '',
- itemCode: item.itemCode || '',
- itemInfo: item.itemInfo || '',
- specification: item.specification || '',
- quantity: item.quantity || 0,
- quantityUnit: item.quantityUnit || 'KG',
- contractDeliveryDate: item.contractDeliveryDate || '',
- contractUnitPrice: item.contractUnitPrice || 0,
- contractAmount: item.contractAmount || 0,
- contractCurrency: item.contractCurrency || currency,
- isSelected: false
- }))
- setLocalItems(formattedItems)
- onItemsChange(formattedItems)
- } catch (error) {
- console.error('Error loading contract items:', error)
- // 기본 빈 배열로 설정
- setLocalItems([])
- onItemsChange([])
- } finally {
- setIsLoading(false)
- }
- }
-
- loadItems()
- }, [contractId, currency, onItemsChange])
-
- // 로컬 상태와 부모 상태 동기화 (초기 로드 후에는 부모 상태 우선)
- React.useEffect(() => {
- if (items.length > 0) {
- setLocalItems(items)
- }
- }, [items])
-
- const handleSaveItems = async () => {
- try {
- setIsSaving(true)
-
- // validation 체크
- const errors = []
- localItems.forEach((item, index) => {
- if (!item.project) errors.push(`${index + 1}번째 품목의 프로젝트`)
- if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`)
- if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`)
- if (!item.specification) errors.push(`${index + 1}번째 품목의 사양`)
- 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)
- 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: any) => {
- 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 = {
- project: '',
- itemCode: '',
- itemInfo: '',
- specification: '',
- quantity: 0,
- quantityUnit: 'KG',
- contractDeliveryDate: '',
- contractUnitPrice: 0,
- contractAmount: 0,
- contractCurrency: currency,
- 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) => {
- 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 (
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="items">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-2">
- <Package className="w-5 h-5" />
- <span>품목 정보</span>
- <span className="text-sm text-gray-500">(로딩 중...)</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <div className="flex items-center justify-center py-8">
- <LoaderIcon className="w-6 h-6 animate-spin mr-2" />
- <span>품목 정보를 불러오는 중...</span>
- </div>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- )
- }
-
- return (
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="items">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <Package className="w-5 h-5" />
- <span className="font-medium">품목 정보</span>
- <span className="text-sm text-gray-500">({localItems.length}개 품목)</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <Card>
- <CardHeader>
- {/* 체크박스 */}
- <div className="flex items-center gap-2 mb-4">
- <Checkbox
- checked={isEnabled}
- onCheckedChange={(checked) => setIsEnabled(checked as boolean)}
- disabled={readOnly}
- />
- <span className="text-sm font-medium">품목 정보 활성화</span>
- </div>
-
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-600">총 금액: {totalAmount.toLocaleString()} {currency}</span>
- <span className="text-sm text-gray-600">총 수량: {totalQuantity.toLocaleString()}</span>
- </div>
- {!readOnly && (
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={addRow}
- disabled={!isEnabled}
- className="flex items-center gap-2"
- >
- <Plus className="w-4 h-4" />
- 행 추가
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={deleteSelectedRows}
- disabled={!isEnabled}
- className="flex items-center gap-2 text-red-600 hover:text-red-700"
- >
- <Trash2 className="w-4 h-4" />
- 행 삭제
- </Button>
- <Button
- onClick={handleSaveItems}
- disabled={isSaving || !isEnabled}
- className="flex items-center gap-2"
- >
- {isSaving ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- 품목정보 저장
- </Button>
- </div>
- )}
- </div>
-
- {/* 요약 정보 */}
- <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)}
- </div>
- </div>
- <div className="space-y-1">
- <Label className="text-sm font-medium">가용예산</Label>
- <div className="text-lg font-bold">
- {formatCurrency(availableBudget)}
- </div>
- </div>
- <div className="space-y-1">
- <Label className="text-sm font-medium">가용예산 比 (금액차)</Label>
- <div className={`text-lg font-bold ${amountDifference >= 0 ? 'text-green-600' : 'text-red-600'}`}>
- {formatCurrency(amountDifference)}
- </div>
- </div>
- <div className="space-y-1">
- <Label className="text-sm font-medium">가용예산 比 (비율)</Label>
- <div className={`text-lg font-bold ${budgetRatio <= 100 ? 'text-green-600' : 'text-red-600'}`}>
- {budgetRatio.toFixed(1)}%
- </div>
- </div>
- </div>
- </CardHeader>
-
- <CardContent>
- <div className="overflow-x-auto">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-12">
- {!readOnly && (
- <Checkbox
- checked={allSelected}
- ref={(el) => {
- if (el) (el as any).indeterminate = someSelected && !allSelected
- }}
- onCheckedChange={toggleSelectAll}
- disabled={!isEnabled}
- />
- )}
- </TableHead>
- <TableHead>프로젝트</TableHead>
- <TableHead>품목코드 (PKG No.)</TableHead>
- <TableHead>Item 정보 (자재그룹 / 자재코드)</TableHead>
- <TableHead>규격</TableHead>
- <TableHead className="text-right">수량</TableHead>
- <TableHead>수량단위</TableHead>
- <TableHead>계약납기일</TableHead>
- <TableHead className="text-right">계약단가</TableHead>
- <TableHead className="text-right">계약금액</TableHead>
- <TableHead>계약통화</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {localItems.map((item, index) => (
- <TableRow key={index}>
- <TableCell>
- {!readOnly && (
- <Checkbox
- checked={item.isSelected || false}
- onCheckedChange={(checked) =>
- updateItem(index, 'isSelected', checked)
- }
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.project || '-'}</span>
- ) : (
- <Input
- value={item.project}
- onChange={(e) => updateItem(index, 'project', e.target.value)}
- placeholder="프로젝트"
- className="w-32"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.itemCode || '-'}</span>
- ) : (
- <Input
- value={item.itemCode}
- onChange={(e) => updateItem(index, 'itemCode', e.target.value)}
- placeholder="품목코드"
- className="w-32"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.itemInfo || '-'}</span>
- ) : (
- <Input
- value={item.itemInfo}
- onChange={(e) => updateItem(index, 'itemInfo', e.target.value)}
- placeholder="Item 정보"
- className="w-48"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.specification || '-'}</span>
- ) : (
- <Input
- value={item.specification}
- onChange={(e) => updateItem(index, 'specification', e.target.value)}
- placeholder="규격"
- className="w-32"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span className="text-right">{item.quantity.toLocaleString()}</span>
- ) : (
- <Input
- type="number"
- value={item.quantity}
- onChange={(e) => updateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
- className="w-24 text-right"
- placeholder="0"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.quantityUnit || '-'}</span>
- ) : (
- <Input
- value={item.quantityUnit}
- onChange={(e) => updateItem(index, 'quantityUnit', e.target.value)}
- placeholder="단위"
- className="w-16"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.contractDeliveryDate || '-'}</span>
- ) : (
- <Input
- type="date"
- value={item.contractDeliveryDate}
- onChange={(e) => updateItem(index, 'contractDeliveryDate', e.target.value)}
- className="w-36"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span className="text-right">{item.contractUnitPrice.toLocaleString()}</span>
- ) : (
- <Input
- type="number"
- value={item.contractUnitPrice}
- onChange={(e) => updateItem(index, 'contractUnitPrice', parseFloat(e.target.value) || 0)}
- className="w-24 text-right"
- placeholder="0"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell>
- <div className="font-semibold text-primary text-right">
- {formatCurrency(item.contractAmount)}
- </div>
- </TableCell>
- <TableCell>
- {readOnly ? (
- <span>{item.contractCurrency || '-'}</span>
- ) : (
- <Input
- value={item.contractCurrency}
- onChange={(e) => updateItem(index, 'contractCurrency', e.target.value)}
- placeholder="통화"
- className="w-16"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
-
- {/* 합계 행 */}
- {localItems.length > 0 && (
- <div className="mt-4">
- <Table>
- <TableBody>
- <TableRow className="bg-muted/50 font-semibold">
- <TableCell colSpan={5} className="text-center">
- 합계
- </TableCell>
- <TableCell className="text-right">
- {totalQuantity.toLocaleString()}
- </TableCell>
- <TableCell>
- {localItems[0]?.quantityUnit || '-'}
- </TableCell>
- <TableCell></TableCell>
- <TableCell className="text-right">
- {totalUnitPrice.toLocaleString()}
- </TableCell>
- <TableCell className="text-right font-bold text-primary">
- {formatCurrency(totalAmount)}
- </TableCell>
- <TableCell>
- {currency}
- </TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </div>
- )}
- </CardContent>
- </Card>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- )
-}
+'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 { + 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 + project: string + itemCode: string + itemInfo: string + specification: string + quantity: number + quantityUnit: 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 + currency?: string + availableBudget?: number + readOnly?: boolean +} + +export function ContractItemsTable({ + contractId, + items, + onItemsChange, + onTotalAmountChange, + currency = 'USD', + availableBudget = 0, + readOnly = false +}: ContractItemsTableProps) { + // 통화 코드가 null이거나 undefined일 때 기본값 설정 + const safeCurrency = currency || 'USD' + 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) + + // 초기 데이터 로드 + React.useEffect(() => { + const loadItems = async () => { + try { + setIsLoading(true) + const fetchedItems = await getContractItems(contractId) + const formattedItems = fetchedItems.map(item => ({ + id: item.id, + project: item.project || '', + itemCode: item.itemCode || '', + itemInfo: item.itemInfo || '', + specification: item.specification || '', + quantity: Number(item.quantity) || 0, + quantityUnit: item.quantityUnit || 'KG', + contractDeliveryDate: item.contractDeliveryDate || '', + contractUnitPrice: Number(item.contractUnitPrice) || 0, + contractAmount: Number(item.contractAmount) || 0, + contractCurrency: item.contractCurrency || safeCurrency, + 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, currency, 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.project) errors.push(`${index + 1}번째 품목의 프로젝트`) + if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`) + if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`) + if (!item.specification) errors.push(`${index + 1}번째 품목의 사양`) + 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 = { + project: '', + itemCode: '', + itemInfo: '', + specification: '', + quantity: 0, + quantityUnit: 'KG', + contractDeliveryDate: '', + contractUnitPrice: 0, + contractAmount: 0, + contractCurrency: safeCurrency, + 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) => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: safeCurrency, + }).format(amount) + } + + const allSelected = localItems.length > 0 && localItems.every(item => item.isSelected) + const someSelected = localItems.some(item => item.isSelected) + + if (isLoading) { + return ( + <Accordion type="single" collapsible className="w-full"> + <AccordionItem value="items"> + <AccordionTrigger className="hover:no-underline"> + <div className="flex items-center gap-2"> + <Package className="w-5 h-5" /> + <span>품목 정보</span> + <span className="text-sm text-gray-500">(로딩 중...)</span> + </div> + </AccordionTrigger> + <AccordionContent> + <div className="flex items-center justify-center py-8"> + <LoaderIcon className="w-6 h-6 animate-spin mr-2" /> + <span>품목 정보를 불러오는 중...</span> + </div> + </AccordionContent> + </AccordionItem> + </Accordion> + ) + } + + return ( + <Accordion type="single" collapsible className="w-full"> + <AccordionItem value="items"> + <AccordionTrigger className="hover:no-underline"> + <div className="flex items-center gap-3 w-full"> + <Package className="w-5 h-5" /> + <span className="font-medium">품목 정보</span> + <span className="text-sm text-gray-500">({localItems.length}개 품목)</span> + </div> + </AccordionTrigger> + <AccordionContent> + <Card> + <CardHeader> + {/* 체크박스 */} + <div className="flex items-center gap-2 mb-4"> + <Checkbox + checked={isEnabled} + onCheckedChange={(checked) => setIsEnabled(checked as boolean)} + disabled={readOnly} + /> + <span className="text-sm font-medium">품목 정보 활성화</span> + </div> + + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <span className="text-sm text-gray-600">총 금액: {totalAmount.toLocaleString()} {currency}</span> + <span className="text-sm text-gray-600">총 수량: {totalQuantity.toLocaleString()}</span> + </div> + {!readOnly && ( + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={addRow} + disabled={!isEnabled} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + 행 추가 + </Button> + <Button + variant="outline" + size="sm" + onClick={deleteSelectedRows} + disabled={!isEnabled} + className="flex items-center gap-2 text-red-600 hover:text-red-700" + > + <Trash2 className="w-4 h-4" /> + 행 삭제 + </Button> + <Button + onClick={handleSaveItems} + disabled={isSaving || !isEnabled} + className="flex items-center gap-2" + > + {isSaving ? ( + <LoaderIcon className="w-4 h-4 animate-spin" /> + ) : ( + <Save className="w-4 h-4" /> + )} + 품목정보 저장 + </Button> + </div> + )} + </div> + + {/* 요약 정보 */} + <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)} + </div> + </div> + <div className="space-y-1"> + <Label className="text-sm font-medium">가용예산</Label> + <div className="text-lg font-bold"> + {formatCurrency(availableBudget)} + </div> + </div> + <div className="space-y-1"> + <Label className="text-sm font-medium">가용예산 比 (금액차)</Label> + <div className={`text-lg font-bold ${amountDifference >= 0 ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(amountDifference)} + </div> + </div> + <div className="space-y-1"> + <Label className="text-sm font-medium">가용예산 比 (비율)</Label> + <div className={`text-lg font-bold ${budgetRatio <= 100 ? 'text-green-600' : 'text-red-600'}`}> + {budgetRatio.toFixed(1)}% + </div> + </div> + </div> + </CardHeader> + + <CardContent> + <div className="overflow-x-auto"> + <Table> + <TableHeader> + <TableRow className="border-b-2"> + <TableHead className="w-12 px-2"> + {!readOnly && ( + <Checkbox + checked={allSelected} + ref={(el) => { + if (el) (el as HTMLInputElement & { indeterminate?: boolean }).indeterminate = someSelected && !allSelected + }} + onCheckedChange={toggleSelectAll} + disabled={!isEnabled} + /> + )} + </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 text-right">수량</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 text-right">계약금액</TableHead> + <TableHead className="px-3 py-3 font-semibold">계약통화</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {localItems.map((item, index) => ( + <TableRow key={index} className="hover:bg-muted/30 transition-colors"> + <TableCell className="px-2"> + {!readOnly && ( + <Checkbox + checked={item.isSelected || false} + onCheckedChange={(checked) => + updateItem(index, 'isSelected', checked) + } + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.project || '-'}</span> + ) : ( + <Input + value={item.project} + onChange={(e) => updateItem(index, 'project', e.target.value)} + placeholder="프로젝트" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.itemCode || '-'}</span> + ) : ( + <Input + value={item.itemCode} + onChange={(e) => updateItem(index, 'itemCode', e.target.value)} + placeholder="품목코드" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.itemInfo || '-'}</span> + ) : ( + <Input + value={item.itemInfo} + onChange={(e) => updateItem(index, 'itemInfo', e.target.value)} + placeholder="Item 정보" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.specification || '-'}</span> + ) : ( + <Input + value={item.specification} + onChange={(e) => updateItem(index, 'specification', e.target.value)} + placeholder="규격" + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm text-right">{item.quantity.toLocaleString()}</span> + ) : ( + <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 ? ( + <span className="text-sm">{item.quantityUnit || '-'}</span> + ) : ( + <Input + value={item.quantityUnit} + onChange={(e) => updateItem(index, 'quantityUnit', e.target.value)} + placeholder="단위" + className="h-8 text-sm w-16" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.contractDeliveryDate || '-'}</span> + ) : ( + <Input + type="date" + value={item.contractDeliveryDate} + onChange={(e) => updateItem(index, 'contractDeliveryDate', e.target.value)} + className="h-8 text-sm" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm text-right">{item.contractUnitPrice.toLocaleString()}</span> + ) : ( + <Input + type="number" + value={item.contractUnitPrice} + onChange={(e) => updateItem(index, 'contractUnitPrice', parseFloat(e.target.value) || 0)} + className="h-8 text-sm text-right" + placeholder="0" + disabled={!isEnabled} + /> + )} + </TableCell> + <TableCell className="px-3 py-3"> + <div className="font-semibold text-primary text-right text-sm"> + {formatCurrency(item.contractAmount)} + </div> + </TableCell> + <TableCell className="px-3 py-3"> + {readOnly ? ( + <span className="text-sm">{item.contractCurrency || '-'}</span> + ) : ( + <Input + value={item.contractCurrency} + onChange={(e) => updateItem(index, 'contractCurrency', e.target.value)} + placeholder="통화" + className="h-8 text-sm w-16" + disabled={!isEnabled} + /> + )} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + + {/* 합계 정보 */} + {localItems.length > 0 && ( + <div className="mt-6 flex justify-end"> + <Card className="w-80 bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20"> + <CardContent className="p-6"> + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <span className="text-sm font-medium text-muted-foreground">총 수량</span> + <span className="text-lg font-semibold"> + {totalQuantity.toLocaleString()} {localItems[0]?.quantityUnit || 'KG'} + </span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm font-medium text-muted-foreground">총 단가</span> + <span className="text-lg font-semibold"> + {totalUnitPrice.toLocaleString()} {currency} + </span> + </div> + <div className="border-t pt-4"> + <div className="flex items-center justify-between"> + <span className="text-xl font-bold text-primary">합계 금액</span> + <span className="text-2xl font-bold text-primary"> + {formatCurrency(totalAmount)} + </span> + </div> + </div> + </div> + </CardContent> + </Card> + </div> + )} + </CardContent> + </Card> + </AccordionContent> + </AccordionItem> + </Accordion> + ) +} |
