diff options
Diffstat (limited to 'lib/general-contracts_old/detail/general-contract-items-table.tsx')
| -rw-r--r-- | lib/general-contracts_old/detail/general-contract-items-table.tsx | 602 |
1 files changed, 0 insertions, 602 deletions
diff --git a/lib/general-contracts_old/detail/general-contract-items-table.tsx b/lib/general-contracts_old/detail/general-contract-items-table.tsx deleted file mode 100644 index 1b9a1a06..00000000 --- a/lib/general-contracts_old/detail/general-contract-items-table.tsx +++ /dev/null @@ -1,602 +0,0 @@ -'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<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, - 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 ( - <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">총 금액: {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}</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, localItems[0]?.contractCurrency || 'KRW')} - </div> - </div> - <div className="space-y-1"> - <Label className="text-sm font-medium">가용예산</Label> - <div className="text-lg font-bold"> - {formatCurrency(availableBudget, localItems[0]?.contractCurrency || 'KRW')} - </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, localItems[0]?.contractCurrency || 'KRW')} - </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">품목코드 (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 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.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> - ) : ( - <Select - value={item.quantityUnit} - onValueChange={(value) => updateItem(index, 'quantityUnit', value)} - disabled={!isEnabled} - > - <SelectTrigger className="h-8 text-sm w-20"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {QUANTITY_UNITS.map((unit) => ( - <SelectItem key={unit} value={unit}> - {unit} - </SelectItem> - ))} - </SelectContent> - </Select> - )} - </TableCell> - <TableCell className="px-3 py-3"> - {readOnly ? ( - <span className="text-sm text-right">{item.totalWeight.toLocaleString()}</span> - ) : ( - <Input - type="number" - value={item.totalWeight} - onChange={(e) => updateItem(index, 'totalWeight', 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.weightUnit || '-'}</span> - ) : ( - <Select - value={item.weightUnit} - onValueChange={(value) => updateItem(index, 'weightUnit', value)} - disabled={!isEnabled} - > - <SelectTrigger className="h-8 text-sm w-20"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {WEIGHT_UNITS.map((unit) => ( - <SelectItem key={unit} value={unit}> - {unit} - </SelectItem> - ))} - </SelectContent> - </Select> - )} - </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> - ) : ( - <Select - value={item.contractCurrency} - onValueChange={(value) => updateItem(index, 'contractCurrency', value)} - disabled={!isEnabled} - > - <SelectTrigger className="h-8 text-sm w-20"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {CURRENCIES.map((currency) => ( - <SelectItem key={currency} value={currency}> - {currency} - </SelectItem> - ))} - </SelectContent> - </Select> - )} - </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"> - {formatCurrency(totalUnitPrice, localItems[0]?.contractCurrency || 'KRW')} - </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, localItems[0]?.contractCurrency || 'KRW')} - </span> - </div> - </div> - </div> - </CardContent> - </Card> - </div> - )} - </CardContent> - </Card> - </AccordionContent> - </AccordionItem> - </Accordion> - ) -} |
