From c72d0897f7b37843109c86f61d97eba05ba3ca0d Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 13 Jun 2025 07:08:01 +0000 Subject: (대표님) 20250613 16시 08분 b-rfq, document 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enhanced-document-service.ts | 400 ++++++++- .../ship/enhanced-doc-table-columns.tsx | 427 ++++++++++ .../ship/enhanced-doc-table-toolbar-actions.tsx | 147 ++++ .../ship/enhanced-document-sheet.tsx | 939 +++++++++++++++++++++ .../ship/enhanced-documents-table.tsx | 238 ++++++ .../ship/import-from-dolce-button.tsx | 356 ++++++++ .../ship/revision-upload-dialog.tsx | 629 ++++++++++++++ .../ship/send-to-shi-button.tsx | 348 ++++++++ .../ship/simplified-document-edit-dialog.tsx | 287 +++++++ .../ship/stage-revision-expanded-content.tsx | 752 +++++++++++++++++ .../ship/stage-revision-sheet.tsx | 86 ++ .../ship/swp-workflow-panel.tsx | 370 ++++++++ lib/vendor-document-list/ship/update-doc-sheet.tsx | 267 ++++++ .../table/bulk-upload-dialog.tsx | 884 +++++++++---------- lib/vendor-document-list/validations.ts | 25 +- 15 files changed, 5713 insertions(+), 442 deletions(-) create mode 100644 lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx create mode 100644 lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx create mode 100644 lib/vendor-document-list/ship/enhanced-document-sheet.tsx create mode 100644 lib/vendor-document-list/ship/enhanced-documents-table.tsx create mode 100644 lib/vendor-document-list/ship/import-from-dolce-button.tsx create mode 100644 lib/vendor-document-list/ship/revision-upload-dialog.tsx create mode 100644 lib/vendor-document-list/ship/send-to-shi-button.tsx create mode 100644 lib/vendor-document-list/ship/simplified-document-edit-dialog.tsx create mode 100644 lib/vendor-document-list/ship/stage-revision-expanded-content.tsx create mode 100644 lib/vendor-document-list/ship/stage-revision-sheet.tsx create mode 100644 lib/vendor-document-list/ship/swp-workflow-panel.tsx create mode 100644 lib/vendor-document-list/ship/update-doc-sheet.tsx (limited to 'lib/vendor-document-list') diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index 66758b89..e01283dc 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -2,9 +2,9 @@ "use server" import { revalidatePath, unstable_cache } from "next/cache" -import { and, asc, desc, eq, ilike, or, count, avg } from "drizzle-orm" +import { and, asc, desc, eq, ilike, or, count, avg, inArray } from "drizzle-orm" import db from "@/db/db" -import { documentAttachments, documents, enhancedDocumentsView, issueStages, revisions, type EnhancedDocumentsView } from "@/db/schema/vendorDocu" +import { documentAttachments, documents, enhancedDocumentsView, issueStages, revisions, simplifiedDocumentsView, type EnhancedDocumentsView } from "@/db/schema/vendorDocu" import { filterColumns } from "@/lib/filter-columns" import type { CreateDocumentInput, @@ -19,6 +19,8 @@ import type { DocumentAttachment, Revision } from "@/types/enhanced-documents" +import { GetVendorShipDcoumentsSchema } from "./validations" +import { contracts, users } from "@/db/schema" // 스키마 타입 정의 export interface GetEnhancedDocumentsSchema { @@ -156,6 +158,225 @@ export async function getEnhancedDocuments( // )() } +export async function getEnhancedDocumentsShip( + input: GetVendorShipDcoumentsSchema, +) { + try { + const offset = (input.page - 1) * input.perPage + + // 고급 필터 처리 + const advancedWhere = filterColumns({ + table: simplifiedDocumentsView, + filters: input.filters || [], + joinOperator: input.joinOperator || "and", + }) + + // 전역 검색 처리 + let globalWhere + if (input.search) { + const searchTerm = `%${input.search}%` + globalWhere = or( + ilike(simplifiedDocumentsView.title, searchTerm), + ilike(simplifiedDocumentsView.docNumber, searchTerm), + ilike(simplifiedDocumentsView.projectCode, searchTerm), + ilike(simplifiedDocumentsView.vendorDocNumber, searchTerm), + ) + } + + // 최종 WHERE 조건 + const finalWhere = and( + advancedWhere, + globalWhere, + ) + + // 정렬 처리 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(simplifiedDocumentsView[item.id]) + : asc(simplifiedDocumentsView[item.id]) + ) + : [desc(simplifiedDocumentsView.createdAt)] + + // 트랜잭션 실행 + const { data, total } = await db.transaction(async (tx) => { + // 데이터 조회 + const data = await tx + .select() + .from(simplifiedDocumentsView) + .where(finalWhere) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset) + + // 총 개수 조회 + const [{ total }] = await tx + .select({ + total: count() + }) + .from(simplifiedDocumentsView) + .where(finalWhere) + + return { data, total } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { data, pageCount, total } + } catch (err) { + console.error("Error fetching enhanced documents:", err) + return { data: [], pageCount: 0, total: 0 } + } +} + +// contractId로 필터링이 필요한 경우를 위한 추가 함수 +export async function getEnhancedDocumentsShipByContract( + input: GetVendorShipDcoumentsSchema, + contractId: number +) { + try { + const offset = (input.page - 1) * input.perPage + + // 고급 필터 처리 + const advancedWhere = filterColumns({ + table: simplifiedDocumentsView, + filters: input.filters || [], + joinOperator: input.joinOperator || "and", + }) + + // 전역 검색 처리 + let globalWhere + if (input.search) { + const searchTerm = `%${input.search}%` + globalWhere = or( + ilike(simplifiedDocumentsView.title, searchTerm), + ilike(simplifiedDocumentsView.docNumber, searchTerm), + ilike(simplifiedDocumentsView.projectCode, searchTerm), + ilike(simplifiedDocumentsView.vendorDocNumber, searchTerm), + ) + } + + // 최종 WHERE 조건 (contractId 포함) + const finalWhere = and( + eq(simplifiedDocumentsView.contractId, contractId), + advancedWhere, + globalWhere, + ) + + // 정렬 처리 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(simplifiedDocumentsView[item.id]) + : asc(simplifiedDocumentsView[item.id]) + ) + : [desc(simplifiedDocumentsView.createdAt)] + + // 트랜잭션 실행 + const { data, total } = await db.transaction(async (tx) => { + // 데이터 조회 + const data = await tx + .select() + .from(simplifiedDocumentsView) + .where(finalWhere) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset) + + // 총 개수 조회 + const [{ total }] = await tx + .select({ + total: count() + }) + .from(simplifiedDocumentsView) + .where(finalWhere) + + return { data, total } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { data, pageCount, total } + } catch (err) { + console.error("Error fetching enhanced documents by contract:", err) + return { data: [], pageCount: 0, total: 0 } + } +} + +// drawingKind별로 문서를 조회하는 함수 +export async function getEnhancedDocumentsShipByDrawingKind( + input: GetVendorShipDcoumentsSchema, + drawingKind: 'B3' | 'B4' | 'B5' +) { + try { + const offset = (input.page - 1) * input.perPage + + // 고급 필터 처리 + const advancedWhere = filterColumns({ + table: simplifiedDocumentsView, + filters: input.filters || [], + joinOperator: input.joinOperator || "and", + }) + + // 전역 검색 처리 + let globalWhere + if (input.search) { + const searchTerm = `%${input.search}%` + globalWhere = or( + ilike(simplifiedDocumentsView.title, searchTerm), + ilike(simplifiedDocumentsView.docNumber, searchTerm), + ilike(simplifiedDocumentsView.projectCode, searchTerm), + ilike(simplifiedDocumentsView.vendorDocNumber, searchTerm), + ) + } + + // 최종 WHERE 조건 (drawingKind 포함) + const finalWhere = and( + eq(simplifiedDocumentsView.drawingKind, drawingKind), + advancedWhere, + globalWhere, + ) + + // 정렬 처리 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(simplifiedDocumentsView[item.id]) + : asc(simplifiedDocumentsView[item.id]) + ) + : [desc(simplifiedDocumentsView.createdAt)] + + // 트랜잭션 실행 + const { data, total } = await db.transaction(async (tx) => { + // 데이터 조회 + const data = await tx + .select() + .from(simplifiedDocumentsView) + .where(finalWhere) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset) + + // 총 개수 조회 + const [{ total }] = await tx + .select({ + total: count() + }) + .from(simplifiedDocumentsView) + .where(finalWhere) + + return { data, total } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { data, pageCount, total } + } catch (err) { + console.error(`Error fetching enhanced documents for drawing kind ${drawingKind}:`, err) + return { data: [], pageCount: 0, total: 0 } + } +} + // 통계 데이터 가져오기 export async function getDocumentStatistics(contractId: number) { // return unstable_cache( @@ -779,4 +1000,179 @@ export async function getDocumentDetails(documentId: number) { error: "일괄 상태 업데이트 중 오류가 발생했습니다." } } + } + + + export async function getUserVendorDocuments( + userId: number, + input: GetVendorShipDcoumentsSchema + ) { + try { + const offset = (input.page - 1) * input.perPage + + // 1. 사용자의 벤더(회사) ID 조회 + const [user] = await db + .select({ companyId: users.companyId }) + .from(users) + .where(eq(users.id, userId)) + .limit(1) + + if (!user?.companyId) { + return { data: [], pageCount: 0, total: 0, drawingKind: null, vendorInfo: null } + } + + // 2. 해당 벤더의 모든 계약 ID들 조회 + const vendorContracts = await db + .select({ id: contracts.id }) + .from(contracts) + .where(eq(contracts.vendorId, user.companyId)) + + const contractIds = vendorContracts.map(c => c.id) + + if (contractIds.length === 0) { + return { data: [], pageCount: 0, total: 0, drawingKind: null, vendorInfo: null } + } + + // 3. 고급 필터 처리 + const advancedWhere = filterColumns({ + table: simplifiedDocumentsView, + filters: input.filters || [], + joinOperator: input.joinOperator || "and", + }) + + // 4. 전역 검색 처리 + let globalWhere + if (input.search) { + const searchTerm = `%${input.search}%` + globalWhere = or( + ilike(simplifiedDocumentsView.title, searchTerm), + ilike(simplifiedDocumentsView.docNumber, searchTerm), + ilike(simplifiedDocumentsView.vendorDocNumber, searchTerm), + ) + } + + // 5. 최종 WHERE 조건 (계약 ID들로 필터링) + const finalWhere = and( + inArray(simplifiedDocumentsView.contractId, contractIds), + advancedWhere, + globalWhere, + ) + + // 6. 정렬 처리 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(simplifiedDocumentsView[item.id]) + : asc(simplifiedDocumentsView[item.id]) + ) + : [desc(simplifiedDocumentsView.createdAt)] + + // 7. 트랜잭션 실행 + const { data, total, drawingKind, vendorInfo } = await db.transaction(async (tx) => { + // 데이터 조회 + const data = await tx + .select() + .from(simplifiedDocumentsView) + .where(finalWhere) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset) + + // 총 개수 조회 + const [{ total }] = await tx + .select({ + total: count() + }) + .from(simplifiedDocumentsView) + .where(finalWhere) + + // DrawingKind 분석 (첫 번째 문서의 drawingKind 사용) + const drawingKind = data.length > 0 ? data[0].drawingKind : null + + // 벤더 정보 조회 + const [vendorInfo] = await tx + .select({ + vendorName: simplifiedDocumentsView.vendorName, + vendorCode: simplifiedDocumentsView.vendorCode, + }) + .from(simplifiedDocumentsView) + .where(eq(simplifiedDocumentsView.contractId, contractIds[0])) + .limit(1) + + return { data, total, drawingKind, vendorInfo } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { + data, + pageCount, + total, + drawingKind: drawingKind as 'B3' | 'B4' | 'B5' | null, + vendorInfo: vendorInfo || null + } + } catch (err) { + console.error("Error fetching user vendor documents:", err) + return { data: [], pageCount: 0, total: 0, drawingKind: null, vendorInfo: null } + } + } + + /** + * DrawingKind별 문서 통계 조회 + */ + export async function getUserVendorDocumentStats(userId: number) { + try { + // 사용자의 벤더 ID 조회 + const [user] = await db + .select({ companyId: users.companyId }) + .from(users) + .where(eq(users.id, userId)) + .limit(1) + + if (!user?.companyId) { + return { stats: {}, totalDocuments: 0, primaryDrawingKind: null } + } + + // 해당 벤더의 계약 ID들 조회 + const vendorContracts = await db + .select({ id: contracts.id }) + .from(contracts) + .where(eq(contracts.vendorId, user.companyId)) + + const contractIds = vendorContracts.map(c => c.id) + + if (contractIds.length === 0) { + return { stats: {}, totalDocuments: 0, primaryDrawingKind: null } + } + + // DrawingKind별 통계 조회 + const documents = await db + .select({ + drawingKind: simplifiedDocumentsView.drawingKind, + contractId: simplifiedDocumentsView.contractId, + }) + .from(simplifiedDocumentsView) + .where(inArray(simplifiedDocumentsView.contractId, contractIds)) + + // 통계 계산 + const stats = documents.reduce((acc, doc) => { + if (doc.drawingKind) { + acc[doc.drawingKind] = (acc[doc.drawingKind] || 0) + 1 + } + return acc + }, {} as Record) + + // 가장 많은 DrawingKind 찾기 + const primaryDrawingKind = Object.entries(stats) + .sort(([,a], [,b]) => b - a)[0]?.[0] as 'B3' | 'B4' | 'B5' | undefined + + return { + stats, + totalDocuments: documents.length, + primaryDrawingKind: primaryDrawingKind || null + } + } catch (err) { + console.error("Error fetching user vendor document stats:", err) + return { stats: {}, totalDocuments: 0, primaryDrawingKind: null } + } } \ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx new file mode 100644 index 00000000..b80c0869 --- /dev/null +++ b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx @@ -0,0 +1,427 @@ +// simplified-doc-table-columns.tsx +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { formatDate, formatDateTime } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { DataTableRowAction } from "@/types/table" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + Ellipsis, + Calendar, + CalendarClock, + User, + FileText, + Eye, + Edit, + Trash2, + Building, + Code, + Settings +} from "lucide-react" +import { cn } from "@/lib/utils" +import { SimplifiedDocumentsView } from "@/db/schema" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +// 유틸리티 함수들 +const getDrawingKindText = (drawingKind: string) => { + switch (drawingKind) { + case 'B3': return 'B3 도면' + case 'B4': return 'B4 도면' + case 'B5': return 'B5 도면' + default: return drawingKind + } +} + +const getDrawingKindColor = (drawingKind: string) => { + switch (drawingKind) { + case 'B3': return 'bg-blue-100 text-blue-800' + case 'B4': return 'bg-green-100 text-green-800' + case 'B5': return 'bg-purple-100 text-purple-800' + default: return 'bg-gray-100 text-gray-800' + } +} + +// 스테이지별 이름 표시 컴포넌트 +const StageNameDisplay = ({ + stageName, + drawingKind, + isFirst = true +}: { + stageName: string | null, + drawingKind: string | null, + isFirst?: boolean +}) => { + if (!stageName) return - + + const stageType = isFirst ? "1차" : "2차" + const getExpectedStage = () => { + if (drawingKind === 'B4') return isFirst ? 'Pre' : 'Work' + if (drawingKind === 'B3') return isFirst ? 'Approval' : 'Work' + if (drawingKind === 'B5') return isFirst ? 'First' : 'Second' + return '' + } + + return ( +
+
{stageType} 스테이지
+
{stageName}
+ {getExpectedStage() && ( +
({getExpectedStage()})
+ )} +
+ ) +} + +// 날짜 정보 표시 컴포넌트 +const StageDateInfo = ({ + planDate, + actualDate, + stageName +}: { + planDate: string | null + actualDate: string | null + stageName: string | null +}) => { + if (!planDate && !actualDate) { + return 날짜 미설정 + } + + const isCompleted = !!actualDate + const isLate = actualDate && planDate && new Date(actualDate) > new Date(planDate) + + return ( +
+ {planDate && ( +
+ 계획: + {formatDate(planDate)} +
+ )} + {actualDate && ( +
+ 실제: + + {formatDate(actualDate)} + +
+ )} + {!actualDate && planDate && ( +
+ 진행중 +
+ )} + {isCompleted && ( +
+ ✓ 완료 +
+ )} +
+ ) +} + +export function getSimplifiedDocumentColumns({ + setRowAction, +}: GetColumnsProps): ColumnDef[] { + + // 기본 컬럼들 + const baseColumns: ColumnDef[] = [ + // 체크박스 선택 + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // 문서번호 + Drawing Kind + { + accessorKey: "docNumber", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + return ( +
+ {doc.docNumber} + {doc.vendorDocNumber && ( + + 벤더: {doc.vendorDocNumber} + + )} + {doc.drawingKind && ( + + {getDrawingKindText(doc.drawingKind)} + + )} +
+ ) + }, + size: 140, + enableResizing: true, + meta: { + excelHeader: "문서번호" + }, + }, + + // 문서명 + 프로젝트/벤더 정보 + { + accessorKey: "title", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + return ( +
+
+ {doc.title} +
+
+ {doc.pic && ( + + PIC: {doc.pic} + + )} + {doc.projectCode && ( +
+ + {doc.projectCode} +
+ )} + {doc.vendorName && ( +
+ + {doc.vendorName} +
+ )} +
+
+ ) + }, + size: 200, + enableResizing: true, + meta: { + excelHeader: "문서명" + }, + }, + + // 첫 번째 스테이지 정보 + { + accessorKey: "firstStageName", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + return ( + + ) + }, + size: 130, + enableResizing: true, + meta: { + excelHeader: "1차 스테이지" + }, + }, + + // 첫 번째 스테이지 날짜 + { + accessorKey: "firstStagePlanDate", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + return ( + + ) + }, + size: 140, + enableResizing: true, + meta: { + excelHeader: "1차 일정" + }, + }, + + // 두 번째 스테이지 정보 + { + accessorKey: "secondStageName", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + return ( + + ) + }, + size: 130, + enableResizing: true, + meta: { + excelHeader: "2차 스테이지" + }, + }, + + // 두 번째 스테이지 날짜 + { + accessorKey: "secondStagePlanDate", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + return ( + + ) + }, + size: 140, + enableResizing: true, + meta: { + excelHeader: "2차 일정" + }, + }, + + // 첨부파일 수 + { + accessorKey: "attachmentCount", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const count = row.original.attachmentCount || 0 + return ( +
+ + {count} +
+ ) + }, + size: 80, + enableResizing: true, + meta: { + excelHeader: "첨부파일" + }, + }, + + // 업데이트 일시 + { + accessorKey: "updatedAt", + header: ({ column }) => ( + + ), + cell: ({ cell }) => ( + + {formatDateTime(cell.getValue() as Date)} + + ), + size: 140, + enableResizing: true, + meta: { + excelHeader: "업데이트" + }, + }, + + // 액션 버튼 + { + id: "actions", + header: () => Actions, + cell: ({ row }) => { + const doc = row.original + return ( + + + + + + setRowAction({ type: "view", row: doc })} + > + + 보기 + + setRowAction({ type: "edit", row: doc })} + > + + 편집 + + + setRowAction({ type: "delete", row: doc })} + className="text-red-600" + > + + 삭제 + + + + + ) + }, + size: 50, + enableSorting: false, + enableHiding: false, + }, + ] + + return baseColumns +} \ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx new file mode 100644 index 00000000..3960bbce --- /dev/null +++ b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx @@ -0,0 +1,147 @@ +"use client" +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, Upload, Plus, Files, RefreshCw } from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { EnhancedDocumentsView } from "@/db/schema/vendorDocu" +import { AddDocumentListDialog } from "./add-doc-dialog" +import { DeleteDocumentsDialog } from "./delete-docs-dialog" +import { BulkUploadDialog } from "./bulk-upload-dialog" +import type { EnhancedDocument } from "@/types/enhanced-documents" +import { SendToSHIButton } from "./send-to-shi-button" +import { ImportFromDOLCEButton } from "./import-from-dolce-button" +import { SWPWorkflowPanel } from "./swp-workflow-panel" + +interface EnhancedDocTableToolbarActionsProps { + table: Table + projectType: "ship" | "plant" + selectedPackageId: number + onNewDocument: () => void + onBulkAction: (action: string, selectedRows: any[]) => Promise +} + +export function EnhancedDocTableToolbarActions({ + table, + projectType, + selectedPackageId, + onNewDocument, + onBulkAction +}: EnhancedDocTableToolbarActionsProps) { + const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = React.useState(false) + + // 현재 테이블의 모든 데이터 (필터링된 상태) + const allDocuments = table.getFilteredRowModel().rows.map(row => row.original) + + const handleSyncComplete = () => { + // 동기화 완료 후 테이블 새로고침 + table.resetRowSelection() + // 필요시 추가 액션 수행 + } + + const handleDocumentAdded = () => { + // 테이블 새로고침 + table.resetRowSelection() + + // 추가적인 새로고침 시도 + setTimeout(() => { + window.location.reload() // 강제 새로고침 + }, 500) + } + + const handleImportComplete = () => { + // 가져오기 완료 후 테이블 새로고침 + table.resetRowSelection() + setTimeout(() => { + window.location.reload() + }, 500) + } + + return ( +
+ {/* 삭제 버튼 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + row.original)} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + ) : null} + + {/* projectType에 따른 조건부 렌더링 */} + {projectType === "ship" ? ( + <> + {/* SHIP: DOLCE에서 목록 가져오기 */} + + + ) : ( + <> + {/* PLANT: 수동 문서 추가 */} + + + )} + + {/* 일괄 업로드 버튼 (공통) */} + + + {/* Export 버튼 (공통) */} + + + {/* Send to SHI 버튼 (공통) - 내부 → 외부로 보내기 */} + + + {/* SWP 전용 워크플로우 패널 */} + {projectType === "plant" && ( + + )} + + {/* 일괄 업로드 다이얼로그 */} + +
+ ) +} \ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-document-sheet.tsx b/lib/vendor-document-list/ship/enhanced-document-sheet.tsx new file mode 100644 index 00000000..88e342c8 --- /dev/null +++ b/lib/vendor-document-list/ship/enhanced-document-sheet.tsx @@ -0,0 +1,939 @@ +// enhanced-document-sheet.tsx +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { z } from "zod" +import { useRouter } from "next/navigation" +import { + Loader, + Save, + Upload, + Calendar, + User, + FileText, + AlertTriangle, + CheckCircle, + Clock, + Plus, + X +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Calendar as CalendarComponent } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { cn } from "@/lib/utils" +import { format } from "date-fns" +import { ko } from "date-fns/locale" +import { EnhancedDocumentsView } from "@/db/schema/vendorDocu" + +// 드롭존과 파일 관련 컴포넌트들 +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone" +import { + FileList, + FileListAction, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, + FileListSize, +} from "@/components/ui/file-list" +import prettyBytes from "pretty-bytes" + +// 스키마 정의 +const enhancedDocumentSchema = z.object({ + // 기본 문서 정보 + docNumber: z.string().min(1, "문서번호는 필수입니다"), + title: z.string().min(1, "제목은 필수입니다"), + pic: z.string().optional(), + status: z.string().min(1, "상태는 필수입니다"), + issuedDate: z.date().optional(), + + // 스테이지 관리 (plant 타입에서만 수정 가능) + stages: z.array(z.object({ + id: z.number().optional(), + stageName: z.string().min(1, "스테이지명은 필수입니다"), + stageOrder: z.number(), + priority: z.enum(["HIGH", "MEDIUM", "LOW"]).default("MEDIUM"), + planDate: z.date().optional(), + assigneeName: z.string().optional(), + description: z.string().optional(), + })).optional(), + + // 리비전 업로드 (현재 스테이지에 대한) + newRevision: z.object({ + stage: z.string().optional(), + revision: z.string().optional(), + uploaderType: z.enum(["vendor", "client", "shi"]).default("vendor"), + uploaderName: z.string().optional(), + comment: z.string().optional(), + attachments: z.array(z.instanceof(File)).optional(), + }).optional(), +}) + +type EnhancedDocumentSchema = z.infer + +// 상태 옵션 정의 +const statusOptions = [ + { value: "ACTIVE", label: "활성" }, + { value: "INACTIVE", label: "비활성" }, + { value: "COMPLETED", label: "완료" }, + { value: "CANCELLED", label: "취소" }, +] + +const priorityOptions = [ + { value: "HIGH", label: "높음" }, + { value: "MEDIUM", label: "보통" }, + { value: "LOW", label: "낮음" }, +] + +const stageStatusOptions = [ + { value: "PLANNED", label: "계획됨" }, + { value: "IN_PROGRESS", label: "진행중" }, + { value: "SUBMITTED", label: "제출됨" }, + { value: "APPROVED", label: "승인됨" }, + { value: "REJECTED", label: "반려됨" }, + { value: "COMPLETED", label: "완료됨" }, +] + +interface EnhancedDocumentSheetProps + extends React.ComponentPropsWithRef { + document: EnhancedDocumentsView | null + projectType: "ship" | "plant" + mode: "view" | "edit" | "upload" | "schedule" | "approve" +} + +export function EnhancedDocumentSheet({ + document, + projectType, + mode = "view", + ...props +}: EnhancedDocumentSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const [selectedFiles, setSelectedFiles] = React.useState([]) + const [uploadProgress, setUploadProgress] = React.useState(0) + const [activeTab, setActiveTab] = React.useState("info") + const router = useRouter() + + // 권한 계산 + const permissions = React.useMemo(() => { + const canEdit = projectType === "plant" || mode === "edit" + const canUpload = mode === "upload" || mode === "edit" + const canApprove = mode === "approve" && projectType === "ship" + const canSchedule = mode === "schedule" || (projectType === "plant" && mode === "edit") + + return { canEdit, canUpload, canApprove, canSchedule } + }, [projectType, mode]) + + const form = useForm({ + resolver: zodResolver(enhancedDocumentSchema), + defaultValues: { + docNumber: "", + title: "", + pic: "", + status: "ACTIVE", + issuedDate: undefined, + stages: [], + newRevision: { + stage: "", + revision: "", + uploaderType: "vendor", + uploaderName: "", + comment: "", + attachments: [], + }, + }, + }) + + // 폼 초기화 + React.useEffect(() => { + if (document) { + form.reset({ + docNumber: document.docNumber, + title: document.title, + pic: document.pic || "", + status: document.status, + issuedDate: document.issuedDate ? new Date(document.issuedDate) : undefined, + stages: document.allStages?.map((stage, index) => ({ + id: stage.id, + stageName: stage.stageName, + stageOrder: stage.stageOrder || index, + priority: stage.priority as "HIGH" | "MEDIUM" | "LOW" || "MEDIUM", + planDate: stage.planDate ? new Date(stage.planDate) : undefined, + assigneeName: stage.assigneeName || "", + description: "", + })) || [], + newRevision: { + stage: document.currentStageName || "", + revision: "", + uploaderType: "vendor", + uploaderName: "", + comment: "", + attachments: [], + }, + }) + + // 모드에 따른 기본 탭 설정 + if (mode === "upload") { + setActiveTab("upload") + } else if (mode === "schedule") { + setActiveTab("schedule") + } else if (mode === "approve") { + setActiveTab("approve") + } + } + }, [document, form, mode]) + + // 파일 처리 + const handleDropAccepted = (acceptedFiles: File[]) => { + const newFiles = [...selectedFiles, ...acceptedFiles] + setSelectedFiles(newFiles) + form.setValue('newRevision.attachments', newFiles) + } + + const removeFile = (index: number) => { + const updatedFiles = [...selectedFiles] + updatedFiles.splice(index, 1) + setSelectedFiles(updatedFiles) + form.setValue('newRevision.attachments', updatedFiles) + } + + // 스테이지 추가/제거 + const addStage = () => { + const currentStages = form.getValues("stages") || [] + const newStage = { + stageName: "", + stageOrder: currentStages.length, + priority: "MEDIUM" as const, + planDate: undefined, + assigneeName: "", + description: "", + } + form.setValue("stages", [...currentStages, newStage]) + } + + const removeStage = (index: number) => { + const currentStages = form.getValues("stages") || [] + const updatedStages = currentStages.filter((_, i) => i !== index) + form.setValue("stages", updatedStages) + } + + // 제출 처리 + function onSubmit(input: EnhancedDocumentSchema) { + startUpdateTransition(async () => { + if (!document) return + + try { + // 모드에 따른 다른 처리 + switch (mode) { + case "edit": + // 문서 정보 업데이트 + 스테이지 관리 + await updateDocumentInfo(input) + break + case "upload": + // 리비전 업로드 + await uploadRevision(input) + break + case "approve": + // 승인 처리 + await approveRevision(input) + break + case "schedule": + // 스케줄 관리 + await updateSchedule(input) + break + } + + form.reset() + setSelectedFiles([]) + props.onOpenChange?.(false) + toast.success("성공적으로 처리되었습니다") + router.refresh() + } catch (error) { + toast.error("처리 중 오류가 발생했습니다") + console.error(error) + } + }) + } + + // 개별 처리 함수들 + const updateDocumentInfo = async (input: EnhancedDocumentSchema) => { + // 문서 기본 정보 업데이트 API 호출 + console.log("문서 정보 업데이트:", input) + } + + const uploadRevision = async (input: EnhancedDocumentSchema) => { + if (!input.newRevision?.attachments?.length) { + throw new Error("파일을 선택해주세요") + } + + // 파일 업로드 처리 + const formData = new FormData() + formData.append("documentId", String(document?.documentId)) + formData.append("stage", input.newRevision.stage || "") + formData.append("revision", input.newRevision.revision || "") + formData.append("uploaderType", input.newRevision.uploaderType) + + input.newRevision.attachments.forEach((file) => { + formData.append("attachments", file) + }) + + // API 호출 + console.log("리비전 업로드:", formData) + } + + const approveRevision = async (input: EnhancedDocumentSchema) => { + // 승인 처리 API 호출 + console.log("리비전 승인:", input) + } + + const updateSchedule = async (input: EnhancedDocumentSchema) => { + // 스케줄 업데이트 API 호출 + console.log("스케줄 업데이트:", input) + } + + // 제목 및 설명 생성 + const getSheetTitle = () => { + switch (mode) { + case "edit": return "문서 정보 수정" + case "upload": return "리비전 업로드" + case "approve": return "문서 승인" + case "schedule": return "일정 관리" + default: return "문서 상세" + } + } + + const getSheetDescription = () => { + const docInfo = document ? `${document.docNumber} - ${document.title}` : "" + switch (mode) { + case "edit": return `문서 정보를 수정합니다. ${docInfo}` + case "upload": return `새 리비전을 업로드합니다. ${docInfo}` + case "approve": return `문서를 검토하고 승인 처리합니다. ${docInfo}` + case "schedule": return `문서의 일정을 관리합니다. ${docInfo}` + default: return docInfo + } + } + + return ( + + + + + {mode === "upload" && } + {mode === "approve" && } + {mode === "schedule" && } + {mode === "edit" && } + {getSheetTitle()} + + + {getSheetDescription()} + + + {/* 프로젝트 타입 및 권한 표시 */} +
+ + {projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} + + {document?.isOverdue && ( + + + 지연 + + )} + {document?.currentStagePriority === "HIGH" && ( + 높은 우선순위 + )} +
+
+ +
+ + + + 기본정보 + + 일정관리 + + + 리비전업로드 + + + 승인처리 + + + + {/* 기본 정보 탭 */} + + +
+ ( + + 문서번호 + + + + + + )} + /> + + ( + + 제목 + + + + + + )} + /> + +
+ ( + + 담당자 (PIC) + + + + + + )} + /> + + ( + + 상태 + + + + )} + /> +
+ + ( + + 발행일 + + + + + + + + date > new Date()} + initialFocus + /> + + + + + )} + /> + + {/* 현재 상태 정보 표시 */} + {document && ( +
+

+ + 현재 진행 상황 +

+
+
+ 현재 스테이지: +

{document.currentStageName || "-"}

+
+
+ 진행률: +

{document.progressPercentage || 0}%

+
+
+ 최신 리비전: +

{document.latestRevision || "-"}

+
+
+ 담당자: +

{document.currentStageAssigneeName || "-"}

+
+
+
+ )} +
+
+
+ + {/* 일정 관리 탭 */} + + +
+
+

스테이지 일정 관리

+ {projectType === "plant" && ( + + )} +
+ + {form.watch("stages")?.map((stage, index) => ( +
+
+
스테이지 {index + 1}
+ {projectType === "plant" && ( + + )} +
+ +
+ ( + + 스테이지명 + + + + + + )} + /> + + ( + + 우선순위 + + + + )} + /> + + ( + + 계획일 + + + + + + + + + + + + + )} + /> + + ( + + 담당자 + + + + + + )} + /> +
+
+ ))} +
+
+
+ + {/* 리비전 업로드 탭 */} + + +
+
+ ( + + 스테이지 + + + + + + )} + /> + + ( + + 리비전 + + + + + + )} + /> +
+ + ( + + 업로더명 (선택) + + + + + + )} + /> + + ( + + 코멘트 (선택) + +