diff options
Diffstat (limited to 'lib/bidding/vendor/components/pr-items-pricing-dialog.tsx')
| -rw-r--r-- | lib/bidding/vendor/components/pr-items-pricing-dialog.tsx | 384 |
1 files changed, 0 insertions, 384 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-dialog.tsx b/lib/bidding/vendor/components/pr-items-pricing-dialog.tsx deleted file mode 100644 index ff0dfd9c..00000000 --- a/lib/bidding/vendor/components/pr-items-pricing-dialog.tsx +++ /dev/null @@ -1,384 +0,0 @@ -'use client' - -import * as React from 'react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { Badge } from '@/components/ui/badge' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, - DialogFooter, -} from '@/components/ui/dialog' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' -import { - Package, - FileText, - Download, - Calculator, - Save -} from 'lucide-react' -import { formatDate } from '@/lib/utils' -import { downloadFile } from '@/lib/file-download' -import { getSpecDocumentsForPrItem } from '../../pre-quote/service' -import { useToast } from '@/hooks/use-toast' - -interface PrItem { - id: number - itemNumber: string | null - prNumber: string | null - itemInfo: string | null - materialDescription: string | null - quantity: string | null - quantityUnit: string | null - currency: string | null - requestedDeliveryDate: string | null - hasSpecDocument: boolean | null -} - -interface PrItemQuotation { - prItemId: number - bidUnitPrice: number - bidAmount: number - proposedDeliveryDate?: string - technicalSpecification?: string -} - -interface SpecDocument { - id: number - fileName: string - originalFileName: string - fileSize: number | null - filePath: string - title: string | null - description: string | null - uploadedAt: string -} - -interface PrItemsPricingDialogProps { - prItems: PrItem[] - initialQuotations?: PrItemQuotation[] - currency?: string - onSave: (quotations: PrItemQuotation[], totalAmount: number) => void - readOnly?: boolean - children: React.ReactNode -} - -export function PrItemsPricingDialog({ - prItems, - initialQuotations = [], - currency = 'KRW', - onSave, - readOnly = false, - children -}: PrItemsPricingDialogProps) { - const { toast } = useToast() - const [open, setOpen] = React.useState(false) - const [quotations, setQuotations] = React.useState<PrItemQuotation[]>([]) - const [specDocuments, setSpecDocuments] = React.useState<Record<number, SpecDocument[]>>({}) - const [loadingSpecs, setLoadingSpecs] = React.useState<Record<number, boolean>>({}) - - // 다이얼로그 열릴 때 초기 견적 데이터 설정 - React.useEffect(() => { - if (open) { - const initQuotations = prItems.map(item => { - const existing = initialQuotations.find(q => q.prItemId === item.id) - if (existing) { - return existing - } - return { - prItemId: item.id, - bidUnitPrice: 0, - bidAmount: 0, - proposedDeliveryDate: '', - technicalSpecification: '' - } - }) - setQuotations(initQuotations) - } - }, [open, prItems, initialQuotations]) - - // SPEC 문서 로드 - const loadSpecDocuments = async (prItemId: number) => { - if (loadingSpecs[prItemId]) return - - setLoadingSpecs(prev => ({ ...prev, [prItemId]: true })) - try { - const docs = await getSpecDocumentsForPrItem(prItemId) - // Date를 string으로 변환 - const mappedDocs = docs.map(doc => ({ - ...doc, - uploadedAt: doc.uploadedAt.toString() - })) - setSpecDocuments(prev => ({ ...prev, [prItemId]: mappedDocs })) - } catch (error) { - console.error('Failed to load spec documents:', error) - } finally { - setLoadingSpecs(prev => ({ ...prev, [prItemId]: false })) - } - } - - // 견적 데이터 업데이트 - const updateQuotation = (prItemId: number, field: keyof PrItemQuotation, value: any) => { - const updatedQuotations = quotations.map(q => { - if (q.prItemId === prItemId) { - const updated = { ...q, [field]: value } - - // 단가가 변경되면 금액 자동 계산 - if (field === 'bidUnitPrice') { - const prItem = prItems.find(item => item.id === prItemId) - const quantity = parseFloat(prItem?.quantity || '1') - updated.bidAmount = updated.bidUnitPrice * quantity - } - - return updated - } - return q - }) - - setQuotations(updatedQuotations) - } - - // 파일 다운로드 - const handleDownloadSpec = async (document: SpecDocument) => { - try { - await downloadFile(document.filePath, document.originalFileName, { - showToast: true - }) - } catch (error) { - console.error('Failed to download spec document:', error) - } - } - - // 저장 처리 - const handleSave = () => { - const totalAmount = quotations.reduce((sum, q) => sum + q.bidAmount, 0) - onSave(quotations, totalAmount) - setOpen(false) - toast({ - title: '저장 완료', - description: '품목별 견적이 저장되었습니다.', - }) - } - - // 통화 포맷팅 - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('ko-KR', { - style: 'currency', - currency: currency, - }).format(amount) - } - - // 총 금액 계산 - const totalAmount = quotations.reduce((sum, q) => sum + q.bidAmount, 0) - - return ( - <Dialog open={open} onOpenChange={setOpen}> - <DialogTrigger asChild> - {children} - </DialogTrigger> - <DialogContent className="max-w-7xl max-h-[90vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle className="flex items-center gap-2"> - <Package className="w-5 h-5" /> - 품목별 견적 작성 - </DialogTitle> - <DialogDescription> - 각 품목별로 견적 단가를 입력하여 총 사전견적 금액을 계산합니다. - </DialogDescription> - </DialogHeader> - - <div className="space-y-4"> - <div className="overflow-x-auto"> - <Table> - <TableHeader> - <TableRow> - <TableHead>아이템번호</TableHead> - <TableHead>PR번호</TableHead> - <TableHead>품목정보</TableHead> - <TableHead>자재내역</TableHead> - <TableHead>수량</TableHead> - <TableHead>단위</TableHead> - <TableHead>견적단가</TableHead> - <TableHead>견적금액</TableHead> - <TableHead>납품예정일</TableHead> - <TableHead>기술사양</TableHead> - <TableHead>SPEC</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {prItems.map((item) => { - const quotation = quotations.find(q => q.prItemId === item.id) || { - prItemId: item.id, - bidUnitPrice: 0, - bidAmount: 0, - proposedDeliveryDate: '', - technicalSpecification: '' - } - - return ( - <TableRow key={item.id}> - <TableCell className="font-medium"> - {item.itemNumber || '-'} - </TableCell> - <TableCell>{item.prNumber || '-'}</TableCell> - <TableCell> - <div className="max-w-32 truncate" title={item.itemInfo || ''}> - {item.itemInfo || '-'} - </div> - </TableCell> - <TableCell> - <div className="max-w-32 truncate" title={item.materialDescription || ''}> - {item.materialDescription || '-'} - </div> - </TableCell> - <TableCell className="text-right"> - {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'} - </TableCell> - <TableCell>{item.quantityUnit || '-'}</TableCell> - <TableCell> - {readOnly ? ( - <span className="font-medium"> - {quotation.bidUnitPrice.toLocaleString()} - </span> - ) : ( - <Input - type="number" - value={quotation.bidUnitPrice} - onChange={(e) => updateQuotation( - item.id, - 'bidUnitPrice', - parseFloat(e.target.value) || 0 - )} - className="w-32 text-right" - placeholder="단가" - /> - )} - </TableCell> - <TableCell> - <div className="font-semibold text-primary"> - {formatCurrency(quotation.bidAmount)} - </div> - </TableCell> - <TableCell> - {readOnly ? ( - quotation.proposedDeliveryDate ? - formatDate(quotation.proposedDeliveryDate, 'KR') : '-' - ) : ( - <Input - type="date" - value={quotation.proposedDeliveryDate} - onChange={(e) => updateQuotation( - item.id, - 'proposedDeliveryDate', - e.target.value - )} - className="w-40" - /> - )} - </TableCell> - <TableCell> - {readOnly ? ( - <div className="max-w-32 truncate" title={quotation.technicalSpecification || ''}> - {quotation.technicalSpecification || '-'} - </div> - ) : ( - <Textarea - value={quotation.technicalSpecification} - onChange={(e) => updateQuotation( - item.id, - 'technicalSpecification', - e.target.value - )} - placeholder="기술사양 입력" - className="w-48 min-h-[60px]" - rows={2} - /> - )} - </TableCell> - <TableCell> - {item.hasSpecDocument ? ( - <div className="space-y-1"> - {!specDocuments[item.id] ? ( - <Button - variant="outline" - size="sm" - onClick={() => loadSpecDocuments(item.id)} - disabled={loadingSpecs[item.id]} - > - <FileText className="w-3 h-3 mr-1" /> - {loadingSpecs[item.id] ? '로딩...' : 'SPEC 보기'} - </Button> - ) : specDocuments[item.id].length > 0 ? ( - <div className="space-y-1"> - {specDocuments[item.id].map((doc) => ( - <Button - key={doc.id} - variant="outline" - size="sm" - onClick={() => handleDownloadSpec(doc)} - className="block text-xs" - > - <Download className="w-3 h-3 mr-1" /> - {doc.originalFileName} - </Button> - ))} - </div> - ) : ( - <Badge variant="secondary">문서 없음</Badge> - )} - </div> - ) : ( - <Badge variant="outline">SPEC 없음</Badge> - )} - </TableCell> - </TableRow> - ) - })} - </TableBody> - </Table> - </div> - - {/* 총 금액 표시 */} - <div className="flex justify-end border-t pt-4"> - <div className="bg-gray-50 rounded-lg p-4 min-w-80"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <Calculator className="w-5 h-5 text-primary" /> - <Label className="font-semibold text-lg">총 사전견적 금액</Label> - </div> - <div className="text-2xl font-bold text-primary"> - {formatCurrency(totalAmount)} - </div> - </div> - </div> - </div> - </div> - - <DialogFooter> - <Button variant="outline" onClick={() => setOpen(false)}> - 취소 - </Button> - {!readOnly && ( - <Button onClick={handleSave}> - <Save className="w-4 h-4 mr-2" /> - 저장하기 - </Button> - )} - </DialogFooter> - </DialogContent> - </Dialog> - ) -} |
