summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor/components/pr-items-pricing-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor/components/pr-items-pricing-dialog.tsx')
-rw-r--r--lib/bidding/vendor/components/pr-items-pricing-dialog.tsx384
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>
- )
-}