summaryrefslogtreecommitdiff
path: root/lib/avl/components/avl-history-modal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/avl/components/avl-history-modal.tsx')
-rw-r--r--lib/avl/components/avl-history-modal.tsx297
1 files changed, 297 insertions, 0 deletions
diff --git a/lib/avl/components/avl-history-modal.tsx b/lib/avl/components/avl-history-modal.tsx
new file mode 100644
index 00000000..4f0c354b
--- /dev/null
+++ b/lib/avl/components/avl-history-modal.tsx
@@ -0,0 +1,297 @@
+"use client"
+
+import * as React from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Calendar, Users, FileText, ChevronDown, ChevronRight } from "lucide-react"
+import type { AvlListItem } from "@/lib/avl/types"
+
+interface AvlHistoryModalProps {
+ isOpen: boolean
+ onClose: () => void
+ avlItem: AvlListItem | null
+ historyData?: AvlHistoryRecord[]
+ onLoadHistory?: (avlItem: AvlListItem) => Promise<AvlHistoryRecord[]>
+}
+
+export interface VendorSnapshot {
+ id: number
+ vendorName?: string
+ avlVendorName?: string
+ vendorCode?: string
+ disciplineName?: string
+ materialNameCustomerSide?: string
+ materialGroupCode?: string
+ materialGroupName?: string
+ tier?: string
+ hasAvl?: boolean
+ faTarget?: boolean
+ headquarterLocation?: string
+ ownerSuggestion?: boolean
+ shiSuggestion?: boolean
+ [key: string]: unknown // 다른 모든 속성들
+}
+
+export interface AvlHistoryRecord {
+ id: number
+ rev: number
+ createdAt: string
+ createdBy: string
+ vendorInfoSnapshot: VendorSnapshot[] // JSON 데이터
+ changeDescription?: string
+}
+
+// 스냅샷 테이블 컴포넌트
+interface SnapshotTableProps {
+ snapshot: VendorSnapshot[]
+ isOpen: boolean
+ onToggle: () => void
+}
+
+function SnapshotTable({ snapshot, isOpen, onToggle }: SnapshotTableProps) {
+ if (!snapshot || snapshot.length === 0) {
+ return (
+ <div className="text-sm text-muted-foreground">
+ 스냅샷 데이터가 없습니다.
+ </div>
+ )
+ }
+
+ return (
+ <Collapsible open={isOpen} onOpenChange={onToggle}>
+ <CollapsibleTrigger asChild>
+ <Button variant="outline" size="sm" className="w-full justify-between">
+ <span>벤더 상세 정보 ({snapshot.length}개)</span>
+ {isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
+ </Button>
+ </CollapsibleTrigger>
+ <CollapsibleContent className="mt-3">
+ <div className="border rounded-lg">
+ <div className="overflow-auto max-h-[400px]">
+ <Table>
+ <TableHeader className="sticky top-0 bg-background z-10">
+ <TableRow>
+ <TableHead className="w-[60px]">No.</TableHead>
+ <TableHead className="w-[100px]">설계공종</TableHead>
+ <TableHead>고객사 AVL 자재명</TableHead>
+ <TableHead className="w-[120px]">자재그룹 코드</TableHead>
+ <TableHead className="w-[130px]">자재그룹 명</TableHead>
+ <TableHead>AVL 등재업체명</TableHead>
+ <TableHead className="w-[120px]">협력업체 코드</TableHead>
+ <TableHead className="w-[130px]">협력업체 명</TableHead>
+ <TableHead className="w-[80px]">선주제안</TableHead>
+ <TableHead className="w-[80px]">SHI 제안</TableHead>
+ <TableHead className="w-[100px]">본사 위치</TableHead>
+ <TableHead className="w-[80px]">등급</TableHead>
+ <TableHead className="w-[60px]">AVL</TableHead>
+ <TableHead className="w-[80px]">FA대상</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {snapshot.map((item, index) => (
+ <TableRow key={item.id || index}>
+ <TableCell className="font-mono text-xs text-center">{index + 1}</TableCell>
+ <TableCell className="text-sm">{item.disciplineName || '-'}</TableCell>
+ <TableCell className="text-sm">{item.materialNameCustomerSide || '-'}</TableCell>
+ <TableCell className="font-mono text-xs">{item.materialGroupCode || '-'}</TableCell>
+ <TableCell className="text-sm">{item.materialGroupName || '-'}</TableCell>
+ <TableCell className="font-medium text-sm">{item.avlVendorName || '-'}</TableCell>
+ <TableCell className="font-mono text-xs">{item.vendorCode || '-'}</TableCell>
+ <TableCell className="font-medium text-sm">{item.vendorName || '-'}</TableCell>
+ <TableCell>
+ <Badge variant={item.ownerSuggestion ? "default" : "secondary"} className="text-xs">
+ {item.ownerSuggestion ? "예" : "아니오"}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ <Badge variant={item.shiSuggestion ? "default" : "secondary"} className="text-xs">
+ {item.shiSuggestion ? "예" : "아니오"}
+ </Badge>
+ </TableCell>
+ <TableCell className="text-xs">{item.headquarterLocation || '-'}</TableCell>
+ <TableCell>
+ {item.tier ? (
+ <Badge variant="outline" className="text-xs">
+ {item.tier}
+ </Badge>
+ ) : '-'}
+ </TableCell>
+ <TableCell>
+ <Badge variant={item.hasAvl ? "default" : "secondary"} className="text-xs">
+ {item.hasAvl ? "Y" : "N"}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ <Badge variant={item.faTarget ? "default" : "secondary"} className="text-xs">
+ {item.faTarget ? "Y" : "N"}
+ </Badge>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ </div>
+ </CollapsibleContent>
+ </Collapsible>
+ )
+}
+
+export function AvlHistoryModal({
+ isOpen,
+ onClose,
+ avlItem,
+ historyData,
+ onLoadHistory
+}: AvlHistoryModalProps) {
+ const [loading, setLoading] = React.useState(false)
+ const [history, setHistory] = React.useState<AvlHistoryRecord[]>([])
+ const [openSnapshots, setOpenSnapshots] = React.useState<Record<number, boolean>>({})
+
+ // 히스토리 데이터 로드
+ React.useEffect(() => {
+ if (isOpen && avlItem && onLoadHistory) {
+ setLoading(true)
+ onLoadHistory(avlItem)
+ .then(setHistory)
+ .catch(console.error)
+ .finally(() => setLoading(false))
+ } else if (historyData) {
+ setHistory(historyData)
+ }
+ }, [isOpen, avlItem, onLoadHistory, historyData])
+
+ // 스냅샷 테이블 토글 함수
+ const toggleSnapshot = (recordId: number) => {
+ setOpenSnapshots(prev => ({
+ ...prev,
+ [recordId]: !prev[recordId]
+ }))
+ }
+
+ if (!avlItem) return null
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent className="max-w-7xl h-[90vh] flex flex-col">
+ <DialogHeader className="flex-shrink-0">
+ <DialogTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ AVL 리비전 히스토리
+ </DialogTitle>
+ <div className="text-sm text-muted-foreground">
+ {avlItem.isTemplate ? "표준 AVL" : "프로젝트 AVL"} - {avlItem.avlKind}
+ {avlItem.projectCode && ` (${avlItem.projectCode})`}
+ </div>
+ </DialogHeader>
+
+ <div className="flex-1 overflow-auto min-h-0">
+ <div className="pr-4">
+ {loading ? (
+ <div className="flex items-center justify-center h-[300px]">
+ <div className="text-muted-foreground">히스토리를 불러오는 중...</div>
+ </div>
+ ) : history.length === 0 ? (
+ <div className="flex items-center justify-center h-[300px]">
+ <div className="text-muted-foreground">히스토리 데이터가 없습니다.</div>
+ </div>
+ ) : (
+ <div className="space-y-4 py-4">
+ {history.map((record, index) => (
+ <div
+ key={record.id}
+ className={`p-4 border rounded-lg ${
+ index === 0 ? "border-primary bg-primary/5" : "border-border"
+ }`}
+ >
+ {/* 리비전 헤더 */}
+ <div className="flex items-center justify-between mb-3">
+ <div className="flex items-center gap-2">
+ <Badge
+ variant={index === 0 ? "default" : "outline"}
+ className="font-mono"
+ >
+ Rev {record.rev}
+ </Badge>
+ {index === 0 && (
+ <Badge variant="secondary" className="text-xs">
+ 현재
+ </Badge>
+ )}
+ </div>
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
+ <div className="flex items-center gap-1">
+ <Calendar className="h-4 w-4" />
+ {new Date(record.createdAt).toLocaleDateString('ko-KR')}
+ </div>
+ </div>
+ </div>
+
+ {/* 변경 설명 */}
+ {record.changeDescription && (
+ <div className="mb-3 p-2 bg-muted/50 rounded text-sm">
+ {record.changeDescription}
+ </div>
+ )}
+
+ {/* Vendor Info 요약 */}
+ <div className="grid grid-cols-3 gap-4 text-sm">
+ <div className="text-center">
+ <div className="font-medium text-lg">
+ {record.vendorInfoSnapshot?.length || 0}
+ </div>
+ <div className="text-muted-foreground">총 협력업체</div>
+ </div>
+ <div className="text-center">
+ <div className="font-medium text-lg">
+ {record.vendorInfoSnapshot?.filter(v => v.hasAvl).length || 0}
+ </div>
+ <div className="text-muted-foreground">AVL 등재</div>
+ </div>
+ <div className="text-center">
+ <div className="font-medium text-lg">
+ {record.vendorInfoSnapshot?.filter(v => v.faTarget).length || 0}
+ </div>
+ <div className="text-muted-foreground">FA 대상</div>
+ </div>
+ </div>
+
+ {/* 스냅샷 테이블 */}
+ <div className="mt-3 pt-3 border-t">
+ <SnapshotTable
+ snapshot={record.vendorInfoSnapshot || []}
+ isOpen={openSnapshots[record.id] || false}
+ onToggle={() => toggleSnapshot(record.id)}
+ />
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div className="flex justify-end pt-4 border-t flex-shrink-0 mt-4">
+ <Button variant="outline" onClick={onClose}>
+ 닫기
+ </Button>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file