summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/table/enhanced-documents-table copy.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 00:32:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 00:32:31 +0000
commit20800b214145ee6056f94ca18fa1054f145eb977 (patch)
treeb5c8b27febe5b126e6d9ece115ea05eace33a020 /lib/vendor-document-list/table/enhanced-documents-table copy.tsx
parente1344a5da1aeef8fbf0f33e1dfd553078c064ccc (diff)
(대표님) lib 파트 커밋
Diffstat (limited to 'lib/vendor-document-list/table/enhanced-documents-table copy.tsx')
-rw-r--r--lib/vendor-document-list/table/enhanced-documents-table copy.tsx604
1 files changed, 604 insertions, 0 deletions
diff --git a/lib/vendor-document-list/table/enhanced-documents-table copy.tsx b/lib/vendor-document-list/table/enhanced-documents-table copy.tsx
new file mode 100644
index 00000000..2ac871db
--- /dev/null
+++ b/lib/vendor-document-list/table/enhanced-documents-table copy.tsx
@@ -0,0 +1,604 @@
+"use client"
+
+import * as React from "react"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { StageRevisionExpandedContent } from "./stage-revision-expanded-content"
+import { RevisionUploadDialog } from "./revision-upload-dialog"
+import { SimplifiedDocumentEditDialog } from "./simplified-document-edit-dialog"
+import { getEnhancedDocuments } from "../enhanced-document-service"
+import type { EnhancedDocument } from "@/types/enhanced-documents"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import {
+ AlertTriangle,
+ Clock,
+ TrendingUp,
+ Target,
+ Users,
+ Plus,
+ Upload,
+ CheckCircle,
+ Edit,
+ Eye,
+ Settings
+} from "lucide-react"
+import { getUpdatedEnhancedColumns } from "./enhanced-doc-table-columns"
+import { ExpandableDataTable } from "@/components/data-table/expandable-data-table"
+import { toast } from "sonner"
+
+interface FinalIntegratedDocumentsTableProps {
+ promises: Promise<[Awaited<ReturnType<typeof getEnhancedDocuments>>]>
+ selectedPackageId: number
+ projectType: "ship" | "plant"
+}
+
+export function EnhancedDocumentsTable({
+ promises,
+ selectedPackageId,
+ projectType,
+}: FinalIntegratedDocumentsTableProps) {
+ // 데이터 로딩
+ const [{ data, pageCount, total }] = React.use(promises)
+
+ // 상태 관리
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<EnhancedDocument> | null>(null)
+ const [expandedRows, setExpandedRows] = React.useState<Set<string>>(new Set())
+ const [quickFilter, setQuickFilter] = React.useState<'all' | 'overdue' | 'due_soon' | 'in_progress' | 'high_priority'>('all')
+
+ // ✅ 스테이지 확장 상태 관리 (문서별로 관리)
+ const [expandedStages, setExpandedStages] = React.useState<Record<string, Record<number, boolean>>>({})
+
+ // 다이얼로그 상태들
+ const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false)
+ const [editDialogOpen, setEditDialogOpen] = React.useState(false)
+ const [viewDialogOpen, setViewDialogOpen] = React.useState(false)
+ const [selectedDocument, setSelectedDocument] = React.useState<EnhancedDocument | null>(null)
+ const [selectedStage, setSelectedStage] = React.useState<string>("")
+ const [selectedRevision, setSelectedRevision] = React.useState<string>("")
+ const [selectedRevisions, setSelectedRevisions] = React.useState<any[]>([])
+ const [uploadMode, setUploadMode] = React.useState<'new' | 'append'>('new')
+
+ // 다음 리비전 계산 함수
+ const getNextRevision = React.useCallback((currentRevision: string): string => {
+ if (!currentRevision) return "A"
+
+ // 알파벳 리비전 (A, B, C...)
+ if (/^[A-Z]$/.test(currentRevision)) {
+ const charCode = currentRevision.charCodeAt(0)
+ if (charCode < 90) { // Z가 아닌 경우
+ return String.fromCharCode(charCode + 1)
+ }
+ return "AA" // Z 다음은 AA
+ }
+
+ // 숫자 리비전 (1, 2, 3...)
+ if (/^\d+$/.test(currentRevision)) {
+ return String(parseInt(currentRevision) + 1)
+ }
+
+ // 기타 복잡한 리비전 형태는 그대로 반환
+ return currentRevision
+ }, [])
+
+ // 컬럼 정의
+ const columns = React.useMemo(
+ () => getUpdatedEnhancedColumns({
+ setRowAction: (action) => {
+ setRowAction(action)
+ if (action) {
+ setSelectedDocument(action.row.original)
+
+ // 액션 타입에 따른 다이얼로그 열기
+ switch (action.type) {
+ case "update":
+ setEditDialogOpen(true)
+ break
+ case "upload":
+ setSelectedStage(action.row.original.currentStageName || "")
+ setUploadDialogOpen(true)
+ break
+ case "view":
+ // 상세보기는 확장된 행으로 대체
+ const rowId = action.row.id
+ const newExpanded = new Set(expandedRows)
+ if (newExpanded.has(rowId)) {
+ newExpanded.delete(rowId)
+ } else {
+ newExpanded.add(rowId)
+ }
+ setExpandedRows(newExpanded)
+ break
+ }
+ }
+ }
+ }),
+ [expandedRows]
+ )
+
+ // 통계 계산
+ const stats = React.useMemo(() => {
+ const totalDocs = data.length
+ const overdue = data.filter(doc => doc.isOverdue).length
+ const dueSoon = data.filter(doc =>
+ doc.daysUntilDue !== null &&
+ doc.daysUntilDue >= 0 &&
+ doc.daysUntilDue <= 3
+ ).length
+ const inProgress = data.filter(doc => doc.currentStageStatus === 'IN_PROGRESS').length
+ const highPriority = data.filter(doc => doc.currentStagePriority === 'HIGH').length
+ const avgProgress = totalDocs > 0
+ ? Math.round(data.reduce((sum, doc) => sum + (doc.progressPercentage || 0), 0) / totalDocs)
+ : 0
+
+ return {
+ total: totalDocs,
+ overdue,
+ dueSoon,
+ inProgress,
+ highPriority,
+ avgProgress
+ }
+ }, [data])
+
+ // 빠른 필터링
+ const filteredData = React.useMemo(() => {
+ switch (quickFilter) {
+ case 'overdue':
+ return data.filter(doc => doc.isOverdue)
+ case 'due_soon':
+ return data.filter(doc =>
+ doc.daysUntilDue !== null &&
+ doc.daysUntilDue >= 0 &&
+ doc.daysUntilDue <= 3
+ )
+ case 'in_progress':
+ return data.filter(doc => doc.currentStageStatus === 'IN_PROGRESS')
+ case 'high_priority':
+ return data.filter(doc => doc.currentStagePriority === 'HIGH')
+ default:
+ return data
+ }
+ }, [data, quickFilter])
+
+ // ✅ 핸들러 함수 수정: 모드 매개변수 추가
+ const handleUploadRevision = React.useCallback((document: EnhancedDocument, stageName?: string, currentRevision?: string, mode: 'new' | 'append' = 'new') => {
+ setSelectedDocument(document)
+ setSelectedStage(stageName || document.currentStageName || "")
+ setUploadMode(mode) // ✅ 모드 설정
+
+ if (mode === 'new') {
+ // 새 리비전 생성: currentRevision이 있으면 다음 리비전을 자동 계산
+ if (currentRevision) {
+ const nextRevision = getNextRevision(currentRevision)
+ setSelectedRevision(nextRevision)
+ } else {
+ // 스테이지의 최신 리비전을 찾아서 다음 리비전 계산
+ const latestRevision = findLatestRevisionInStage(document, stageName || document.currentStageName || "")
+ if (latestRevision) {
+ setSelectedRevision(getNextRevision(latestRevision))
+ } else {
+ setSelectedRevision("A") // 첫 번째 리비전
+ }
+ }
+ } else {
+ // 기존 리비전에 파일 추가: 같은 리비전 번호 사용
+ setSelectedRevision(currentRevision || "")
+ }
+
+ setUploadDialogOpen(true)
+ }, [getNextRevision])
+
+ // ✅ 스테이지에서 최신 리비전을 찾는 헬퍼 함수
+ const findLatestRevisionInStage = React.useCallback((document: EnhancedDocument, stageName: string) => {
+ const stage = document.allStages?.find(s => s.stageName === stageName)
+ if (!stage || !stage.revisions || stage.revisions.length === 0) {
+ return null
+ }
+
+ // 리비전들을 정렬해서 최신 것 찾기 (간단한 알파벳/숫자 정렬)
+ const sortedRevisions = [...stage.revisions].sort((a, b) => {
+ // 알파벳과 숫자를 구분해서 정렬
+ const aIsAlpha = /^[A-Z]+$/.test(a.revision)
+ const bIsAlpha = /^[A-Z]+$/.test(b.revision)
+
+ if (aIsAlpha && bIsAlpha) {
+ return a.revision.localeCompare(b.revision)
+ } else if (!aIsAlpha && !bIsAlpha) {
+ return parseInt(a.revision) - parseInt(b.revision)
+ } else {
+ return aIsAlpha ? -1 : 1 // 알파벳이 숫자보다 먼저
+ }
+ })
+
+ return sortedRevisions[sortedRevisions.length - 1]?.revision || null
+ }, [])
+
+ const handleEditDocument = (document: EnhancedDocument) => {
+ setSelectedDocument(document)
+ setEditDialogOpen(true)
+ }
+
+ const handleViewRevisions = (revisions: any[]) => {
+ setSelectedRevisions(revisions)
+ setViewDialogOpen(true)
+ }
+
+ const handleNewDocument = () => {
+ setSelectedDocument(null)
+ setEditDialogOpen(true)
+ }
+
+ // ✅ 스테이지 토글 핸들러 추가
+ const handleStageToggle = React.useCallback((documentId: string, stageId: number) => {
+ setExpandedStages(prev => ({
+ ...prev,
+ [documentId]: {
+ ...prev[documentId],
+ [stageId]: !prev[documentId]?.[stageId]
+ }
+ }))
+ }, [])
+
+ const handleBulkAction = async (action: string, selectedRows: any[]) => {
+ try {
+ if (action === 'bulk_approve') {
+ // 일괄 승인 로직
+ const stageIds = selectedRows
+ .map(row => row.original.currentStageId)
+ .filter(Boolean)
+
+ if (stageIds.length > 0) {
+ // await bulkUpdateStageStatus(stageIds, 'APPROVED')
+ toast.success(`${stageIds.length}개 항목이 승인되었습니다.`)
+ }
+ } else if (action === 'bulk_upload') {
+ // 일괄 업로드 로직
+ toast.info("일괄 업로드 기능은 준비 중입니다.")
+ }
+ } catch (error) {
+ toast.error("일괄 작업 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 다이얼로그 닫기
+ const closeAllDialogs = () => {
+ setUploadDialogOpen(false)
+ setEditDialogOpen(false)
+ setViewDialogOpen(false)
+ setSelectedDocument(null)
+ setSelectedStage("")
+ setSelectedRevision("")
+ setSelectedRevisions([])
+ setUploadMode('new') // ✅ 모드 초기화
+ setRowAction(null)
+ }
+
+ // 필터 필드 정의
+ const filterFields: DataTableFilterField<EnhancedDocument>[] = [
+ {
+ label: "문서번호",
+ value: "docNumber",
+ placeholder: "문서번호로 검색...",
+ },
+ {
+ label: "제목",
+ value: "title",
+ placeholder: "제목으로 검색...",
+ },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<EnhancedDocument>[] = [
+ {
+ id: "docNumber",
+ label: "문서번호",
+ type: "text",
+ },
+ {
+ id: "title",
+ label: "문서제목",
+ type: "text",
+ },
+ {
+ id: "currentStageStatus",
+ label: "스테이지 상태",
+ type: "select",
+ options: [
+ { label: "계획됨", value: "PLANNED" },
+ { label: "진행중", value: "IN_PROGRESS" },
+ { label: "제출됨", value: "SUBMITTED" },
+ { label: "승인됨", value: "APPROVED" },
+ { label: "완료됨", value: "COMPLETED" },
+ ],
+ },
+ {
+ id: "currentStagePriority",
+ label: "우선순위",
+ type: "select",
+ options: [
+ { label: "높음", value: "HIGH" },
+ { label: "보통", value: "MEDIUM" },
+ { label: "낮음", value: "LOW" },
+ ],
+ },
+ {
+ id: "isOverdue",
+ label: "지연 여부",
+ type: "select",
+ options: [
+ { label: "지연됨", value: "true" },
+ { label: "정상", value: "false" },
+ ],
+ },
+ {
+ id: "currentStageAssigneeName",
+ label: "담당자",
+ type: "text",
+ },
+ {
+ id: "createdAt",
+ label: "생성일",
+ type: "date",
+ },
+ ]
+
+ // 데이터 테이블 훅
+ const { table } = useDataTable({
+ data: filteredData,
+ columns,
+ pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.documentId),
+ shallow: false,
+ clearOnDefault: true,
+ columnResizeMode: "onEnd",
+ })
+
+ return (
+ <div className="space-y-6">
+ {/* 통계 대시보드 */}
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+ <Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('all')}>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">전체 문서</CardTitle>
+ <TrendingUp className="h-4 w-4 text-muted-foreground" />
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{stats.total}</div>
+ <p className="text-xs text-muted-foreground">
+ 총 {total}개 중 {stats.total}개 표시
+ </p>
+ </CardContent>
+ </Card>
+
+ <Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('overdue')}>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">지연 문서</CardTitle>
+ <AlertTriangle className="h-4 w-4 text-red-500" />
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-red-600">{stats.overdue}</div>
+ <p className="text-xs text-muted-foreground">즉시 확인 필요</p>
+ </CardContent>
+ </Card>
+
+ <Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('due_soon')}>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">마감 임박</CardTitle>
+ <Clock className="h-4 w-4 text-orange-500" />
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-orange-600">{stats.dueSoon}</div>
+ <p className="text-xs text-muted-foreground">3일 이내 마감</p>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">평균 진행률</CardTitle>
+ <Target className="h-4 w-4 text-green-500" />
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-green-600">{stats.avgProgress}%</div>
+ <p className="text-xs text-muted-foreground">전체 프로젝트 진행도</p>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 빠른 필터 및 액션 버튼 */}
+ <div className="flex flex-col sm:flex-row gap-4 justify-between">
+ {/* 빠른 필터 */}
+ <div className="flex gap-2 overflow-x-auto pb-2">
+ <Badge
+ variant={quickFilter === 'all' ? 'default' : 'outline'}
+ className="cursor-pointer hover:bg-primary hover:text-primary-foreground whitespace-nowrap"
+ onClick={() => setQuickFilter('all')}
+ >
+ 전체 ({stats.total})
+ </Badge>
+ <Badge
+ variant={quickFilter === 'overdue' ? 'destructive' : 'outline'}
+ className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground whitespace-nowrap"
+ onClick={() => setQuickFilter('overdue')}
+ >
+ <AlertTriangle className="w-3 h-3 mr-1" />
+ 지연 ({stats.overdue})
+ </Badge>
+ <Badge
+ variant={quickFilter === 'due_soon' ? 'default' : 'outline'}
+ className="cursor-pointer hover:bg-orange-500 hover:text-white whitespace-nowrap"
+ onClick={() => setQuickFilter('due_soon')}
+ >
+ <Clock className="w-3 h-3 mr-1" />
+ 마감임박 ({stats.dueSoon})
+ </Badge>
+ <Badge
+ variant={quickFilter === 'in_progress' ? 'default' : 'outline'}
+ className="cursor-pointer hover:bg-blue-500 hover:text-white whitespace-nowrap"
+ onClick={() => setQuickFilter('in_progress')}
+ >
+ <Users className="w-3 h-3 mr-1" />
+ 진행중 ({stats.inProgress})
+ </Badge>
+ <Badge
+ variant={quickFilter === 'high_priority' ? 'destructive' : 'outline'}
+ className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground whitespace-nowrap"
+ onClick={() => setQuickFilter('high_priority')}
+ >
+ <Target className="w-3 h-3 mr-1" />
+ 높은우선순위 ({stats.highPriority})
+ </Badge>
+ </div>
+
+ {/* 메인 액션 버튼들 */}
+ <div className="flex gap-2 flex-shrink-0">
+ {projectType === "plant" && (
+ <Button onClick={handleNewDocument} className="flex items-center gap-2">
+ <Plus className="w-4 h-4" />
+ 새 문서
+ </Button>
+ )}
+
+ <Button variant="outline" onClick={() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ if (selectedRows.length > 0) {
+ handleBulkAction('bulk_approve', selectedRows)
+ } else {
+ toast.info("승인할 항목을 선택해주세요.")
+ }
+ }}>
+ <CheckCircle className="w-4 h-4 mr-2" />
+ 일괄 승인
+ </Button>
+
+ <Button variant="outline" onClick={() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ if (selectedRows.length > 0) {
+ handleBulkAction('bulk_upload', selectedRows)
+ } else {
+ toast.info("업로드할 항목을 선택해주세요.")
+ }
+ }}>
+ <Upload className="w-4 h-4 mr-2" />
+ 일괄 업로드
+ </Button>
+ </div>
+ </div>
+
+ {/* 메인 테이블 - 가로스크롤 문제 해결을 위한 구조 개선 */}
+ <div className="space-y-4">
+ <div className="rounded-md border bg-white overflow-hidden">
+ <ExpandableDataTable
+ table={table}
+ expandable={true}
+ expandedRows={expandedRows}
+ setExpandedRows={setExpandedRows}
+ renderExpandedContent={(document) => (
+ <div className="w-full bg-gray-50 border-t">
+ {/* 👇 새 래퍼: 뷰포트 폭을 상한으로, 내부에만 스크롤 */}
+ <div className="max-w-full overflow-x-auto">
+ <StageRevisionExpandedContent
+ document={document}
+ onUploadRevision={handleUploadRevision}
+ onViewRevision={handleViewRevisions}
+ projectType={projectType}
+ expandedStages={expandedStages[String(document.documentId)] || {}}
+ onStageToggle={(stageId) =>
+ handleStageToggle(String(document.documentId), stageId)
+ }
+ />
+ </div>
+ </div>
+ )}
+ expandedRowClassName="!p-0"
+ >
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ />
+ </ExpandableDataTable>
+ </div>
+
+ {/* 선택된 항목 정보 */}
+ {/* {table.getFilteredSelectedRowModel().rows.length > 0 && (
+ <div className="flex items-center justify-between p-4 bg-blue-50 border border-blue-200 rounded-lg">
+ <span className="text-sm text-blue-700">
+ {table.getFilteredSelectedRowModel().rows.length}개 항목이 선택되었습니다
+ </span>
+ <div className="flex gap-2">
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => table.toggleAllRowsSelected(false)}
+ >
+ 선택 해제
+ </Button>
+ <Button
+ size="sm"
+ onClick={() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ handleBulkAction('bulk_approve', selectedRows)
+ }}
+ >
+ 선택 항목 승인
+ </Button>
+ </div>
+ </div>
+ )} */}
+ </div>
+
+ {/* 분리된 다이얼로그들 */}
+
+ {/* ✅ 리비전 업로드 다이얼로그 - mode props 추가 */}
+ <RevisionUploadDialog
+ open={uploadDialogOpen}
+ onOpenChange={(open) => {
+ if (!open) closeAllDialogs()
+ else setUploadDialogOpen(open)
+ }}
+ document={selectedDocument}
+ projectType={projectType}
+ presetStage={selectedStage}
+ presetRevision={selectedRevision}
+ mode={uploadMode}
+ />
+
+ {/* 문서 편집 다이얼로그 */}
+ <SimplifiedDocumentEditDialog
+ open={editDialogOpen}
+ onOpenChange={(open) => {
+ if (!open) closeAllDialogs()
+ else setEditDialogOpen(open)
+ }}
+ document={selectedDocument}
+ projectType={projectType}
+ />
+
+ {/* PDF 뷰어 다이얼로그 (기존 ViewDocumentDialog 재사용) */}
+ {/*
+ <ViewDocumentDialog
+ open={viewDialogOpen}
+ onOpenChange={(open) => {
+ if (!open) closeAllDialogs()
+ else setViewDialogOpen(open)
+ }}
+ revisions={selectedRevisions}
+ />
+ */}
+ </div>
+ )
+} \ No newline at end of file