From 8440ac29c7dcbef992039678ecc0fabff2fd04ec Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Dec 2025 00:58:23 +0000 Subject: (대표님) S-EDP 관련 대표님 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[packageCode]/eng/[formCode]/page.tsx | 95 +++ .../[packageCode]/im/[formCode]/page.tsx | 95 +++ .../[projectCode]/[packageCode]/page.tsx | 37 + .../(partners)/vendor-data-plant/layout.tsx | 18 +- app/api/cron/form-tags-plant/start/route.ts | 141 ++++ app/api/cron/form-tags-plant/status/route.ts | 46 + app/api/cron/tags-plant/start/route.ts | 149 ++++ app/api/cron/tags-plant/status/route.ts | 46 + components/client-data-table/data-table.tsx | 300 +++---- .../form-data-plant/delete-form-data-dialog.tsx | 9 +- .../form-data-report-batch-dialog.tsx | 21 +- .../form-data-plant/form-data-report-dialog.tsx | 21 +- .../form-data-report-temp-upload-dialog.tsx | 12 +- .../form-data-report-temp-upload-tab.tsx | 7 +- .../form-data-report-temp-uploaded-list-tab.tsx | 19 +- components/form-data-plant/form-data-table.tsx | 91 +- components/form-data-plant/import-excel-form.tsx | 13 +- components/form-data-plant/publish-dialog.tsx | 226 +++-- components/form-data-plant/spreadJS-dialog.tsx | 15 +- components/form-data-plant/update-form-sheet.tsx | 9 +- components/vendor-data-plant/project-swicher.tsx | 163 ++-- components/vendor-data-plant/sidebar.tsx | 479 +++++------ .../vendor-data-plant/vendor-data-container.tsx | 523 +++--------- db/schema/vendorData.ts | 87 ++ lib/forms-plant/services.ts | 458 +++++----- lib/forms-plant/stat.ts | 32 +- lib/sedp/get-form-tags-plant.ts | 933 +++++++++++++++++++++ lib/sedp/get-tags-plant.ts | 639 ++++++++++++++ lib/sedp/sync-form.ts | 9 +- lib/tags-plant/column-builder.service.ts | 34 + lib/tags-plant/queries.ts | 68 ++ lib/tags-plant/repository.ts | 42 +- lib/tags-plant/service.ts | 729 ++++++++++------ lib/tags-plant/table/add-tag-dialog.tsx | 18 +- lib/tags-plant/table/delete-tags-dialog.tsx | 12 +- lib/tags-plant/table/tag-table.tsx | 775 +++++++++++++++-- lib/tags-plant/table/tags-export.tsx | 5 +- lib/tags-plant/table/tags-table-floating-bar.tsx | 5 +- .../table/tags-table-toolbar-actions.tsx | 42 +- lib/tags-plant/table/update-tag-sheet.tsx | 13 +- lib/vendor-data/services.ts | 93 ++ lib/vendor-document/service.ts | 706 +++++++++++++++- 42 files changed, 5481 insertions(+), 1754 deletions(-) create mode 100644 app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx create mode 100644 app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx create mode 100644 app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx create mode 100644 app/api/cron/form-tags-plant/start/route.ts create mode 100644 app/api/cron/form-tags-plant/status/route.ts create mode 100644 app/api/cron/tags-plant/start/route.ts create mode 100644 app/api/cron/tags-plant/status/route.ts create mode 100644 lib/sedp/get-form-tags-plant.ts create mode 100644 lib/sedp/get-tags-plant.ts create mode 100644 lib/tags-plant/column-builder.service.ts create mode 100644 lib/tags-plant/queries.ts diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx new file mode 100644 index 00000000..351fbca3 --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx @@ -0,0 +1,95 @@ +// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx +import DynamicTable from "@/components/form-data-plant/form-data-table"; +import { getFormData, getFormId,getProjectIdByCode } from "@/lib/forms-plant/services"; +import { useTranslation } from "@/i18n"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface EngineeringFormPageProps { + params: { + lng: string; + projectCode: string; + packageCode: string; + formCode: string; + }; + searchParams?: { + mode?: string; + }; +} + +export default async function EngineeringFormPage({ + params, + searchParams, +}: EngineeringFormPageProps) { + // 1) 구조 분해 할당 + const resolvedParams = await params; + + // 2) searchParams도 await 필요 + const resolvedSearchParams = await searchParams; + + // 3) 구조 분해 할당 + const { lng, projectCode, packageCode, formCode } = resolvedParams; + + // i18n 설정 + const { t } = await useTranslation(lng, 'engineering'); + + // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) + const mode = "ENG"; // 기본값은 IM + + // 4) DB 조회 - projectCode와 packageCode를 전달 + const { columns, data, editableFieldsMap } = await getFormData( + formCode, + projectCode, + packageCode + ); + + // 5) formId 조회 - projectCode와 packageCode를 전달 + const { formId } = await getFormId(projectCode, packageCode, formCode, mode); + + const projectId = await getProjectIdByCode(projectCode) + + // 6) 예외 처리 + if (!columns) { + return ( +

+ {t('errors.form_meta_not_found')} +

+ ); + } + + // 7) 렌더링 + return ( +
+
+
+

Engineering Form

+

+ Project: {projectCode} / Package: {packageCode} / Form: {formCode} +

+
+
+ +
+ +
+
+ ); +} + +function TableSkeleton() { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx new file mode 100644 index 00000000..29188061 --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/im/[formCode]/page.tsx @@ -0,0 +1,95 @@ +// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/eng/[formCode]/page.tsx +import DynamicTable from "@/components/form-data-plant/form-data-table"; +import { getFormData, getFormId, getProjectIdByCode } from "@/lib/forms-plant/services"; +import { useTranslation } from "@/i18n"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface EngineeringFormPageProps { + params: { + lng: string; + projectCode: string; + packageCode: string; + formCode: string; + }; + searchParams?: { + mode?: string; + }; +} + +export default async function IMFormPage({ + params, + searchParams, +}: EngineeringFormPageProps) { + // 1) 구조 분해 할당 + const resolvedParams = await params; + + // 2) searchParams도 await 필요 + const resolvedSearchParams = await searchParams; + + // 3) 구조 분해 할당 + const { lng, projectCode, packageCode, formCode } = resolvedParams; + + // i18n 설정 + const { t } = await useTranslation(lng, 'engineering'); + + // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) + const mode = "IM"; // 기본값은 IM + + // 4) DB 조회 - projectCode와 packageCode를 전달 + const { columns, data, editableFieldsMap } = await getFormData( + formCode, + projectCode, + packageCode + ); + + // 5) formId 조회 - projectCode와 packageCode를 전달 + const { formId } = await getFormId(projectCode, packageCode, formCode, mode); + + const projectId = await getProjectIdByCode(projectCode) + + // 6) 예외 처리 + if (!columns) { + return ( +

+ {t('errors.form_meta_not_found')} +

+ ); + } + + // 7) 렌더링 + return ( +
+
+
+

Engineering Form

+

+ Project: {projectCode} / Package: {packageCode} / Form: {formCode} +

+
+
+ +
+ +
+
+ ); +} + +function TableSkeleton() { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx new file mode 100644 index 00000000..4904a8ff --- /dev/null +++ b/app/[lng]/partners/(partners)/vendor-data-plant/[projectCode]/[packageCode]/page.tsx @@ -0,0 +1,37 @@ +// app/[lng]/partners/vendor-data-plant/[projectCode]/[packageCode]/page.tsx + +import { TagsTable } from "@/lib/tags-plant/table/tag-table" + +interface MasterTagListPageProps { + params: Promise<{ + lng: string + projectCode: string + packageCode: string + }> +} + +export default async function MasterTagListPage({ + params, +}: MasterTagListPageProps) { + const { projectCode, packageCode } = await params + + return ( +
+
+
+

Master Tag List

+

+ Project: {projectCode} / Package: {packageCode} +

+
+
+ + {/* 완전 클라이언트 컴포넌트 */} + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx b/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx index 8a9c43e9..792a3a6a 100644 --- a/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx +++ b/app/[lng]/partners/(partners)/vendor-data-plant/layout.tsx @@ -2,41 +2,35 @@ import * as React from "react" import { cookies } from "next/headers" import { Shell } from "@/components/shell" -import { getVendorProjectsAndContracts } from "@/lib/vendor-data-plant/services" import { VendorDataContainer } from "@/components/vendor-data-plant/vendor-data-container" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { getServerSession } from "next-auth" import { InformationButton } from "@/components/information/information-button" import { useTranslation } from "@/i18n" +import { getVendorProjectsWithPackages } from "@/lib/vendor-data/services" interface VendorDataLayoutProps { children: React.ReactNode params: { lng?: string } } -// Layout 컴포넌트는 서버 컴포넌트입니다 export default async function VendorDataLayout({ children, params, }: VendorDataLayoutProps) { - // 기본 언어는 'ko'로 설정, params.locale이 있으면 사용 - const { lng } = await params; + const { lng } = await params const language = lng || 'en' const { t } = await useTranslation(language, 'engineering') const session = await getServerSession(authOptions) const vendorId = session?.user.companyId - // const vendorId = "17" const idAsNumber = Number(vendorId) - // 프로젝트 데이터 가져오기 (type=plant만) - const projects = await getVendorProjectsAndContracts(idAsNumber, "plant") + // 프로젝트 및 패키지 데이터 가져오기 + const projects = await getVendorProjectsWithPackages(idAsNumber, "plant") // 레이아웃 설정 쿠키 가져오기 - // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 const cookieStore = await cookies() - - // 이제 cookieStore.get() 메서드 사용 가능 const layout = cookieStore.get("react-resizable-panels:layout:mail") const collapsed = cookieStore.get("react-resizable-panels:collapsed") @@ -54,9 +48,6 @@ export default async function VendorDataLayout({ - {/*

- 각종 Data 입력할 수 있습니다 -

*/} @@ -74,7 +65,6 @@ export default async function VendorDataLayout({ defaultCollapsed={defaultCollapsed} navCollapsedSize={4} > - {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} {children} )} diff --git a/app/api/cron/form-tags-plant/start/route.ts b/app/api/cron/form-tags-plant/start/route.ts new file mode 100644 index 00000000..17eb8979 --- /dev/null +++ b/app/api/cron/form-tags-plant/start/route.ts @@ -0,0 +1,141 @@ +// app/api/cron/form-tags/start/route.ts +import { NextRequest } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +import { revalidateTag } from 'next/cache'; + +// 동기화 작업의 상태를 저장할 Map +// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다 +const syncJobs = new Map(); + +export async function POST(request: NextRequest) { + try { + // 요청 데이터 가져오기 + let projectCode: string | undefined; + let formCode: string | undefined; + let packageCode: string | undefined; + let mode: string | undefined; + + + const body = await request.json(); + projectCode = body.projectCode; + formCode = body.formCode; + packageCode = body.packageCode; + mode = body.mode; // 모드 정보 추출 + + + // 고유 ID 생성 + const syncId = uuidv4(); + + // 작업 상태 초기화 + syncJobs.set(syncId, { + status: 'queued', + startTime: new Date(), + formCode, + projectCode, + packageCode, + mode + + }); + + // 비동기 작업 시작 (백그라운드에서 실행) + processTagImport(syncId).catch(error => { + console.error('Background tag import job failed:', error); + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + status: 'failed', + endTime: new Date(), + error: error.message || 'Unknown error occurred' + }); + }); + + // 즉시 응답 반환 (작업 ID 포함) + return Response.json({ + success: true, + message: 'Tag import job started', + syncId + }, { status: 200 }); + + } catch (error: any) { + console.error('Failed to start tag import job:', error); + return Response.json({ + success: false, + error: error.message || 'Failed to start tag import job' + }, { status: 500 }); + } +} + +// 백그라운드에서 실행되는 태그 가져오기 작업 +async function processTagImport(syncId: string) { + try { + const jobInfo = syncJobs.get(syncId)!; + const formCode = jobInfo.formCode; + const projectCode = jobInfo.projectCode; + const packageCode = jobInfo.packageCode || 0; + const mode = jobInfo.mode || 0; + + // 상태 업데이트: 처리 중 + syncJobs.set(syncId, { + ...jobInfo, + status: 'processing', + progress: 0, + }); + + if (!formCode || !projectCode ) { + throw new Error('formCode,projectCode is required'); + } + + // 여기서 실제 태그 가져오기 로직 import + const { importTagsFromSEDP } = await import('@/lib/sedp/get-form-tags-plant'); + + // 진행 상황 업데이트를 위한 콜백 함수 + const updateProgress = (progress: number) => { + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + progress + }); + }; + + // 실제 태그 가져오기 실행 + const result = await importTagsFromSEDP(formCode, projectCode, packageCode, updateProgress); + + // 명시적으로 캐시 무효화 + revalidateTag(`forms-${packageCode}-${mode}`); + + // 상태 업데이트: 완료 + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + status: 'completed', + endTime: new Date(), + result, + progress: 100, + }); + + return result; + } catch (error: any) { + // 에러 발생 시 상태 업데이트 + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + status: 'failed', + endTime: new Date(), + error: error.message || 'Unknown error occurred', + }); + + throw error; // 에러 다시 던지기 + } +} + +// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용) +export function getSyncJobStatus(id: string) { + return syncJobs.get(id); +} \ No newline at end of file diff --git a/app/api/cron/form-tags-plant/status/route.ts b/app/api/cron/form-tags-plant/status/route.ts new file mode 100644 index 00000000..9d288f52 --- /dev/null +++ b/app/api/cron/form-tags-plant/status/route.ts @@ -0,0 +1,46 @@ +// app/api/cron/tags/status/route.ts +import { NextRequest } from 'next/server'; +import { getSyncJobStatus } from '../start/route'; + +export async function GET(request: NextRequest) { + try { + // URL에서 작업 ID 가져오기 + const searchParams = request.nextUrl.searchParams; + const syncId = searchParams.get('id'); + + if (!syncId) { + return Response.json({ + success: false, + error: 'Missing sync ID parameter' + }, { status: 400 }); + } + + // 작업 상태 조회 + const jobStatus = getSyncJobStatus(syncId); + + if (!jobStatus) { + return Response.json({ + success: false, + error: 'Sync job not found' + }, { status: 404 }); + } + + // 작업 상태 반환 + return Response.json({ + success: true, + status: jobStatus.status, + startTime: jobStatus.startTime, + endTime: jobStatus.endTime, + progress: jobStatus.progress, + result: jobStatus.result, + error: jobStatus.error + }, { status: 200 }); + + } catch (error: any) { + console.error('Error retrieving tag import status:', error); + return Response.json({ + success: false, + error: error.message || 'Failed to retrieve tag import status' + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/cron/tags-plant/start/route.ts b/app/api/cron/tags-plant/start/route.ts new file mode 100644 index 00000000..83e06935 --- /dev/null +++ b/app/api/cron/tags-plant/start/route.ts @@ -0,0 +1,149 @@ +// app/api/cron/tags/start/route.ts +import { NextRequest } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +import { revalidateTag } from 'next/cache'; + +// 동기화 작업의 상태를 저장할 Map +// 실제 프로덕션에서는 Redis 또는 DB에 저장하는 것이 좋습니다 +const syncJobs = new Map(); + +export async function POST(request: NextRequest) { + try { + // 요청 데이터 가져오기 + let projectCode: string | undefined; + let packageCode: string | undefined; + let mode: string | undefined; + + try { + const body = await request.json(); + projectCode = body.projectCode; + packageCode = body.packageCode; + mode = body.mode; // 모드 정보 추출 + + } catch (error) { + // 요청 본문이 없거나 JSON이 아닌 경우, URL 파라미터 확인 + const searchParams = request.nextUrl.searchParams; + const projectCodeParam = searchParams.get('projectCode'); + const packageCodeParam = searchParams.get('packageCode'); + + if (projectCodeParam&&packageCodeParam) { + projectCode = projectCodeParam; + packageCode = packageCodeParam; + } + mode = searchParams.get('mode') || undefined; + } + + // 고유 ID 생성 + const syncId = uuidv4(); + + // 작업 상태 초기화 + syncJobs.set(syncId, { + status: 'queued', + startTime: new Date(), + projectCode, + packageCode, + mode + }); + + // 비동기 작업 시작 (백그라운드에서 실행) + processTagImport(syncId).catch(error => { + console.error('Background tag import job failed:', error); + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + status: 'failed', + endTime: new Date(), + error: error.message || 'Unknown error occurred' + }); + }); + + // 즉시 응답 반환 (작업 ID 포함) + return Response.json({ + success: true, + message: 'Tag import job started', + syncId + }, { status: 200 }); + + } catch (error: any) { + console.error('Failed to start tag import job:', error); + return Response.json({ + success: false, + error: error.message || 'Failed to start tag import job' + }, { status: 500 }); + } +} + +// 백그라운드에서 실행되는 태그 가져오기 작업 +async function processTagImport(syncId: string) { + try { + const jobInfo = syncJobs.get(syncId)!; + const projectCode = jobInfo.projectCode; + const packageCode = jobInfo.packageCode; + const mode = jobInfo.mode; // 모드 정보 추출 + + + // 상태 업데이트: 처리 중 + syncJobs.set(syncId, { + ...jobInfo, + status: 'processing', + progress: 0, + }); + + if (!packageCode) { + throw new Error('Package is required'); + } + + // 여기서 실제 태그 가져오기 로직 import + const { importTagsFromSEDP } = await import('@/lib/sedp/get-tags-plant'); + + // 진행 상황 업데이트를 위한 콜백 함수 + const updateProgress = (progress: number) => { + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + progress + }); + }; + + // 실제 태그 가져오기 실행 + const result = await importTagsFromSEDP(projectCode, packageCode,updateProgress, mode); + + // 명시적으로 캐시 무효화 + revalidateTag(`tags-${packageCode}`); + revalidateTag(`forms-${packageCode}-${mode}`); + + // 상태 업데이트: 완료 + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + status: 'completed', + endTime: new Date(), + result, + progress: 100, + }); + + return result; + } catch (error: any) { + // 에러 발생 시 상태 업데이트 + syncJobs.set(syncId, { + ...syncJobs.get(syncId)!, + status: 'failed', + endTime: new Date(), + error: error.message || 'Unknown error occurred', + }); + + throw error; // 에러 다시 던지기 + } +} + +// 서버 메모리에 저장된 작업 상태 접근 함수 (다른 API에서 사용) +export function getSyncJobStatus(id: string) { + return syncJobs.get(id); +} \ No newline at end of file diff --git a/app/api/cron/tags-plant/status/route.ts b/app/api/cron/tags-plant/status/route.ts new file mode 100644 index 00000000..9d288f52 --- /dev/null +++ b/app/api/cron/tags-plant/status/route.ts @@ -0,0 +1,46 @@ +// app/api/cron/tags/status/route.ts +import { NextRequest } from 'next/server'; +import { getSyncJobStatus } from '../start/route'; + +export async function GET(request: NextRequest) { + try { + // URL에서 작업 ID 가져오기 + const searchParams = request.nextUrl.searchParams; + const syncId = searchParams.get('id'); + + if (!syncId) { + return Response.json({ + success: false, + error: 'Missing sync ID parameter' + }, { status: 400 }); + } + + // 작업 상태 조회 + const jobStatus = getSyncJobStatus(syncId); + + if (!jobStatus) { + return Response.json({ + success: false, + error: 'Sync job not found' + }, { status: 404 }); + } + + // 작업 상태 반환 + return Response.json({ + success: true, + status: jobStatus.status, + startTime: jobStatus.startTime, + endTime: jobStatus.endTime, + progress: jobStatus.progress, + result: jobStatus.result, + error: jobStatus.error + }, { status: 200 }); + + } catch (error: any) { + console.error('Error retrieving tag import status:', error); + return Response.json({ + success: false, + error: error.message || 'Failed to retrieve tag import status' + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/components/client-data-table/data-table.tsx b/components/client-data-table/data-table.tsx index 3e009302..371a1dab 100644 --- a/components/client-data-table/data-table.tsx +++ b/components/client-data-table/data-table.tsx @@ -49,8 +49,9 @@ interface DataTableProps { children?: React.ReactNode /** 선택 상태 초기화 트리거 */ clearSelection?: boolean - initialColumnPinning?: ColumnPinningState // 추가 - + initialColumnPinning?: ColumnPinningState + /** Table 인스턴스를 상위 컴포넌트에 전달하는 콜백 */ + onTableReady?: (table: Table) => void } export function ClientDataTable({ @@ -63,7 +64,8 @@ export function ClientDataTable({ maxHeight, onSelectedRowsChange, clearSelection, - initialColumnPinning + initialColumnPinning, + onTableReady }: DataTableProps) { // (1) React Table 상태 @@ -118,6 +120,13 @@ export function ClientDataTable({ useAutoSizeColumns(table, autoSizeColumns) + // 🆕 Table 인스턴스를 상위 컴포넌트에 전달 + React.useEffect(() => { + if (onTableReady) { + onTableReady(table) + } + }, [table, onTableReady]) + React.useEffect(() => { if (!onSelectedRowsChange) return const selectedRows = table @@ -164,6 +173,7 @@ export function ClientDataTable({ }), } } + // 🎯 테이블 총 너비 계산 const getTableWidth = React.useCallback(() => { const totalSize = table.getCenterTotalSize() + table.getLeftTotalSize() + table.getRightTotalSize() @@ -206,174 +216,172 @@ export function ClientDataTable({ {children} - -
- + thead]:sticky [&>thead]:top-0 [&>thead]:z-10", !hasNestedHeader && "table-fixed" // nested header가 없으면 table-fixed 적용 )} style={{ minWidth: hasNestedHeader ? getTableWidth() : undefined }}> - {/* nested header가 있으면 table-fixed 제거, 없으면 적용 */} - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 - if (header.column.getIsGrouped()) { - return null - } + {/* nested header가 있으면 table-fixed 제거, 없으면 적용 */} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 + if (header.column.getIsGrouped()) { + return null + } - return ( - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */} - {header.column.getCanResize() && !('columns' in header.column.columnDef) && ( - + return ( + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() )} -
-
- ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => { - // --------------------------------------------------- - // 1) "그룹핑 헤더" Row인지 확인 - // --------------------------------------------------- - if (row.getIsGrouped()) { - // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음 - const groupingColumnId = row.groupingColumnId ?? "" - const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체 + + {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */} + {header.column.getCanResize() && !('columns' in header.column.columnDef) && ( + + )} +
+
+ ) + })} +
+ ))} +
+ + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => { + // --------------------------------------------------- + // 1) "그룹핑 헤더" Row인지 확인 + // --------------------------------------------------- + if (row.getIsGrouped()) { + // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음 + const groupingColumnId = row.groupingColumnId ?? "" + const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체 - // 컬럼 라벨 가져오기 - let columnLabel = groupingColumnId - if (groupingColumn) { - const headerDef = groupingColumn.columnDef.meta?.excelHeader - if (typeof headerDef === "string") { - columnLabel = headerDef - } + // 컬럼 라벨 가져오기 + let columnLabel = groupingColumnId + if (groupingColumn) { + const headerDef = groupingColumn.columnDef.meta?.excelHeader + if (typeof headerDef === "string") { + columnLabel = headerDef } - - return ( - - {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} - - {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} - {row.getCanExpand() && ( - - )} - - {/* Group Label + 값 */} - - {columnLabel}: {row.getValue(groupingColumnId)} - - - ({row.subRows.length} rows) - - - - ) } - // --------------------------------------------------- - // 2) 일반 Row - // → "그룹핑된 컬럼"은 숨긴다 - // --------------------------------------------------- return ( - {row.getVisibleCells().map((cell) => { - // 이 셀의 컬럼이 grouped라면 숨긴다 - if (cell.column.getIsGrouped()) { - return null - } - - return ( - + {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} + {row.getCanExpand() && ( + + )} + + {/* Group Label + 값 */} + + {columnLabel}: {row.getValue(groupingColumnId)} + + + ({row.subRows.length} rows) + + ) - }) - ) : ( + } + // --------------------------------------------------- - // 3) 데이터가 없을 때 + // 2) 일반 Row + // → "그룹핑된 컬럼"은 숨긴다 // --------------------------------------------------- - - - No results. - - - )} - -
-
+ {row.getVisibleCells().map((cell) => { + // 이 셀의 컬럼이 grouped라면 숨긴다 + if (cell.column.getIsGrouped()) { + return null + } + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + })} + + ) + }) + ) : ( + // --------------------------------------------------- + // 3) 데이터가 없을 때 + // --------------------------------------------------- + + + No results. + + + )} + + + diff --git a/components/form-data-plant/delete-form-data-dialog.tsx b/components/form-data-plant/delete-form-data-dialog.tsx index 6ac8f67c..2406407e 100644 --- a/components/form-data-plant/delete-form-data-dialog.tsx +++ b/components/form-data-plant/delete-form-data-dialog.tsx @@ -40,7 +40,8 @@ interface DeleteFormDataDialogProps extends React.ComponentPropsWithoutRef { formData: GenericData[] formCode: string - contractItemId: number + projectCode: string + packageCode: string projectId?: number showTrigger?: boolean onSuccess?: () => void @@ -50,7 +51,8 @@ interface DeleteFormDataDialogProps export function DeleteFormDataDialog({ formData, formCode, - contractItemId, + projectCode, + packageCode, projectId, showTrigger = true, onSuccess, @@ -77,7 +79,8 @@ export function DeleteFormDataDialog({ const result = await deleteFormDataByTags({ formCode, - contractItemId, + projectCode, + packageCode, tagIdxs, projectId, }) diff --git a/components/form-data-plant/form-data-report-batch-dialog.tsx b/components/form-data-plant/form-data-report-batch-dialog.tsx index 24b5827b..ba41a3c2 100644 --- a/components/form-data-plant/form-data-report-batch-dialog.tsx +++ b/components/form-data-plant/form-data-report-batch-dialog.tsx @@ -71,7 +71,8 @@ interface FormDataReportBatchDialogProps { setOpen: Dispatch>; columnsJSON: DataTableColumnJSON[]; reportData: ReportData[]; - packageId: number; + projectCode: string; + packageCode: string; formId: number; formCode: string; } @@ -81,7 +82,8 @@ export const FormDataReportBatchDialog: FC = ({ setOpen, columnsJSON, reportData, - packageId, + projectCode, + packageCode, formId, formCode, }) => { @@ -100,8 +102,8 @@ export const FormDataReportBatchDialog: FC = ({ const [generatedFileBlob, setGeneratedFileBlob] = useState(null); useEffect(() => { - updateReportTempList(packageId, formId, setTempList); - }, [packageId, formId]); + updateReportTempList(projectCode, packageCode, formId, setTempList); + }, [projectCode, packageCode, formId]); const onClose = () => { if (isUploading) { @@ -361,7 +363,8 @@ export const FormDataReportBatchDialog: FC = ({ @@ -409,17 +412,19 @@ const UploadFileItem: FC = ({ }; type UpdateReportTempList = ( - packageId: number, + projectCode: string, + packageCode: string, formId: number, setPrevReportTemp: Dispatch> ) => void; const updateReportTempList: UpdateReportTempList = async ( - packageId, + projectCode, + packageCode, formId, setTempList ) => { - const tempList = await getReportTempList(packageId, formId); + const tempList = await getReportTempList(projectCode,packageCode, formId); setTempList( tempList.map((c) => { diff --git a/components/form-data-plant/form-data-report-dialog.tsx b/components/form-data-plant/form-data-report-dialog.tsx index 9177ab36..2413fc28 100644 --- a/components/form-data-plant/form-data-report-dialog.tsx +++ b/components/form-data-plant/form-data-report-dialog.tsx @@ -49,7 +49,8 @@ interface FormDataReportDialogProps { columnsJSON: DataTableColumnJSON[]; reportData: ReportData[]; setReportData: Dispatch>; - packageId: number; + projectCode: string; + packageCode: string; formId: number; formCode: string; } @@ -58,7 +59,8 @@ export const FormDataReportDialog: FC = ({ columnsJSON, reportData, setReportData, - packageId, + projectCode, + packageCode, formId, formCode, }) => { @@ -76,8 +78,8 @@ export const FormDataReportDialog: FC = ({ const [generatedFileBlob, setGeneratedFileBlob] = useState(null); useEffect(() => { - updateReportTempList(packageId, formId, setTempList); - }, [packageId, formId]); + updateReportTempList(projectCode, packageCode, formId, setTempList); + }, [projectCode,packageCode, formId]); const onClose = async (value: boolean) => { if (fileLoading) { @@ -197,7 +199,8 @@ export const FormDataReportDialog: FC = ({ @@ -394,17 +397,19 @@ const importReportData: ImportReportData = async ( }; type UpdateReportTempList = ( - packageId: number, + projectCode: string, + packageCode: string, formId: number, setPrevReportTemp: Dispatch> ) => void; const updateReportTempList: UpdateReportTempList = async ( - packageId, + projectCode, + packageCode, formId, setTempList ) => { - const tempList = await getReportTempList(packageId, formId); + const tempList = await getReportTempList(projectCode,packageCode, formId); setTempList( tempList.map((c) => { diff --git a/components/form-data-plant/form-data-report-temp-upload-dialog.tsx b/components/form-data-plant/form-data-report-temp-upload-dialog.tsx index 59ea6ade..66915198 100644 --- a/components/form-data-plant/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data-plant/form-data-report-temp-upload-dialog.tsx @@ -23,7 +23,8 @@ interface FormDataReportTempUploadDialogProps { columnsJSON: DataTableColumnJSON[]; open: boolean; setOpen: Dispatch>; - packageId: number; + projectCode: string; + packageCode: string; formCode: string; formId: number; uploaderType: string; @@ -35,7 +36,8 @@ export const FormDataReportTempUploadDialog: FC< columnsJSON, open, setOpen, - packageId, + projectCode, + packageCode, formId, formCode, uploaderType, @@ -83,14 +85,16 @@ export const FormDataReportTempUploadDialog: FC< diff --git a/components/form-data-plant/form-data-report-temp-upload-tab.tsx b/components/form-data-plant/form-data-report-temp-upload-tab.tsx index 81186ba4..41466f90 100644 --- a/components/form-data-plant/form-data-report-temp-upload-tab.tsx +++ b/components/form-data-plant/form-data-report-temp-upload-tab.tsx @@ -36,14 +36,15 @@ import { uploadReportTemp } from "@/lib/forms-plant/services"; const MAX_FILE_SIZE = 3000000; interface FormDataReportTempUploadTabProps { - packageId: number; + projectCode: string; + packageCode: string; formId: number; uploaderType: string; } export const FormDataReportTempUploadTab: FC< FormDataReportTempUploadTabProps -> = ({ packageId, formId, uploaderType }) => { +> = ({ projectCode,packageCode, formId, uploaderType }) => { const { toast } = useToast(); const params = useParams(); const lng = (params?.lng as string) || "ko"; @@ -94,7 +95,7 @@ export const FormDataReportTempUploadTab: FC< formData.append("customFileName", file.name); formData.append("uploaderType", uploaderType); - await uploadReportTemp(packageId, formId, formData); + await uploadReportTemp(projectCode, packageCode, formId, formData); successCount++; setUploadProgress(Math.round((successCount / totalFiles) * 100)); diff --git a/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx b/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx index 4cfbad69..1b6cefaf 100644 --- a/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx +++ b/components/form-data-plant/form-data-report-temp-uploaded-list-tab.tsx @@ -39,13 +39,14 @@ import { getReportTempList, deleteReportTempFile } from "@/lib/forms-plant/servi import { VendorDataReportTemps } from "@/db/schema/vendorData"; interface FormDataReportTempUploadedListTabProps { - packageId: number; + projectCode: string; + packageCode: string; formId: number; } export const FormDataReportTempUploadedListTab: FC< FormDataReportTempUploadedListTabProps -> = ({ packageId, formId }) => { +> = ({ projectCode,packageCode , formId }) => { const params = useParams(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "engineering"); @@ -57,12 +58,12 @@ export const FormDataReportTempUploadedListTab: FC< useEffect(() => { const getTempFiles = async () => { - await updateReportTempList(packageId, formId, setPrevReportTemp); + await updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp); setIsLoading(false); }; getTempFiles(); - }, [packageId, formId]); + }, [projectCode,packageCode, formId]); return (
@@ -70,7 +71,7 @@ export const FormDataReportTempUploadedListTab: FC< - updateReportTempList(packageId, formId, setPrevReportTemp) + updateReportTempList(projectCode,packageCode, formId, setPrevReportTemp) } isLoading={isLoading} t={t} @@ -80,17 +81,19 @@ export const FormDataReportTempUploadedListTab: FC< }; type UpdateReportTempList = ( - packageId: number, + projectCode: string, + packageCode: string, formId: number, setPrevReportTemp: Dispatch> ) => Promise; const updateReportTempList: UpdateReportTempList = async ( - packageId, + projectCode, + packageCode, formId, setPrevReportTemp ) => { - const tempList = await getReportTempList(packageId, formId); + const tempList = await getReportTempList(projectCode, packageCode, formId); setPrevReportTemp(tempList); }; diff --git a/components/form-data-plant/form-data-table.tsx b/components/form-data-plant/form-data-table.tsx index 30c176bd..c6c79a69 100644 --- a/components/form-data-plant/form-data-table.tsx +++ b/components/form-data-plant/form-data-table.tsx @@ -76,7 +76,8 @@ interface GenericData { export interface DynamicTableProps { dataJSON: GenericData[]; columnsJSON: DataTableColumnJSON[]; - contractItemId: number; + projectCode: string; + packageCode: string; formCode: string; formId: number; projectId: number; @@ -89,7 +90,8 @@ export interface DynamicTableProps { export default function DynamicTable({ dataJSON, columnsJSON, - contractItemId, + projectCode, + packageCode, formCode, formId, projectId, @@ -156,7 +158,8 @@ export default function DynamicTable({ // 서버 액션 호출 const result = await excludeFormDataByTags({ formCode, - contractItemId, + projectCode, + packageCode, tagNumbers, }); @@ -288,7 +291,7 @@ export default function DynamicTable({ try { setIsLoadingStats(true); // getFormStatusByVendor 서버 액션 직접 호출 - const data = await getFormStatusByVendor(projectId, contractItemId, formCode); + const data = await getFormStatusByVendor(projectId, projectCode, packageCode,formCode); if (data && data.length > 0) { setFormStats(data[0]); @@ -339,9 +342,7 @@ export default function DynamicTable({ // SEDP compare dialog state const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false); - const [projectCode, setProjectCode] = React.useState(''); - const [projectType, setProjectType] = React.useState('plant'); - const [packageCode, setPackageCode] = React.useState(''); + const projectType = "plant"; // 새로 추가된 Template 다이얼로그 상태 const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false); @@ -374,43 +375,13 @@ const [isLoadingRegisters, setIsLoadingRegisters] = React.useState(false); React.useEffect(() => { const getTempCount = async () => { - const tempList = await getReportTempList(contractItemId, formId); + const tempList = await getReportTempList(projectCode, packageCode, formId); setTempCount(tempList.length); }; getTempCount(); - }, [contractItemId, formId, tempUpDialog]); + }, [projectCode,packageCode, formId, tempUpDialog]); - React.useEffect(() => { - const getPackageCode = async () => { - try { - const packageCode = await getPackageCodeById(contractItemId); - setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값 - } catch (error) { - console.error('패키지 조회 실패:', error); - setPackageCode(''); - } - }; - - getPackageCode(); - }, [contractItemId]) - // Get project code when component mounts - React.useEffect(() => { - const getProjectCode = async () => { - try { - const project = await getProjectById(projectId); - setProjectCode(project.code); - setProjectType(project.type); - } catch (error) { - console.error("Error fetching project code:", error); - toast.error("Failed to fetch project code"); - } - }; - - if (projectId) { - getProjectCode(); - } - }, [projectId]); // 선택된 행들의 실제 데이터 가져오기 const getSelectedRowsData = React.useCallback(() => { @@ -529,7 +500,7 @@ React.useEffect(() => { async function handleSyncTags() { try { setIsSyncingTags(true); - const result = await syncMissingTags(contractItemId, formCode); + const result = await syncMissingTags(projectCode,packageCode, formCode); // Prepare the toast messages based on what changed const changes = []; @@ -562,9 +533,9 @@ React.useEffect(() => { setIsLoadingTags(true); // API 엔드포인트 호출 - 작업 시작만 요청 - const response = await fetch('/api/cron/form-tags/start', { + const response = await fetch('/api/cron/form-tags-plant/start', { method: 'POST', - body: JSON.stringify({ projectCode, formCode, contractItemId }) + body: JSON.stringify({ projectCode, formCode, packageCode }) }); if (!response.ok) { @@ -603,7 +574,7 @@ React.useEffect(() => { // 5초마다 상태 확인 pollingRef.current = setInterval(async () => { try { - const response = await fetch(`/api/cron/form-tags/status?id=${id}`); + const response = await fetch(`/api/cron/form-tags-plant/status?id=${id}`); if (!response.ok) { throw new Error('Failed to get tag import status'); @@ -666,7 +637,8 @@ React.useEffect(() => { tableData, columnsJSON, formCode, - contractItemId, + projectCode, + packageCode, editableFieldsMap, // 추가: 편집 가능 필드 정보 전달 onPendingChange: setIsImporting, // Let importExcelData handle loading state onDataUpdate: (newData) => { @@ -747,7 +719,8 @@ React.useEffect(() => { const sedpResult = await sendFormDataToSEDP( formCode, // Send formCode instead of formName projectId, // Project ID - contractItemId, + projectCode, + packageCode, tableData.filter(v=>v.status !== 'excluded'), // Table data columnsJSON // Column definitions ); @@ -1226,7 +1199,8 @@ React.useEffect(() => { columns={columnsJSON} rowData={rowAction?.row.original ?? null} formCode={formCode} - contractItemId={contractItemId} + projectCode={projectCode} + packageCode={packageCode} editableFieldsMap={editableFieldsMap} onUpdateSuccess={(updatedValues) => { // Update the specific row in tableData when a single row is updated @@ -1244,7 +1218,8 @@ React.useEffect(() => { { @@ -1257,16 +1232,6 @@ React.useEffect(() => { showTrigger={false} /> - {/* Dialog for adding tags */} - {/* */} {/* 새로 추가된 Template 다이얼로그 */} { selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용 tableData={tableData} // SPR_LST_SETUP용 - 새로 추가 formCode={formCode} - contractItemId={contractItemId} + projectCode={projectCode} + packageCode={packageCode} editableFieldsMap={editableFieldsMap} columnsJSON={columnsJSON} onUpdateSuccess={(updatedValues) => { @@ -1344,7 +1310,8 @@ React.useEffect(() => { columnsJSON={columnsJSON} open={tempUpDialog} setOpen={setTempUpDialog} - packageId={contractItemId} + projectCode={projectCode} + packageCode={packageCode} formCode={formCode} formId={formId} uploaderType="vendor" @@ -1356,7 +1323,8 @@ React.useEffect(() => { columnsJSON={columnsJSON} reportData={reportData} setReportData={setReportData} - packageId={contractItemId} + projectCode={projectCode} + packageCode={packageCode} formCode={formCode} formId={formId} /> @@ -1368,7 +1336,8 @@ React.useEffect(() => { setOpen={setBatchDownDialog} columnsJSON={columnsJSON} reportData={selectedRowCount > 0 ? getSelectedRowsData() : tableData} - packageId={contractItemId} + projectCode={projectCode} + packageCode={packageCode} formCode={formCode} formId={formId} /> diff --git a/components/form-data-plant/import-excel-form.tsx b/components/form-data-plant/import-excel-form.tsx index ffc6f2f9..8ac70c59 100644 --- a/components/form-data-plant/import-excel-form.tsx +++ b/components/form-data-plant/import-excel-form.tsx @@ -23,7 +23,8 @@ export interface ImportExcelOptions { tableData: GenericData[]; columnsJSON: DataTableColumnJSON[]; formCode?: string; - contractItemId?: number; + projectCode: string; + packageCode: string; editableFieldsMap?: Map; // 새로 추가 onPendingChange?: (isPending: boolean) => void; onDataUpdate?: (updater: ((prev: GenericData[]) => GenericData[]) | GenericData[]) => void; @@ -218,7 +219,8 @@ export async function importExcelData({ tableData, columnsJSON, formCode, - contractItemId, + projectCode, + packageCode, editableFieldsMap = new Map(), // 새로 추가 onPendingChange, onDataUpdate @@ -527,14 +529,14 @@ export async function importExcelData({ } }); - // If formCode and contractItemId are provided, save directly to DB // importExcelData 함수에서 DB 저장 부분 - if (formCode && contractItemId) { + if (formCode && projectCode && packageCode) { try { // 배치 업데이트 함수 호출 const result = await updateFormDataBatchInDB( formCode, - contractItemId, + projectCode, + packageCode, importedData // 모든 imported rows를 한번에 전달 ); @@ -633,7 +635,6 @@ export async function importExcelData({ } } else { - // formCode나 contractItemId가 없는 경우 - 로컬 업데이트만 if (onDataUpdate) { onDataUpdate(() => mergedData); } diff --git a/components/form-data-plant/publish-dialog.tsx b/components/form-data-plant/publish-dialog.tsx index a3a2ef0b..f63c2db8 100644 --- a/components/form-data-plant/publish-dialog.tsx +++ b/components/form-data-plant/publish-dialog.tsx @@ -37,19 +37,21 @@ import { Loader2, Check, ChevronsUpDown } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { - createRevisionAction, - fetchDocumentsByPackageId, - fetchStagesByDocumentId, - fetchRevisionsByStageParams, - Document, - IssueStage, - Revision + createSubmissionAction, // 새로운 액션 이름 + fetchDocumentsByProjectAndPackage, // 업데이트된 액션 + fetchStagesByDocumentIdPlant, + fetchSubmissionsByStageParams, // revisions 대신 submissions } from "@/lib/vendor-document/service"; +import type { + StageDocument, + StageIssueStage, +} from "@/db/schema/vendorDocu"; interface PublishDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - packageId: number; + projectCode: string; + packageCode: string; formCode: string; fileBlob?: Blob; } @@ -57,7 +59,8 @@ interface PublishDialogProps { export const PublishDialog: React.FC = ({ open, onOpenChange, - packageId, + projectCode, + packageCode, formCode, fileBlob, }) => { @@ -65,9 +68,10 @@ export const PublishDialog: React.FC = ({ const { data: session } = useSession(); // State for form data - const [documents, setDocuments] = useState([]); - const [stages, setStages] = useState([]); - const [latestRevision, setLatestRevision] = useState(""); + const [documents, setDocuments] = useState([]); + const [stages, setStages] = useState([]); + const [latestRevisionCode, setLatestRevisionCode] = useState(""); + const [latestRevisionNumber, setLatestRevisionNumber] = useState(0); // State for document search const [openDocumentCombobox, setOpenDocumentCombobox] = useState(false); @@ -77,9 +81,10 @@ export const PublishDialog: React.FC = ({ const [selectedDocId, setSelectedDocId] = useState(""); const [selectedDocumentDisplay, setSelectedDocumentDisplay] = useState(""); const [selectedStage, setSelectedStage] = useState(""); - const [revisionInput, setRevisionInput] = useState(""); - const [uploaderName, setUploaderName] = useState(""); - const [comment, setComment] = useState(""); + const [revisionCodeInput, setRevisionCodeInput] = useState(""); + const [submitterName, setSubmitterName] = useState(""); + const [submissionTitle, setSubmissionTitle] = useState(""); + const [submissionDescription, setSubmissionDescription] = useState(""); const [customFileName, setCustomFileName] = useState(`${formCode}_document.docx`); // Loading states @@ -94,10 +99,10 @@ export const PublishDialog: React.FC = ({ ) : documents; - // Set uploader name from session when dialog opens + // Set submitter name from session when dialog opens useEffect(() => { if (open && session?.user?.name) { - setUploaderName(session.user.name); + setSubmitterName(session.user.name); } }, [open, session]); @@ -107,24 +112,26 @@ export const PublishDialog: React.FC = ({ setSelectedDocId(""); setSelectedDocumentDisplay(""); setSelectedStage(""); - setRevisionInput(""); - // Only set uploaderName if not already set from session - if (!session?.user?.name) setUploaderName(""); - setComment(""); - setLatestRevision(""); + setRevisionCodeInput(""); + setSubmissionTitle(""); + setSubmissionDescription(""); + // Only set submitterName if not already set from session + if (!session?.user?.name) setSubmitterName(""); + setLatestRevisionCode(""); + setLatestRevisionNumber(0); setCustomFileName(`${formCode}_document.docx`); setDocumentSearchValue(""); } }, [open, formCode, session]); - // Fetch documents based on packageId + // Fetch documents based on projectCode and packageCode useEffect(() => { async function loadDocuments() { - if (packageId && open) { + if (projectCode && packageCode && open) { setIsLoading(true); try { - const docs = await fetchDocumentsByPackageId(packageId); + const docs = await fetchDocumentsByProjectAndPackage(projectCode, packageCode); setDocuments(docs); } catch (error) { console.error("Error fetching documents:", error); @@ -136,7 +143,7 @@ export const PublishDialog: React.FC = ({ } loadDocuments(); - }, [packageId, open]); + }, [projectCode, packageCode, open]); // Fetch stages when document is selected useEffect(() => { @@ -146,11 +153,12 @@ export const PublishDialog: React.FC = ({ // Reset dependent fields setSelectedStage(""); - setRevisionInput(""); - setLatestRevision(""); + setRevisionCodeInput(""); + setLatestRevisionCode(""); + setLatestRevisionNumber(0); try { - const stagesList = await fetchStagesByDocumentId(parseInt(selectedDocId, 10)); + const stagesList = await fetchStagesByDocumentIdPlant(parseInt(selectedDocId, 10)); setStages(stagesList); } catch (error) { console.error("Error fetching stages:", error); @@ -166,65 +174,78 @@ export const PublishDialog: React.FC = ({ loadStages(); }, [selectedDocId]); - // Fetch latest revision when stage is selected (for reference) + // Fetch latest submission (revision) when stage is selected useEffect(() => { - async function loadLatestRevision() { + async function loadLatestSubmission() { if (selectedDocId && selectedStage) { setIsLoading(true); try { - const revsList = await fetchRevisionsByStageParams( + const submissionsList = await fetchSubmissionsByStageParams( parseInt(selectedDocId, 10), selectedStage ); - // Find the latest revision (assuming revisions are sorted by revision number) - if (revsList.length > 0) { - // Sort revisions if needed - const sortedRevisions = [...revsList].sort((a, b) => { - return b.revision.localeCompare(a.revision, undefined, { numeric: true }); - }); + // Find the latest submission (assuming sorted by revision number) + if (submissionsList.length > 0) { + // Sort submissions by revision number descending + const sortedSubmissions = [...submissionsList].sort((a, b) => + b.revisionNumber - a.revisionNumber + ); - setLatestRevision(sortedRevisions[0].revision); + const latestSubmission = sortedSubmissions[0]; + setLatestRevisionCode(latestSubmission.revisionCode); + setLatestRevisionNumber(latestSubmission.revisionNumber); - // Pre-fill the revision input with an incremented value if possible - if (sortedRevisions[0].revision.match(/^\d+$/)) { + // Auto-increment revision code + if (latestSubmission.revisionCode.match(/^\d+$/)) { // If it's a number, increment it - const nextRevision = String(parseInt(sortedRevisions[0].revision, 10) + 1); - setRevisionInput(nextRevision); - } else if (sortedRevisions[0].revision.match(/^[A-Za-z]$/)) { + const nextRevision = String(parseInt(latestSubmission.revisionCode, 10) + 1); + setRevisionCodeInput(nextRevision); + } else if (latestSubmission.revisionCode.match(/^[A-Za-z]$/)) { // If it's a single letter, get the next letter - const currentChar = sortedRevisions[0].revision.charCodeAt(0); + const currentChar = latestSubmission.revisionCode.charCodeAt(0); const nextChar = String.fromCharCode(currentChar + 1); - setRevisionInput(nextChar); + setRevisionCodeInput(nextChar); + } else if (latestSubmission.revisionCode.toLowerCase().startsWith("rev")) { + // Handle "Rev0", "Rev1" format + const numMatch = latestSubmission.revisionCode.match(/\d+$/); + if (numMatch) { + const nextNum = parseInt(numMatch[0], 10) + 1; + setRevisionCodeInput(`Rev${nextNum}`); + } else { + setRevisionCodeInput(""); + } } else { // For other formats, just show the latest as reference - setRevisionInput(""); + setRevisionCodeInput(""); } } else { - // If no revisions exist, set default values - setLatestRevision(""); - setRevisionInput("0"); + // If no submissions exist, set default values + setLatestRevisionCode(""); + setLatestRevisionNumber(0); + setRevisionCodeInput("Rev0"); // Start with Rev0 } } catch (error) { - console.error("Error fetching revisions:", error); - toast.error("Failed to load revision information"); + console.error("Error fetching submissions:", error); + toast.error("Failed to load submission information"); } finally { setIsLoading(false); } } else { - setLatestRevision(""); - setRevisionInput(""); + setLatestRevisionCode(""); + setLatestRevisionNumber(0); + setRevisionCodeInput(""); } } - loadLatestRevision(); + loadLatestSubmission(); }, [selectedDocId, selectedStage]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!selectedDocId || !selectedStage || !revisionInput || !fileBlob) { + if (!selectedDocId || !selectedStage || !revisionCodeInput || !fileBlob) { toast.error("Please fill in all required fields"); return; } @@ -235,17 +256,30 @@ export const PublishDialog: React.FC = ({ // Create FormData const formData = new FormData(); formData.append("documentId", selectedDocId); - formData.append("stage", selectedStage); - formData.append("revision", revisionInput); + formData.append("stageName", selectedStage); + formData.append("revisionCode", revisionCodeInput); formData.append("customFileName", customFileName); - formData.append("uploaderType", "vendor"); // Default value - if (uploaderName) { - formData.append("uploaderName", uploaderName); + if (submitterName) { + formData.append("submittedBy", submitterName); } - if (comment) { - formData.append("comment", comment); + if (session?.user?.email) { + formData.append("submittedByEmail", session.user.email); + } + + if (submissionTitle) { + formData.append("submissionTitle", submissionTitle); + } + + if (submissionDescription) { + formData.append("submissionDescription", submissionDescription); + } + + // Get vendor info from selected document + const selectedDoc = documents.find(doc => doc.id === parseInt(selectedDocId, 10)); + if (selectedDoc) { + formData.append("vendorId", String(selectedDoc.vendorId)); } // Append file as attachment @@ -256,12 +290,14 @@ export const PublishDialog: React.FC = ({ formData.append("attachment", file); } - // Call server action directly - const result = await createRevisionAction(formData); + // Call server action + const result = await createSubmissionAction(formData); - if (result) { + if (result.success) { toast.success("Document published successfully!"); onOpenChange(false); + } else { + toast.error(result.error || "Failed to publish document"); } } catch (error) { console.error("Error publishing document:", error); @@ -301,7 +337,6 @@ export const PublishDialog: React.FC = ({ className="w-full justify-between" disabled={isLoading || documents.length === 0} > - {/* Add text-overflow handling for selected document display */} {selectedDocumentDisplay ? selectedDocumentDisplay @@ -338,7 +373,6 @@ export const PublishDialog: React.FC = ({ : "opacity-0" )} /> - {/* Add text-overflow handling for document items */} {doc.docNumber} - {doc.title} ))} @@ -366,7 +400,6 @@ export const PublishDialog: React.FC = ({ {stages.map((stage) => ( - {/* Add text-overflow handling for stage names */} {stage.stageName} ))} @@ -375,27 +408,41 @@ export const PublishDialog: React.FC = ({
- {/* Revision Input */} + {/* Revision Code Input */}
-
+
+ +
+ setSubmissionTitle(e.target.value)} + placeholder="Optional submission title" + /> +
+
+
-