diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-28 00:32:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-28 00:32:31 +0000 |
| commit | 20800b214145ee6056f94ca18fa1054f145eb977 (patch) | |
| tree | b5c8b27febe5b126e6d9ece115ea05eace33a020 /lib/vendor-document-list/table/enhanced-documents-table copy.tsx | |
| parent | e1344a5da1aeef8fbf0f33e1dfd553078c064ccc (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.tsx | 604 |
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 |
