summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-23 09:03:29 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-23 09:03:29 +0000
commit95866a13ba4e1c235373834460aa284b763fe0d9 (patch)
tree47a7a13d6e20907adbcbe04080f7c0aa3c7aea7f /lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
parent5c9b39eb011763a7491b3e8542de9f6d4976dd65 (diff)
(최겸) 기술영업 RFQ 개발(0620 요구사항, 첨부파일, REV 등)
Diffstat (limited to 'lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx')
-rw-r--r--lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx312
1 files changed, 312 insertions, 0 deletions
diff --git a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
new file mode 100644
index 00000000..7832fa2b
--- /dev/null
+++ b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx
@@ -0,0 +1,312 @@
+"use client"
+
+import * as React from "react"
+import { useState, useEffect } from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Separator } from "@/components/ui/separator"
+import { Skeleton } from "@/components/ui/skeleton"
+import { Clock, User, FileText, AlertCircle, Paperclip } from "lucide-react"
+import { formatDate } from "@/lib/utils"
+import { toast } from "sonner"
+
+interface QuotationAttachment {
+ id: number
+ quotationId: number
+ revisionId: number
+ fileName: string
+ originalFileName: string
+ fileSize: number
+ fileType: string | null
+ filePath: string
+ description: string | null
+ isVendorUpload: boolean
+ createdAt: Date
+ updatedAt: Date
+}
+
+interface QuotationSnapshot {
+ currency: string | null
+ totalPrice: string | null
+ validUntil: Date | null
+ remark: string | null
+ status: string | null
+ quotationVersion: number | null
+ submittedAt: Date | null
+ acceptedAt: Date | null
+ updatedAt: Date | null
+}
+
+interface QuotationRevision {
+ id: number
+ version: number
+ snapshot: QuotationSnapshot
+ changeReason: string | null
+ revisionNote: string | null
+ revisedBy: number | null
+ revisedAt: Date
+ revisedByName: string | null
+ attachments: QuotationAttachment[]
+}
+
+interface QuotationHistoryData {
+ current: {
+ id: number
+ currency: string | null
+ totalPrice: string | null
+ validUntil: Date | null
+ remark: string | null
+ status: string
+ quotationVersion: number | null
+ submittedAt: Date | null
+ acceptedAt: Date | null
+ updatedAt: Date | null
+ attachments: QuotationAttachment[]
+ }
+ revisions: QuotationRevision[]
+}
+
+interface QuotationHistoryDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ quotationId: number | null
+}
+
+const statusConfig = {
+ "Draft": { label: "초안", color: "bg-yellow-100 text-yellow-800" },
+ "Submitted": { label: "제출됨", color: "bg-blue-100 text-blue-800" },
+ "Revised": { label: "수정됨", color: "bg-purple-100 text-purple-800" },
+ "Accepted": { label: "승인됨", color: "bg-green-100 text-green-800" },
+ "Rejected": { label: "거절됨", color: "bg-red-100 text-red-800" },
+}
+
+function QuotationCard({
+ data,
+ version,
+ isCurrent = false,
+ changeReason,
+ revisedBy,
+ revisedAt,
+ attachments
+}: {
+ data: QuotationSnapshot | QuotationHistoryData["current"]
+ version: number
+ isCurrent?: boolean
+ changeReason?: string | null
+ revisedBy?: string | null
+ revisedAt?: Date
+ attachments?: QuotationAttachment[]
+}) {
+ const statusInfo = statusConfig[data.status as keyof typeof statusConfig] ||
+ { label: data.status || "알 수 없음", color: "bg-gray-100 text-gray-800" }
+
+ return (
+ <Card className={`${isCurrent ? "border-blue-500 shadow-md" : "border-gray-200"}`}>
+ <CardHeader className="pb-3">
+ <div className="flex items-center justify-between">
+ <CardTitle className="text-lg flex items-center gap-2">
+ <span>버전 {version}</span>
+ {isCurrent && <Badge variant="default">현재</Badge>}
+ </CardTitle>
+ <Badge className={statusInfo.color}>
+ {statusInfo.label}
+ </Badge>
+ </div>
+ {changeReason && (
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <FileText className="size-4" />
+ <span>{changeReason}</span>
+ </div>
+ )}
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">견적 금액</p>
+ <p className="text-lg font-semibold">
+ {data.totalPrice ? `${data.currency} ${Number(data.totalPrice).toLocaleString()}` : "미입력"}
+ </p>
+ </div>
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">유효 기한</p>
+ <p className="text-sm">
+ {data.validUntil ? formatDate(data.validUntil) : "미설정"}
+ </p>
+ </div>
+ </div>
+
+ {data.remark && (
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">비고</p>
+ <p className="text-sm bg-gray-50 p-2 rounded">{data.remark}</p>
+ </div>
+ )}
+
+ {/* 첨부파일 섹션 */}
+ {attachments && attachments.length > 0 && (
+ <div>
+ <p className="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
+ <Paperclip className="size-3" />
+ 첨부파일 ({attachments.length}개)
+ </p>
+ <div className="space-y-1">
+ {attachments.map((attachment) => (
+ <div key={attachment.id} className="flex items-center justify-between p-2 bg-gray-50 rounded text-xs">
+ <div className="flex items-center gap-2 min-w-0 flex-1">
+ <div className="min-w-0 flex-1">
+ <p className="font-medium truncate" title={attachment.originalFileName}>
+ {attachment.originalFileName}
+ </p>
+ {attachment.description && (
+ <p className="text-muted-foreground truncate" title={attachment.description}>
+ {attachment.description}
+ </p>
+ )}
+ </div>
+ </div>
+ <div className="text-muted-foreground whitespace-nowrap ml-2">
+ {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <Separator />
+
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
+ <div className="flex items-center gap-1">
+ <Clock className="size-3" />
+ <span>
+ {isCurrent
+ ? `수정: ${data.updatedAt ? formatDate(data.updatedAt) : "N/A"}`
+ : `변경: ${revisedAt ? formatDate(revisedAt) : "N/A"}`
+ }
+ </span>
+ </div>
+ {revisedBy && (
+ <div className="flex items-center gap-1">
+ <User className="size-3" />
+ <span>{revisedBy}</span>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+export function QuotationHistoryDialog({
+ open,
+ onOpenChange,
+ quotationId
+}: QuotationHistoryDialogProps) {
+ const [data, setData] = useState<QuotationHistoryData | null>(null)
+ const [isLoading, setIsLoading] = useState(false)
+
+ useEffect(() => {
+ if (open && quotationId) {
+ loadQuotationHistory()
+ }
+ }, [open, quotationId])
+
+ const loadQuotationHistory = async () => {
+ if (!quotationId) return
+
+ try {
+ setIsLoading(true)
+ const { getTechSalesVendorQuotationWithRevisions } = await import("@/lib/techsales-rfq/service")
+
+ const result = await getTechSalesVendorQuotationWithRevisions(quotationId)
+
+ if (result.error) {
+ toast.error(result.error)
+ return
+ }
+
+ setData(result.data as QuotationHistoryData)
+ } catch (error) {
+ console.error("견적 히스토리 로드 오류:", error)
+ toast.error("견적 히스토리를 불러오는 중 오류가 발생했습니다")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleOpenChange = (newOpen: boolean) => {
+ onOpenChange(newOpen)
+ if (!newOpen) {
+ setData(null) // 다이얼로그 닫을 때 데이터 초기화
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogContent className=" max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>견적서 수정 히스토리</DialogTitle>
+ <DialogDescription>
+ 견적서의 변경 이력을 확인할 수 있습니다. 최신 버전부터 순서대로 표시됩니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {isLoading ? (
+ <div className="space-y-4">
+ {[1, 2, 3].map((i) => (
+ <div key={i} className="space-y-3">
+ <Skeleton className="h-6 w-32" />
+ <Skeleton className="h-32 w-full" />
+ </div>
+ ))}
+ </div>
+ ) : data ? (
+ <>
+ {/* 현재 버전 */}
+ <QuotationCard
+ data={data.current}
+ version={data.current.quotationVersion || 1}
+ isCurrent={true}
+ attachments={data.current.attachments}
+ />
+
+ {/* 이전 버전들 */}
+ {data.revisions.length > 0 ? (
+ data.revisions.map((revision) => (
+ <QuotationCard
+ key={revision.id}
+ data={revision.snapshot}
+ version={revision.version}
+ changeReason={revision.changeReason}
+ revisedBy={revision.revisedByName}
+ revisedAt={revision.revisedAt}
+ attachments={revision.attachments}
+ />
+ ))
+ ) : (
+ <div className="text-center py-8 text-muted-foreground">
+ <AlertCircle className="size-12 mx-auto mb-2 opacity-50" />
+ <p>수정 이력이 없습니다.</p>
+ <p className="text-sm">이 견적서는 아직 수정되지 않았습니다.</p>
+ </div>
+ )}
+ </>
+ ) : (
+ <div className="text-center py-8 text-muted-foreground">
+ <AlertCircle className="size-12 mx-auto mb-2 opacity-50" />
+ <p>견적서 정보를 불러올 수 없습니다.</p>
+ </div>
+ )}
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file