summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-14 05:28:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-14 05:28:01 +0000
commit675b4e3d8ffcb57a041db285417d81e61284d900 (patch)
tree254f3d6a6c0ce39ae8fba35618f3810e08945f19 /lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
parent39f12cb19f29cbc5568057e154e6adf4789ae736 (diff)
(대표님) RFQ-last, tbe-last, 기본계약 템플릿 내 견적,입찰,계약 추가, env.dev NAS_PATH 수정
Diffstat (limited to 'lib/rfq-last/vendor-response/editor/quotation-items-table.tsx')
-rw-r--r--lib/rfq-last/vendor-response/editor/quotation-items-table.tsx449
1 files changed, 449 insertions, 0 deletions
diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
new file mode 100644
index 00000000..08928b4d
--- /dev/null
+++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
@@ -0,0 +1,449 @@
+"use client"
+
+import { useFormContext, useFieldArray } from "react-hook-form"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Button } from "@/components/ui/button"
+import { Calendar } from "@/components/ui/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { Badge } from "@/components/ui/badge"
+import { Label } from "@/components/ui/label"
+import { CalendarIcon, Eye, Calculator, AlertCircle } from "lucide-react"
+import { format } from "date-fns"
+import { cn, formatCurrency } from "@/lib/utils"
+import { useState } from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+interface QuotationItemsTableProps {
+ prItems: any[]
+}
+
+export default function QuotationItemsTable({ prItems }: QuotationItemsTableProps) {
+ const { control, register, setValue, watch } = useFormContext()
+ const { fields } = useFieldArray({
+ control,
+ name: "quotationItems"
+ })
+
+ const [selectedItem, setSelectedItem] = useState<any>(null)
+ const [showDetail, setShowDetail] = useState(false)
+
+ const currency = watch("vendorCurrency") || "USD"
+ const quotationItems = watch("quotationItems")
+
+ // 단가 * 수량 계산
+ const calculateTotal = (index: number) => {
+ const item = quotationItems[index]
+ const prItem = prItems[index]
+ if (item && prItem) {
+ const total = (item.unitPrice || 0) * (prItem.quantity || 0)
+ setValue(`quotationItems.${index}.totalPrice`, total)
+ }
+ }
+
+ // 할인 적용
+ const applyDiscount = (index: number) => {
+ const item = quotationItems[index]
+ const prItem = prItems[index]
+ if (item && prItem && item.discountRate) {
+ const originalTotal = (item.unitPrice || 0) * (prItem.quantity || 0)
+ const discountAmount = originalTotal * (item.discountRate / 100)
+ const finalTotal = originalTotal - discountAmount
+ setValue(`quotationItems.${index}.totalPrice`, finalTotal)
+ }
+ }
+
+ const totalAmount = quotationItems?.reduce(
+ (sum: number, item: any) => sum + (item.totalPrice || 0), 0
+ ) || 0
+
+ // 상세 정보 다이얼로그
+ const ItemDetailDialog = ({ item, prItem, index }: any) => (
+ <Dialog open={showDetail} onOpenChange={setShowDetail}>
+ <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>견적 상세 정보</DialogTitle>
+ <DialogDescription>
+ {prItem.materialCode} - {prItem.materialDescription}
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* PR 아이템 정보 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-base">PR 아이템 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="grid grid-cols-2 gap-4">
+ <div>
+ <Label className="text-xs text-muted-foreground">PR 번호</Label>
+ <p className="font-medium">{prItem.prNo}</p>
+ </div>
+ <div>
+ <Label className="text-xs text-muted-foreground">자재 코드</Label>
+ <p className="font-medium">{prItem.materialCode}</p>
+ </div>
+ <div>
+ <Label className="text-xs text-muted-foreground">수량</Label>
+ <p className="font-medium">{prItem.quantity} {prItem.uom}</p>
+ </div>
+ <div>
+ <Label className="text-xs text-muted-foreground">요청 납기일</Label>
+ <p className="font-medium">
+ {prItem.deliveryDate ? format(new Date(prItem.deliveryDate), "yyyy-MM-dd") : '-'}
+ </p>
+ </div>
+ {prItem.specNo && (
+ <div>
+ <Label className="text-xs text-muted-foreground">스펙 번호</Label>
+ <p className="font-medium">{prItem.specNo}</p>
+ </div>
+ )}
+ {prItem.trackingNo && (
+ <div>
+ <Label className="text-xs text-muted-foreground">추적 번호</Label>
+ <p className="font-medium">{prItem.trackingNo}</p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 제조사 정보 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-base">제조사 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="grid grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <Label htmlFor={`manufacturer-${index}`}>제조사</Label>
+ <Input
+ id={`manufacturer-${index}`}
+ {...register(`quotationItems.${index}.manufacturer`)}
+ placeholder="제조사 입력"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor={`manufacturerCountry-${index}`}>제조국</Label>
+ <Input
+ id={`manufacturerCountry-${index}`}
+ {...register(`quotationItems.${index}.manufacturerCountry`)}
+ placeholder="제조국 입력"
+ />
+ </div>
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor={`modelNo-${index}`}>모델 번호</Label>
+ <Input
+ id={`modelNo-${index}`}
+ {...register(`quotationItems.${index}.modelNo`)}
+ placeholder="모델 번호 입력"
+ />
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 기술 준수 및 대안 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-base">기술 사양</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`technicalCompliance-${index}`}
+ checked={watch(`quotationItems.${index}.technicalCompliance`)}
+ onCheckedChange={(checked) =>
+ setValue(`quotationItems.${index}.technicalCompliance`, checked)
+ }
+ />
+ <Label htmlFor={`technicalCompliance-${index}`}>
+ 기술 사양 준수
+ </Label>
+ </div>
+
+ {!watch(`quotationItems.${index}.technicalCompliance`) && (
+ <div className="space-y-2">
+ <Label htmlFor={`alternativeProposal-${index}`}>
+ 대안 제안 <span className="text-red-500">*</span>
+ </Label>
+ <Textarea
+ id={`alternativeProposal-${index}`}
+ {...register(`quotationItems.${index}.alternativeProposal`)}
+ placeholder="기술 사양을 준수하지 않는 경우 대안을 제시해주세요"
+ className="min-h-[100px]"
+ />
+ </div>
+ )}
+
+ <div className="space-y-2">
+ <Label htmlFor={`deviationReason-${index}`}>편차 사유</Label>
+ <Textarea
+ id={`deviationReason-${index}`}
+ {...register(`quotationItems.${index}.deviationReason`)}
+ placeholder="요구사항과 다른 부분이 있는 경우 사유를 입력하세요"
+ className="min-h-[80px]"
+ />
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 할인 정보 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-base">할인 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="grid grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <Label htmlFor={`discountRate-${index}`}>할인율 (%)</Label>
+ <Input
+ id={`discountRate-${index}`}
+ type="number"
+ step="0.01"
+ {...register(`quotationItems.${index}.discountRate`, { valueAsNumber: true })}
+ onChange={(e) => {
+ setValue(`quotationItems.${index}.discountRate`, parseFloat(e.target.value))
+ applyDiscount(index)
+ }}
+ placeholder="0.00"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label>할인 금액</Label>
+ <div className="h-10 px-3 py-2 border rounded-md bg-muted">
+ {formatCurrency(
+ (watch(`quotationItems.${index}.unitPrice`) || 0) *
+ (prItem.quantity || 0) *
+ ((watch(`quotationItems.${index}.discountRate`) || 0) / 100),
+ currency
+ )}
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 비고 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-base">비고</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <Textarea
+ {...register(`quotationItems.${index}.itemRemark`)}
+ placeholder="아이템별 비고사항을 입력하세요"
+ className="min-h-[100px]"
+ />
+ </CardContent>
+ </Card>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle>견적 품목</CardTitle>
+ <CardDescription>
+ 각 PR 아이템에 대한 견적 단가와 정보를 입력하세요
+ </CardDescription>
+ </div>
+ <div className="text-right">
+ <p className="text-sm text-muted-foreground">총 견적금액</p>
+ <p className="text-2xl font-bold text-primary">
+ {formatCurrency(totalAmount, currency)}
+ </p>
+ </div>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className="overflow-x-auto">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[50px]">No</TableHead>
+ <TableHead className="w-[100px]">PR No</TableHead>
+ <TableHead className="min-w-[150px]">자재코드</TableHead>
+ <TableHead className="min-w-[200px]">자재설명</TableHead>
+ <TableHead className="text-right w-[100px]">수량</TableHead>
+ <TableHead className="w-[150px]">단가</TableHead>
+ <TableHead className="text-right w-[150px]">총액</TableHead>
+ <TableHead className="w-[150px]">납기일</TableHead>
+ <TableHead className="w-[100px]">리드타임</TableHead>
+ <TableHead className="w-[80px]">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {fields.map((field, index) => {
+ const prItem = prItems[index]
+ const quotationItem = quotationItems[index]
+ const isMajor = prItem?.majorYn
+
+ return (
+ <TableRow key={field.id} className={isMajor ? "bg-yellow-50" : ""}>
+ <TableCell>
+ <div className="flex items-center gap-2">
+ {prItem?.rfqItem || index + 1}
+ {isMajor && (
+ <Badge variant="secondary" className="text-xs">
+ 주요
+ </Badge>
+ )}
+ </div>
+ </TableCell>
+ <TableCell className="font-mono text-xs">
+ {prItem?.prNo}
+ </TableCell>
+ <TableCell className="font-mono text-xs">
+ {prItem?.materialCode}
+ </TableCell>
+ <TableCell>
+ <div className="max-w-[200px]">
+ <p className="truncate text-sm" title={prItem?.materialDescription}>
+ {prItem?.materialDescription}
+ </p>
+ {prItem?.size && (
+ <p className="text-xs text-muted-foreground">
+ 사이즈: {prItem.size}
+ </p>
+ )}
+ </div>
+ </TableCell>
+ <TableCell className="text-right">
+ {prItem?.quantity} {prItem?.uom}
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-1">
+ <Input
+ type="number"
+ step="0.01"
+ {...register(`quotationItems.${index}.unitPrice`, { valueAsNumber: true })}
+ onChange={(e) => {
+ setValue(`quotationItems.${index}.unitPrice`, parseFloat(e.target.value))
+ calculateTotal(index)
+ }}
+ className="w-[120px]"
+ placeholder="0.00"
+ />
+ <span className="text-xs text-muted-foreground">
+ {currency}
+ </span>
+ </div>
+ </TableCell>
+ <TableCell className="text-right font-medium">
+ {formatCurrency(quotationItem?.totalPrice || 0, currency)}
+ </TableCell>
+ <TableCell>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ size="sm"
+ className={cn(
+ "w-[130px] justify-start text-left font-normal",
+ !quotationItem?.vendorDeliveryDate && "text-muted-foreground"
+ )}
+ >
+ <CalendarIcon className="mr-2 h-3 w-3" />
+ {quotationItem?.vendorDeliveryDate
+ ? format(quotationItem.vendorDeliveryDate, "yyyy-MM-dd")
+ : "선택"}
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={quotationItem?.vendorDeliveryDate}
+ onSelect={(date) => setValue(`quotationItems.${index}.vendorDeliveryDate`, date)}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ {prItem?.deliveryDate && quotationItem?.vendorDeliveryDate &&
+ new Date(quotationItem.vendorDeliveryDate) > new Date(prItem.deliveryDate) && (
+ <div className="mt-1">
+ <Badge variant="destructive" className="text-xs">
+ 지연
+ </Badge>
+ </div>
+ )}
+ </TableCell>
+ <TableCell>
+ <Input
+ type="number"
+ {...register(`quotationItems.${index}.leadTime`, { valueAsNumber: true })}
+ className="w-[80px]"
+ placeholder="일"
+ />
+ </TableCell>
+ <TableCell>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => {
+ setSelectedItem({ item: quotationItem, prItem, index })
+ setShowDetail(true)
+ }}
+ >
+ <Eye className="h-4 w-4" />
+ </Button>
+ </TableCell>
+ </TableRow>
+ )
+ })}
+ </TableBody>
+ </Table>
+ </div>
+
+ {/* 총액 요약 */}
+ <div className="mt-4 flex justify-end">
+ <Card className="w-[400px]">
+ <CardContent className="pt-4">
+ <div className="space-y-2">
+ <div className="flex justify-between text-sm">
+ <span className="text-muted-foreground">소계</span>
+ <span>{formatCurrency(totalAmount, currency)}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-muted-foreground">통화</span>
+ <span>{currency}</span>
+ </div>
+ <div className="border-t pt-2">
+ <div className="flex justify-between">
+ <span className="font-semibold">총 견적금액</span>
+ <span className="text-xl font-bold text-primary">
+ {formatCurrency(totalAmount, currency)}
+ </span>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </CardContent>
+
+ {/* 상세 다이얼로그 */}
+ {selectedItem && (
+ <ItemDetailDialog
+ item={selectedItem.item}
+ prItem={selectedItem.prItem}
+ index={selectedItem.index}
+ />
+ )}
+ </Card>
+ )
+} \ No newline at end of file