diff options
Diffstat (limited to 'lib/vendor-document-list/table')
4 files changed, 451 insertions, 246 deletions
diff --git a/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx index c8487d82..191ce3e2 100644 --- a/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx +++ b/lib/vendor-document-list/table/enhanced-doc-table-columns.tsx @@ -1,4 +1,4 @@ -// updated-enhanced-doc-table-columns.tsx +// enhanced-doc-table-columns-with-b4.tsx "use client" import * as React from "react" @@ -32,13 +32,15 @@ import { Edit, Trash2, Building, - Code + Code, + Settings } from "lucide-react" import { cn } from "@/lib/utils" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EnhancedDocumentsView> | null>> projectType: string | null + drawingKindFilter?: string // ✅ 추가 } // 유틸리티 함수들 @@ -140,11 +142,12 @@ const DueDateInfo = ({ export function getUpdatedEnhancedColumns({ setRowAction, - projectType + projectType, + drawingKindFilter = "all" // ✅ 추가 }: GetColumnsProps): ColumnDef<EnhancedDocumentsView>[] { const isPlantProject = projectType === "plant" + const showB4Columns = drawingKindFilter === "B4" // ✅ B4 컬럼 표시 여부 - // 기본 컬럼들 const baseColumns: ColumnDef<EnhancedDocumentsView>[] = [ // 체크박스 선택 @@ -174,7 +177,7 @@ export function getUpdatedEnhancedColumns({ enableHiding: false, }, - // 문서번호 + 우선순위 + // 문서번호 + Drawing Kind { accessorKey: "docNumber", header: ({ column }) => ( @@ -185,6 +188,11 @@ export function getUpdatedEnhancedColumns({ return ( <div className="flex flex-col gap-1 items-start"> <span className="font-mono text-sm font-medium">{doc.docNumber}</span> + {doc.drawingKind && ( + <Badge variant="outline" className="text-xs"> + {doc.drawingKind} + </Badge> + )} </div> ) }, @@ -196,7 +204,7 @@ export function getUpdatedEnhancedColumns({ }, ] - // ✅ Ship 프로젝트용 추가 컬럼들 + // ✅ Plant 프로젝트용 추가 컬럼들 const plantColumns: ColumnDef<EnhancedDocumentsView>[] = isPlantProject ? [ // 벤더 문서번호 { @@ -233,7 +241,6 @@ export function getUpdatedEnhancedColumns({ const doc = row.original return ( <div className="flex items-center gap-2"> - {/* <Code className="w-4 h-4 text-gray-500" /> */} <span className="font-mono text-sm font-medium text-gray-700"> {doc.projectCode || '-'} </span> @@ -258,7 +265,6 @@ export function getUpdatedEnhancedColumns({ return ( <div className="flex flex-col gap-1 items-start"> <div className="flex items-center gap-2"> - {/* <Building className="w-4 h-4 text-gray-500" /> */} <span className="text-sm font-medium text-gray-900"> {doc.vendorName || '-'} </span> @@ -279,6 +285,116 @@ export function getUpdatedEnhancedColumns({ }, ] : [] + // ✅ B4 전용 컬럼들 (B4 필터 선택 시에만 표시) + const b4Columns: ColumnDef<EnhancedDocumentsView>[] = showB4Columns ? [ + { + accessorKey: "cGbn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="cGbn" /> + ), + cell: ({ row }) => { + const doc = row.original + return ( + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {doc.cGbn || '-'} + </Badge> + </div> + ) + }, + size: 100, + enableResizing: true, + meta: { + excelHeader: "cGbn" + }, + }, + { + accessorKey: "dGbn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="dGbn" /> + ), + cell: ({ row }) => { + const doc = row.original + return ( + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {doc.dGbn || '-'} + </Badge> + </div> + ) + }, + size: 100, + enableResizing: true, + meta: { + excelHeader: "dGbn" + }, + }, + { + accessorKey: "degreeGbn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="degreeGbn" /> + ), + cell: ({ row }) => { + const doc = row.original + return ( + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {doc.degreeGbn || '-'} + </Badge> + </div> + ) + }, + size: 100, + enableResizing: true, + meta: { + excelHeader: "degreeGbn" + }, + }, + { + accessorKey: "deptGbn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="deptGbn" /> + ), + cell: ({ row }) => { + const doc = row.original + return ( + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {doc.deptGbn || '-'} + </Badge> + </div> + ) + }, + size: 100, + enableResizing: true, + meta: { + excelHeader: "deptGbn" + }, + }, + { + accessorKey: "sGbn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="sGbn" /> + ), + cell: ({ row }) => { + const doc = row.original + return ( + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {doc.sGbn || '-'} + </Badge> + </div> + ) + }, + size: 100, + enableResizing: true, + meta: { + excelHeader: "sGbn" + }, + }, + + ] : [] + // 나머지 공통 컬럼들 const commonColumns: ColumnDef<EnhancedDocumentsView>[] = [ // 문서명 + 담당자 @@ -310,7 +426,7 @@ export function getUpdatedEnhancedColumns({ </div> ) }, - size: isPlantProject ? 200 : 250, // Ship 프로젝트일 때는 너비 조정 + size: showB4Columns ? 180 : (isPlantProject ? 200 : 250), // ✅ B4 컬럼이 있을 때 너비 조정 enableResizing: true, meta: { excelHeader: "문서명" @@ -378,7 +494,7 @@ export function getUpdatedEnhancedColumns({ size: 140, enableResizing: true, meta: { - excelHeader: "계획일" + excelHeader: "일정" }, }, @@ -476,7 +592,6 @@ export function getUpdatedEnhancedColumns({ const canApprove = doc.currentStageStatus === 'SUBMITTED' const isPlantProject = projectType === "plant" - // 메뉴 아이템들을 그룹별로 정의 const viewActions = [ { key: "view", @@ -519,7 +634,6 @@ export function getUpdatedEnhancedColumns({ } ] - // 각 그룹에서 표시될 아이템이 있는지 확인 const hasEditActions = editActions.some(action => action.show) const hasFileActions = fileActions.some(action => action.show) const hasDangerActions = dangerActions.some(action => action.show) @@ -536,7 +650,6 @@ export function getUpdatedEnhancedColumns({ </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-48"> - {/* 기본 액션 그룹 */} {viewActions.map(action => action.show && ( <DropdownMenuItem key={action.key} @@ -551,7 +664,6 @@ export function getUpdatedEnhancedColumns({ </DropdownMenuItem> ))} - {/* 편집 액션 그룹 */} {hasEditActions && ( <> <DropdownMenuSeparator /> @@ -571,7 +683,6 @@ export function getUpdatedEnhancedColumns({ </> )} - {/* 파일 액션 그룹 */} {hasFileActions && ( <> <DropdownMenuSeparator /> @@ -591,7 +702,6 @@ export function getUpdatedEnhancedColumns({ </> )} - {/* 위험한 액션 그룹 */} {hasDangerActions && ( <> <DropdownMenuSeparator /> @@ -621,84 +731,8 @@ export function getUpdatedEnhancedColumns({ // ✅ 모든 컬럼을 순서대로 결합 return [ ...baseColumns, // 체크박스, 문서번호 - ...plantColumns, // Ship 전용 컬럼들 (조건부) + ...plantColumns, // Plant 전용 컬럼들 (조건부) + ...b4Columns, // B4 전용 컬럼들 (조건부) ...commonColumns // 나머지 공통 컬럼들 ] -} - -// 확장된 행 컨텐츠 컴포넌트 (업데이트된 버전) -export const UpdatedExpandedRowContent = ({ - document -}: { - document: EnhancedDocumentsView -}) => { - if (!document.allStages || document.allStages.length === 0) { - return ( - <div className="p-4 text-sm text-gray-500 italic"> - 스테이지 정보가 없습니다. - </div> - ) - } - - return ( - <div className="p-4 w-1/2"> - <h4 className="font-medium mb-3 flex items-center gap-2"> - <FileText className="w-4 h-4" /> - 전체 스테이지 현황 - </h4> - - <div className="grid gap-3"> - {document.allStages.map((stage, index) => ( - <div key={stage.id} className="flex items-center justify-between p-3 bg-white rounded-lg border"> - <div className="flex items-center gap-3"> - <div className="flex items-center gap-2"> - <div className="w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center text-xs font-medium"> - {stage.stageOrder || index + 1} - </div> - <div className={cn( - "w-3 h-3 rounded-full", - stage.stageStatus === 'COMPLETED' ? 'bg-green-500' : - stage.stageStatus === 'IN_PROGRESS' ? 'bg-blue-500' : - stage.stageStatus === 'SUBMITTED' ? 'bg-purple-500' : - 'bg-gray-300' - )} /> - </div> - - <div> - <div className="font-medium text-sm">{stage.stageName}</div> - {stage.assigneeName && ( - <div className="text-xs text-gray-500 flex items-center gap-1 mt-1"> - <User className="w-3 h-3" /> - {stage.assigneeName} - </div> - )} - </div> - </div> - - <div className="flex items-center gap-4 text-sm"> - <div> - <span className="text-gray-500">계획: </span> - <span>{formatDate(stage.planDate)}</span> - </div> - {stage.actualDate && ( - <div> - <span className="text-gray-500">완료: </span> - <span>{formatDate(stage.actualDate)}</span> - </div> - )} - - <div className="flex items-center gap-2"> - <Badge variant={getPriorityColor(stage.priority)} className="text-xs"> - {getPriorityText(stage.priority)} - </Badge> - <Badge variant={getStatusColor(stage.stageStatus)} className="text-xs"> - {getStatusText(stage.stageStatus)} - </Badge> - </div> - </div> - </div> - ))} - </div> - </div> - ) }
\ No newline at end of file diff --git a/lib/vendor-document-list/table/enhanced-documents-table.tsx b/lib/vendor-document-list/table/enhanced-documents-table.tsx index 3bd6668d..cb49f796 100644 --- a/lib/vendor-document-list/table/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/table/enhanced-documents-table.tsx @@ -1,6 +1,7 @@ +// enhanced-documents-table-with-drawing-filter.tsx "use client" -import * as React from "react" +import React from "react" import type { DataTableAdvancedFilterField, DataTableFilterField, @@ -10,7 +11,6 @@ import type { import { useDataTable } from "@/hooks/use-data-table" import { StageRevisionExpandedContent } from "./stage-revision-expanded-content" import { RevisionUploadDialog } from "./revision-upload-dialog" -// ✅ UpdateDocumentSheet import 추가 import { EnhancedDocTableToolbarActions } from "./enhanced-doc-table-toolbar-actions" import { getEnhancedDocuments } from "../enhanced-document-service" import type { EnhancedDocument } from "@/types/enhanced-documents" @@ -23,69 +23,102 @@ import { TrendingUp, Target, Users, + Settings, + Filter } from "lucide-react" import { getUpdatedEnhancedColumns } from "./enhanced-doc-table-columns" import { ExpandableDataTable } from "@/components/data-table/expandable-data-table" import { toast } from "sonner" import { UpdateDocumentSheet } from "./update-doc-sheet" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Label } from "@/components/ui/label" interface FinalIntegratedDocumentsTableProps { promises: Promise<[Awaited<ReturnType<typeof getEnhancedDocuments>>]> selectedPackageId: number projectType: "ship" | "plant" - // ✅ contractId 추가 (AddDocumentListDialog에서 필요) contractId: number + initialDrawingKind?: string } +// ✅ Drawing Kind 옵션 정의 +const DRAWING_KIND_OPTIONS = [ + { value: "all", label: "전체 문서" }, + { value: "B3", label: "B3: Vendor" }, + { value: "B4", label: "B4: GTT" }, + { value: "B5", label: "B5: FMEA" }, +] as const + export function EnhancedDocumentsTable({ promises, selectedPackageId, projectType, - contractId, // ✅ contractId 추가 + contractId, + initialDrawingKind = "all" }: FinalIntegratedDocumentsTableProps) { - // 데이터 로딩 const [{ data, pageCount, total }] = React.use(promises) - - // 상태 관리 + // ✅ Drawing Kind 필터 상태 추가 + const [drawingKindFilter, setDrawingKindFilter] = React.useState<string>(initialDrawingKind) + + // 기존 상태들 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>>>({}) - - // ✅ 다이얼로그 상태들 - editDialogOpen -> editSheetOpen으로 변경 const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false) - const [editSheetOpen, setEditSheetOpen] = React.useState(false) // Sheet로 변경 + const [editSheetOpen, setEditSheetOpen] = React.useState(false) const [selectedDocument, setSelectedDocument] = React.useState<EnhancedDocument | null>(null) const [selectedStage, setSelectedStage] = React.useState<string>("") const [selectedRevision, setSelectedRevision] = React.useState<string>("") const [uploadMode, setUploadMode] = React.useState<'new' | 'append'>('new') + // ✅ Drawing Kind별 데이터 필터링 + const filteredByDrawingKind = React.useMemo(() => { + if (drawingKindFilter === "all") return data + return data.filter(doc => doc.drawingKind === drawingKindFilter) + }, [data, drawingKindFilter]) + + // ✅ Drawing Kind별 통계 계산 + const drawingKindStats = React.useMemo(() => { + const stats = DRAWING_KIND_OPTIONS.reduce((acc, option) => { + if (option.value === "all") { + acc[option.value] = data.length + } else { + acc[option.value] = data.filter(doc => doc.drawingKind === option.value).length + } + return acc + }, {} as Record<string, number>) + + return stats + }, [data]) + // 다음 리비전 계산 함수 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가 아닌 경우 + if (charCode < 90) { return String.fromCharCode(charCode + 1) } - return "AA" // Z 다음은 AA + return "AA" } - // 숫자 리비전 (1, 2, 3...) if (/^\d+$/.test(currentRevision)) { return String(parseInt(currentRevision) + 1) } - // 기타 복잡한 리비전 형태는 그대로 반환 return currentRevision }, []) - // 컬럼 정의 + // ✅ 컬럼 정의 - drawingKindFilter 추가 const columns = React.useMemo( () => getUpdatedEnhancedColumns({ setRowAction: (action) => { @@ -93,17 +126,15 @@ export function EnhancedDocumentsTable({ if (action) { setSelectedDocument(action.row.original) - // 액션 타입에 따른 다이얼로그 열기 switch (action.type) { case "update": - setEditSheetOpen(true) // ✅ Sheet 열기로 변경 + setEditSheetOpen(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)) { @@ -116,24 +147,25 @@ export function EnhancedDocumentsTable({ } } }, - projectType + projectType, + drawingKindFilter // ✅ 추가 }), - [expandedRows, projectType] + [expandedRows, projectType, drawingKindFilter] ) - // 통계 계산 + // 기존 통계 계산 const stats = React.useMemo(() => { - const totalDocs = data.length - const overdue = data.filter(doc => doc.isOverdue).length - const dueSoon = data.filter(doc => + const totalDocs = filteredByDrawingKind.length + const overdue = filteredByDrawingKind.filter(doc => doc.isOverdue).length + const dueSoon = filteredByDrawingKind.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 inProgress = filteredByDrawingKind.filter(doc => doc.currentStageStatus === 'IN_PROGRESS').length + const highPriority = filteredByDrawingKind.filter(doc => doc.currentStagePriority === 'HIGH').length const avgProgress = totalDocs > 0 - ? Math.round(data.reduce((sum, doc) => sum + (doc.progressPercentage || 0), 0) / totalDocs) + ? Math.round(filteredByDrawingKind.reduce((sum, doc) => sum + (doc.progressPercentage || 0), 0) / totalDocs) : 0 return { @@ -144,66 +176,60 @@ export function EnhancedDocumentsTable({ highPriority, avgProgress } - }, [data]) + }, [filteredByDrawingKind]) // 빠른 필터링 const filteredData = React.useMemo(() => { switch (quickFilter) { case 'overdue': - return data.filter(doc => doc.isOverdue) + return filteredByDrawingKind.filter(doc => doc.isOverdue) case 'due_soon': - return data.filter(doc => + return filteredByDrawingKind.filter(doc => doc.daysUntilDue !== null && doc.daysUntilDue >= 0 && doc.daysUntilDue <= 3 ) case 'in_progress': - return data.filter(doc => doc.currentStageStatus === 'IN_PROGRESS') + return filteredByDrawingKind.filter(doc => doc.currentStageStatus === 'IN_PROGRESS') case 'high_priority': - return data.filter(doc => doc.currentStagePriority === 'HIGH') + return filteredByDrawingKind.filter(doc => doc.currentStagePriority === 'HIGH') default: - return data + return filteredByDrawingKind } - }, [data, quickFilter]) + }, [filteredByDrawingKind, quickFilter]) - // ✅ 핸들러 함수 수정: 모드 매개변수 추가 + // 나머지 핸들러 함수들 const handleUploadRevision = React.useCallback((document: EnhancedDocument, stageName?: string, currentRevision?: string, mode: 'new' | 'append' = 'new') => { setSelectedDocument(document) setSelectedStage(stageName || document.currentStageName || "") - setUploadMode(mode) // ✅ 모드 설정 + 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") // 첫 번째 리비전 + 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) @@ -212,20 +238,17 @@ export function EnhancedDocumentsTable({ } else if (!aIsAlpha && !bIsAlpha) { return parseInt(a.revision) - parseInt(b.revision) } else { - return aIsAlpha ? -1 : 1 // 알파벳이 숫자보다 먼저 + return aIsAlpha ? -1 : 1 } }) return sortedRevisions[sortedRevisions.length - 1]?.revision || null }, []) - // ✅ 새 문서 추가 핸들러 - EnhancedDocTableToolbarActions에서 AddDocumentListDialog를 직접 렌더링하므로 별도 상태 관리 불필요 const handleNewDocument = () => { // AddDocumentListDialog는 자체적으로 Dialog trigger를 가지므로 별도 처리 불필요 - // EnhancedDocTableToolbarActions에서 처리됨 } - // ✅ 스테이지 토글 핸들러 추가 const handleStageToggle = React.useCallback((documentId: string, stageId: number) => { setExpandedStages(prev => ({ ...prev, @@ -239,17 +262,14 @@ export function EnhancedDocumentsTable({ 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) { @@ -257,27 +277,25 @@ export function EnhancedDocumentsTable({ } } - // ✅ 다이얼로그 닫기 함수 수정 const closeAllDialogs = () => { setUploadDialogOpen(false) - setEditSheetOpen(false) // editDialogOpen -> editSheetOpen + setEditSheetOpen(false) setSelectedDocument(null) setSelectedStage("") setSelectedRevision("") - setUploadMode('new') // ✅ 모드 초기화 + setUploadMode('new') setRowAction(null) } - // ✅ EnhancedDocument를 UpdateDocumentSheet의 document 형식으로 변환하는 함수 const convertToUpdateFormat = React.useCallback((doc: EnhancedDocument | null) => { if (!doc) return null return { id: doc.documentId, - contractId: contractId, // contractId 사용 + contractId: contractId, docNumber: doc.docNumber, title: doc.title, - status: doc.status || "pending", // 기본값 설정 + status: doc.status || "pending", description: doc.description || null, remarks: doc.remarks || null, } @@ -309,6 +327,16 @@ export function EnhancedDocumentsTable({ type: "text", }, { + id: "drawingKind", + label: "문서종류", + type: "select", + options: [ + { label: "B3", value: "B3" }, + { label: "B4", value: "B4" }, + { label: "B5", value: "B5" }, + ], + }, + { id: "currentStageStatus", label: "스테이지 상태", type: "select", @@ -351,7 +379,6 @@ export function EnhancedDocumentsTable({ }, ] - // 데이터 테이블 훅 const { table } = useDataTable({ data: filteredData, columns, @@ -371,11 +398,15 @@ export function EnhancedDocumentsTable({ 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> + <CardTitle className="text-sm font-medium"> + {drawingKindFilter === "all" ? "전체 문서" : `${DRAWING_KIND_OPTIONS.find(o => o.value === drawingKindFilter)?.label} 문서`} + </CardTitle> <TrendingUp className="h-4 w-4 text-muted-foreground" /> </CardHeader> <CardContent> @@ -420,80 +451,107 @@ export function EnhancedDocumentsTable({ </Card> </div> - {/* 빠른 필터 */} - <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 className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center"> + {/* 왼쪽: 빠른 필터 */} + <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 items-center gap-2 flex-shrink-0"> + <Select value={drawingKindFilter} onValueChange={setDrawingKindFilter}> + <SelectTrigger className="w-[140px]"> + <SelectValue placeholder="문서 종류" /> + </SelectTrigger> + <SelectContent> + {DRAWING_KIND_OPTIONS.map(option => ( + <SelectItem key={option.value} value={option.value}> + <div className="flex items-center justify-between w-full"> + <span>{option.label}</span> + <Badge variant="outline" className="ml-2 text-xs"> + {drawingKindStats[option.value] || 0} + </Badge> + </div> + </SelectItem> + ))} + </SelectContent> + </Select> + + {/* B4 필드 표시 안내 (아이콘만) */} + {drawingKindFilter === "B4" && ( + <div className="flex items-center gap-1 text-blue-600 bg-blue-50 px-2 py-1 rounded text-xs"> + <Settings className="h-3 w-3" /> + <span className="hidden sm:inline">상세정보 확장가능</span> + </div> + )} + </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=""> - <StageRevisionExpandedContent - document={document} - onUploadRevision={handleUploadRevision} - projectType={projectType} - expandedStages={expandedStages[String(document.documentId)] || {}} - onStageToggle={(stageId) => handleStageToggle(String(document.documentId), stageId)} - /> - </div> - )} - expandedRowClassName="!p-0" - // clickableColumns={[ - // 'docNumber', - // 'title', - // 'currentStageStatus', - // 'progressPercentage', - // ]} - excludeFromClick={[ - 'actions', - 'select' - ]} - > + <ExpandableDataTable + table={table} + expandable={true} + expandedRows={expandedRows} + setExpandedRows={setExpandedRows} + renderExpandedContent={(document) => ( + <div className=""> + <StageRevisionExpandedContent + document={document} + onUploadRevision={handleUploadRevision} + projectType={projectType} + expandedStages={expandedStages[String(document.documentId)] || {}} + onStageToggle={(stageId) => handleStageToggle(String(document.documentId), stageId)} + // showB4Fields={document.drawingKind === "B4"} + /> + </div> + )} + expandedRowClassName="!p-0" + excludeFromClick={[ + 'actions', + 'select' + ]} + > <DataTableAdvancedToolbar table={table} filterFields={advancedFilterFields} @@ -503,7 +561,7 @@ export function EnhancedDocumentsTable({ table={table} projectType={projectType} selectedPackageId={selectedPackageId} - contractId={contractId} // ✅ contractId 추가 + contractId={contractId} onNewDocument={handleNewDocument} onBulkAction={handleBulkAction} /> @@ -512,9 +570,7 @@ export function EnhancedDocumentsTable({ </div> </div> - {/* ✅ 분리된 다이얼로그들 - UpdateDocumentSheet와 AddDocumentListDialog로 교체 */} - - {/* 리비전 업로드 다이얼로그 - mode props 추가 */} + {/* 다이얼로그들 */} <RevisionUploadDialog open={uploadDialogOpen} onOpenChange={(open) => { @@ -528,7 +584,6 @@ export function EnhancedDocumentsTable({ mode={uploadMode} /> - {/* ✅ 문서 편집 Sheet로 교체 */} <UpdateDocumentSheet open={editSheetOpen} onOpenChange={(open) => { diff --git a/lib/vendor-document-list/table/revision-upload-dialog.tsx b/lib/vendor-document-list/table/revision-upload-dialog.tsx index 546fa7a3..16fc9fbb 100644 --- a/lib/vendor-document-list/table/revision-upload-dialog.tsx +++ b/lib/vendor-document-list/table/revision-upload-dialog.tsx @@ -66,7 +66,29 @@ const revisionUploadSchema = z.object({ uploaderName: z.string().optional(), comment: z.string().optional(), attachments: z.array(z.instanceof(File)).min(1, "최소 1개 파일이 필요합니다"), -}) + // ✅ B3 문서용 usage 필드 추가 + usage: z.string().optional(), +}).refine((data) => { + // B3 문서이고 특정 stage인 경우 usage 필수 + // 이 검증은 컴포넌트 내에서 조건부로 처리 + return true; +}, { + message: "Usage는 필수입니다", + path: ["usage"], +}); + +const getUsageOptions = (stageName: string): string[] => { + const stageNameLower = stageName.toLowerCase(); + + if (stageNameLower.includes('approval')) { + return ['Approval (Partial)', 'Approval (Full)']; + } else if (stageNameLower.includes('working')) { + return ['Working (Partial)', 'Working (Full)']; + } + + return []; +}; + type RevisionUploadSchema = z.infer<typeof revisionUploadSchema> @@ -93,7 +115,7 @@ export function RevisionUploadDialog({ presetStage, presetRevision, mode = 'new', - onUploadComplete, // ✅ 추가된 prop + onUploadComplete, }: RevisionUploadDialogProps) { const targetSystem = React.useMemo( @@ -106,7 +128,6 @@ export function RevisionUploadDialog({ const [uploadProgress, setUploadProgress] = React.useState(0) const router = useRouter() - // ✅ next-auth session 가져오기 const { data: session } = useSession() // 사용 가능한 스테이지 옵션 @@ -125,17 +146,33 @@ export function RevisionUploadDialog({ uploaderName: session?.user?.name || "", comment: "", attachments: [], + usage: "", // ✅ usage 기본값 추가 }, }) - // ✅ session이 로드되면 uploaderName 업데이트 + // ✅ 현재 선택된 stage 값을 watch + const currentStage = form.watch('stage') + + // ✅ B3 문서 여부 확인 + const isB3Document = document?.drawingKind === 'B3' + + // ✅ 현재 stage에 따른 usage 옵션 + const usageOptions = React.useMemo(() => { + if (!isB3Document || !currentStage) return [] + return getUsageOptions(currentStage) + }, [isB3Document, currentStage]) + + // ✅ usage 필드가 필요한지 확인 + const isUsageRequired = isB3Document && usageOptions.length > 0 + + // session이 로드되면 uploaderName 업데이트 React.useEffect(() => { if (session?.user?.name) { form.setValue('uploaderName', session.user.name) } }, [session?.user?.name, form]) - // ✅ presetStage와 presetRevision이 변경될 때 폼 값 업데이트 + // presetStage와 presetRevision이 변경될 때 폼 값 업데이트 React.useEffect(() => { if (presetStage) { form.setValue('stage', presetStage) @@ -145,6 +182,22 @@ export function RevisionUploadDialog({ } }, [presetStage, presetRevision, form]) + // ✅ stage가 변경될 때 usage 값 리셋 + React.useEffect(() => { + if (isB3Document) { + const newUsageOptions = getUsageOptions(currentStage) + if (newUsageOptions.length === 0) { + form.setValue('usage', '') + } else { + // 기존 값이 새로운 옵션에 없으면 리셋 + const currentUsage = form.getValues('usage') + if (currentUsage && !newUsageOptions.includes(currentUsage)) { + form.setValue('usage', '') + } + } + } + }, [currentStage, isB3Document, form]) + // 파일 드롭 처리 const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles] @@ -159,26 +212,22 @@ export function RevisionUploadDialog({ form.setValue('attachments', updatedFiles, { shouldValidate: true }) } - // ✅ 캐시 갱신 함수 + // 캐시 갱신 함수 const refreshCaches = async () => { try { - // 1. 서버 컴포넌트 캐시 갱신 (Enhanced Documents 등) router.refresh() - // 2. SWR 캐시 갱신 (Sync Status) if (document?.contractId) { await mutate(`/api/sync/status/${document.contractId}/${targetSystem}`) console.log('✅ Sync status cache refreshed') } - // 3. 다른 관련 SWR 캐시들도 갱신 (필요시) await mutate(key => typeof key === 'string' && key.includes('sync') && key.includes(String(document?.contractId)) ) - // 4. 상위 컴포넌트 콜백 호출 onUploadComplete?.() console.log('✅ All caches refreshed after upload') @@ -187,10 +236,19 @@ export function RevisionUploadDialog({ } } - // 업로드 처리 + // ✅ 업로드 처리 - usage 필드 검증 및 전송 async function onSubmit(data: RevisionUploadSchema) { if (!document) return + // ✅ B3 문서에서 usage가 필요한 경우 검증 + if (isUsageRequired && !data.usage) { + form.setError('usage', { + type: 'required', + message: 'Usage 선택은 필수입니다' + }) + return + } + setIsUploading(true) setUploadProgress(0) @@ -210,6 +268,11 @@ export function RevisionUploadDialog({ formData.append("comment", data.comment) } + // ✅ B3 문서인 경우 usage 추가 + if (isB3Document && data.usage) { + formData.append("usage", data.usage) + } + // 파일들 추가 data.attachments.forEach((file) => { formData.append("attachments", file) @@ -220,7 +283,6 @@ export function RevisionUploadDialog({ setUploadProgress(Math.min(progress, 95)) } - // 파일 크기에 따른 진행률 시뮬레이션 const totalSize = data.attachments.reduce((sum, file) => sum + file.size, 0) let uploadedSize = 0 @@ -230,7 +292,6 @@ export function RevisionUploadDialog({ updateProgress(progress) }, 300) - // ✅ 실제 API 호출 const response = await fetch('/api/revision-upload', { method: 'POST', body: formData, @@ -253,7 +314,6 @@ export function RevisionUploadDialog({ console.log('✅ 업로드 성공:', result) - // ✅ 캐시 갱신 및 다이얼로그 닫기 setTimeout(async () => { await refreshCaches() handleDialogClose() @@ -275,6 +335,7 @@ export function RevisionUploadDialog({ uploaderName: session?.user?.name || "", comment: "", attachments: [], + usage: "", // ✅ usage 리셋 추가 }) setSelectedFiles([]) setIsUploading(false) @@ -295,14 +356,19 @@ export function RevisionUploadDialog({ mode === 'new' ? "문서에 새 리비전을 업로드합니다." : "기존 리비전에 파일을 추가합니다."} </DialogDescription> - <div className="flex items-center gap-2 pt-2"> + <div className="flex items-center gap-2 pt-2 flex-wrap"> <Badge variant={projectType === "ship" ? "default" : "secondary"}> {projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} </Badge> - {/* ✅ 타겟 시스템 표시 추가 */} <Badge variant="outline" className="text-xs"> → {targetSystem} </Badge> + {/* ✅ B3 문서 표시 */} + {isB3Document && ( + <Badge variant="outline" className="text-xs bg-orange-50 text-orange-700 border-orange-200"> + B3 문서 + </Badge> + )} {session?.user?.name && ( <Badge variant="outline" className="text-xs"> 업로더: {session.user.name} @@ -379,6 +445,40 @@ export function RevisionUploadDialog({ /> </div> + {/* ✅ B3 문서용 Usage 필드 - 조건부 표시 */} + {isB3Document && usageOptions.length > 0 && ( + <FormField + control={form.control} + name="usage" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center gap-2"> + 용도 + {isUsageRequired && <span className="text-red-500">*</span>} + </FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="용도를 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {usageOptions.map((usage) => ( + <SelectItem key={usage} value={usage}> + {usage} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + <p className="text-xs text-gray-500"> + {currentStage} 스테이지에 필요한 용도를 선택하세요. + </p> + </FormItem> + )} + /> + )} + <FormField control={form.control} name="uploaderName" diff --git a/lib/vendor-document-list/table/stage-revision-expanded-content.tsx b/lib/vendor-document-list/table/stage-revision-expanded-content.tsx index a4de03b7..6b9cffb9 100644 --- a/lib/vendor-document-list/table/stage-revision-expanded-content.tsx +++ b/lib/vendor-document-list/table/stage-revision-expanded-content.tsx @@ -494,6 +494,9 @@ export const StageRevisionExpandedContent = ({ <TableHead className="w-16 py-1 px-2 text-xs"></TableHead> <TableHead className="w-16 py-1 px-2 text-xs">리비전</TableHead> <TableHead className="w-20 py-1 px-2 text-xs">상태</TableHead> + {documentData.drawingKind === 'B3' && ( + <TableHead className="w-24 py-1 px-2 text-xs">용도</TableHead> + )} <TableHead className="w-24 py-1 px-2 text-xs">업로더</TableHead> <TableHead className="w-32 py-1 px-2 text-xs">등록일</TableHead> <TableHead className="w-32 py-1 px-2 text-xs">제출일</TableHead> @@ -529,6 +532,19 @@ export const StageRevisionExpandedContent = ({ </Badge> </TableCell> + {/* ✅ B3 문서일 때만 Usage 셀 표시 */} + {documentData.drawingKind === 'B3' && ( + <TableCell className="py-1 px-2"> + {revision.usage ? ( + <span className="text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded border border-blue-200"> + {revision.usage} + </span> + ) : ( + <span className="text-gray-400 text-xs">-</span> + )} + </TableCell> + )} + {/* 업로더 */} <TableCell className="py-1 px-2"> <div className="flex items-center gap-1"> |
