From 5b6313f16f508882a0ea67716b7dbaa1c6967f04 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 30 Jun 2025 08:28:13 +0000 Subject: (대표님) 20250630 16시 - 유저 도메인별 라우터 분리와 보안성검토 대응 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(engineering)/b-rfq/[id]/final/page.tsx | 0 .../(engineering)/b-rfq/[id]/initial/page.tsx | 52 + .../(engineering)/b-rfq/[id]/layout.tsx | 87 + .../engineering/(engineering)/b-rfq/[id]/page.tsx | 53 + app/[lng]/engineering/(engineering)/b-rfq/page.tsx | 79 + .../(engineering)/basic-contract-template/page.tsx | 74 + .../(engineering)/basic-contract/page.tsx | 74 + .../(engineering)/bid-projects/page.tsx | 74 + app/[lng]/engineering/(engineering)/bqcbe/page.tsx | 74 + app/[lng]/engineering/(engineering)/bqtbe/page.tsx | 72 + .../(engineering)/budgetary-rfq/[id]/cbe/page.tsx | 56 + .../(engineering)/budgetary-rfq/[id]/layout.tsx | 90 + .../(engineering)/budgetary-rfq/[id]/page.tsx | 57 + .../(engineering)/budgetary-rfq/[id]/tbe/page.tsx | 55 + .../(engineering)/budgetary-rfq/page.tsx | 86 + .../budgetary-tech-sales-hull/page.tsx | 61 + .../budgetary-tech-sales-ship/page.tsx | 61 + .../budgetary-tech-sales-top/page.tsx | 61 + .../(engineering)/budgetary/[id]/cbe/page.tsx | 56 + .../(engineering)/budgetary/[id]/layout.tsx | 90 + .../(engineering)/budgetary/[id]/page.tsx | 57 + .../(engineering)/budgetary/[id]/tbe/page.tsx | 55 + .../engineering/(engineering)/budgetary/page.tsx | 86 + .../engineering/(engineering)/cbe-tech/page.tsx | 67 + .../engineering/(engineering)/dashboard/page.tsx | 17 + .../(engineering)/email-template/[name]/page.tsx | 26 + .../(engineering)/email-template/page.tsx | 19 + .../engineering/(engineering)/equip-class/page.tsx | 75 + .../(engineering)/esg-check-list/page.tsx | 74 + .../(engineering)/evaluation-check-list/page.tsx | 81 + .../(engineering)/evaluation-target-list/page.tsx | 115 + .../engineering/(engineering)/evaluation/page.tsx | 181 + .../(engineering)/faq/manage/actions.ts | 48 + .../engineering/(engineering)/faq/manage/page.tsx | 38 + app/[lng]/engineering/(engineering)/faq/page.tsx | 62 + .../engineering/(engineering)/form-list/page.tsx | 75 + .../engineering/(engineering)/incoterms/page.tsx | 53 + .../(engineering)/items-tech/layout.tsx | 38 + .../engineering/(engineering)/items-tech/page.tsx | 67 + app/[lng]/engineering/(engineering)/items/page.tsx | 68 + app/[lng]/engineering/(engineering)/layout.tsx | 18 + .../engineering/(engineering)/menu-list/page.tsx | 70 + .../(engineering)/payment-conditions/page.tsx | 53 + .../engineering/(engineering)/po-rfq/page.tsx | 61 + app/[lng]/engineering/(engineering)/po/page.tsx | 65 + app/[lng]/engineering/(engineering)/poa/page.tsx | 61 + .../(engineering)/pq-criteria/[id]/page.tsx | 81 + .../engineering/(engineering)/pq-criteria/page.tsx | 70 + .../(engineering)/pq/[vendorId]/page.tsx | 108 + app/[lng]/engineering/(engineering)/pq/page.tsx | 71 + .../pq_new/[vendorId]/[submissionId]/page.tsx | 215 + .../engineering/(engineering)/pq_new/page.tsx | 96 + .../engineering/(engineering)/project-gtc/page.tsx | 63 + .../(engineering)/project-vendors/page.tsx | 74 + .../engineering/(engineering)/projects/page.tsx | 75 + .../engineering/(engineering)/report/page.tsx | 47 + .../(engineering)/rfq-tech/[id]/cbe/page.tsx | 55 + .../(engineering)/rfq-tech/[id]/layout.tsx | 89 + .../(engineering)/rfq-tech/[id]/page.tsx | 55 + .../(engineering)/rfq-tech/[id]/tbe/page.tsx | 55 + .../engineering/(engineering)/rfq-tech/page.tsx | 76 + .../(engineering)/rfq/[id]/cbe/page.tsx | 55 + .../engineering/(engineering)/rfq/[id]/layout.tsx | 89 + .../engineering/(engineering)/rfq/[id]/page.tsx | 55 + .../(engineering)/rfq/[id]/tbe/page.tsx | 55 + app/[lng]/engineering/(engineering)/rfq/page.tsx | 80 + .../engineering/(engineering)/settings/layout.tsx | 68 + .../engineering/(engineering)/settings/page.tsx | 18 + .../(engineering)/settings/preferences/page.tsx | 17 + .../(engineering)/system/admin-users/page.tsx | 60 + .../engineering/(engineering)/system/layout.tsx | 80 + .../engineering/(engineering)/system/page.tsx | 56 + .../(engineering)/system/password-policy/page.tsx | 63 + .../(engineering)/system/permissions/page.tsx | 17 + .../(engineering)/system/roles/page.tsx | 68 + .../(engineering)/tag-numbering/page.tsx | 74 + app/[lng]/engineering/(engineering)/tasks/page.tsx | 63 + .../engineering/(engineering)/tbe-tech/page.tsx | 67 + app/[lng]/engineering/(engineering)/tbe/page.tsx | 113 + .../(engineering)/tech-project-avl/page.tsx | 85 + .../(engineering)/tech-vendor-candidates/page.tsx | 78 + .../tech-vendors/[id]/info/items/page.tsx | 48 + .../tech-vendors/[id]/info/layout.tsx | 82 + .../(engineering)/tech-vendors/[id]/info/page.tsx | 55 + .../tech-vendors/[id]/info/rfq-history/page.tsx | 55 + .../(engineering)/tech-vendors/page.tsx | 58 + .../(engineering)/vendor-candidates/page.tsx | 78 + .../(engineering)/vendor-check-list/page.tsx | 74 + .../(engineering)/vendor-investigation/page.tsx | 65 + .../engineering/(engineering)/vendor-type/page.tsx | 70 + .../(engineering)/vendors/[id]/info/items/page.tsx | 56 + .../(engineering)/vendors/[id]/info/layout.tsx | 94 + .../vendors/[id]/info/materials/page.tsx | 56 + .../(engineering)/vendors/[id]/info/page.tsx | 56 + .../vendors/[id]/info/rfq-history/page.tsx | 55 + .../engineering/(engineering)/vendors/page.tsx | 78 + app/[lng]/engineering/page.tsx | 21 + app/[lng]/evcp/(evcp)/menu-access/page.tsx | 53 + app/[lng]/evcp/(evcp)/menu-list/page.tsx | 70 + app/[lng]/pending/layout.tsx | 42 + app/[lng]/pending/page.tsx | 129 + .../(procurement)/b-rfq/[id]/final/page.tsx | 0 .../(procurement)/b-rfq/[id]/initial/page.tsx | 52 + .../(procurement)/b-rfq/[id]/layout.tsx | 87 + .../procurement/(procurement)/b-rfq/[id]/page.tsx | 53 + app/[lng]/procurement/(procurement)/b-rfq/page.tsx | 79 + .../(procurement)/basic-contract-template/page.tsx | 74 + .../(procurement)/basic-contract/page.tsx | 74 + .../(procurement)/bid-projects/page.tsx | 74 + app/[lng]/procurement/(procurement)/bqcbe/page.tsx | 74 + app/[lng]/procurement/(procurement)/bqtbe/page.tsx | 72 + .../(procurement)/budgetary-rfq/[id]/cbe/page.tsx | 56 + .../(procurement)/budgetary-rfq/[id]/layout.tsx | 90 + .../(procurement)/budgetary-rfq/[id]/page.tsx | 57 + .../(procurement)/budgetary-rfq/[id]/tbe/page.tsx | 55 + .../(procurement)/budgetary-rfq/page.tsx | 86 + .../budgetary-tech-sales-hull/page.tsx | 61 + .../budgetary-tech-sales-ship/page.tsx | 61 + .../budgetary-tech-sales-top/page.tsx | 61 + .../(procurement)/budgetary/[id]/cbe/page.tsx | 56 + .../(procurement)/budgetary/[id]/layout.tsx | 90 + .../(procurement)/budgetary/[id]/page.tsx | 57 + .../(procurement)/budgetary/[id]/tbe/page.tsx | 55 + .../procurement/(procurement)/budgetary/page.tsx | 86 + .../procurement/(procurement)/cbe-tech/page.tsx | 67 + .../procurement/(procurement)/dashboard/page.tsx | 17 + .../(procurement)/email-template/[name]/page.tsx | 26 + .../(procurement)/email-template/page.tsx | 19 + .../procurement/(procurement)/equip-class/page.tsx | 75 + .../(procurement)/esg-check-list/page.tsx | 74 + .../(procurement)/evaluation-check-list/page.tsx | 81 + .../(procurement)/evaluation-target-list/page.tsx | 115 + .../procurement/(procurement)/evaluation/page.tsx | 181 + .../(procurement)/faq/manage/actions.ts | 48 + .../procurement/(procurement)/faq/manage/page.tsx | 38 + app/[lng]/procurement/(procurement)/faq/page.tsx | 62 + .../procurement/(procurement)/form-list/page.tsx | 75 + .../procurement/(procurement)/incoterms/page.tsx | 53 + .../(procurement)/items-tech/layout.tsx | 38 + .../procurement/(procurement)/items-tech/page.tsx | 67 + app/[lng]/procurement/(procurement)/items/page.tsx | 68 + app/[lng]/procurement/(procurement)/layout.tsx | 18 + .../procurement/(procurement)/menu-list/page.tsx | 70 + .../(procurement)/payment-conditions/page.tsx | 53 + .../procurement/(procurement)/po-rfq/page.tsx | 61 + app/[lng]/procurement/(procurement)/po/page.tsx | 65 + app/[lng]/procurement/(procurement)/poa/page.tsx | 61 + .../(procurement)/pq-criteria/[id]/page.tsx | 81 + .../procurement/(procurement)/pq-criteria/page.tsx | 70 + .../(procurement)/pq/[vendorId]/page.tsx | 108 + app/[lng]/procurement/(procurement)/pq/page.tsx | 71 + .../pq_new/[vendorId]/[submissionId]/page.tsx | 215 + .../procurement/(procurement)/pq_new/page.tsx | 96 + .../procurement/(procurement)/project-gtc/page.tsx | 63 + .../(procurement)/project-vendors/page.tsx | 74 + .../procurement/(procurement)/projects/page.tsx | 75 + .../procurement/(procurement)/report/page.tsx | 47 + .../(procurement)/rfq-tech/[id]/cbe/page.tsx | 55 + .../(procurement)/rfq-tech/[id]/layout.tsx | 89 + .../(procurement)/rfq-tech/[id]/page.tsx | 55 + .../(procurement)/rfq-tech/[id]/tbe/page.tsx | 55 + .../procurement/(procurement)/rfq-tech/page.tsx | 76 + .../(procurement)/rfq/[id]/cbe/page.tsx | 55 + .../procurement/(procurement)/rfq/[id]/layout.tsx | 89 + .../procurement/(procurement)/rfq/[id]/page.tsx | 55 + .../(procurement)/rfq/[id]/tbe/page.tsx | 55 + app/[lng]/procurement/(procurement)/rfq/page.tsx | 80 + .../procurement/(procurement)/settings/layout.tsx | 68 + .../procurement/(procurement)/settings/page.tsx | 18 + .../(procurement)/settings/preferences/page.tsx | 17 + .../(procurement)/system/admin-users/page.tsx | 60 + .../procurement/(procurement)/system/layout.tsx | 80 + .../procurement/(procurement)/system/page.tsx | 56 + .../(procurement)/system/password-policy/page.tsx | 63 + .../(procurement)/system/permissions/page.tsx | 17 + .../(procurement)/system/roles/page.tsx | 68 + .../(procurement)/tag-numbering/page.tsx | 74 + app/[lng]/procurement/(procurement)/tasks/page.tsx | 63 + .../procurement/(procurement)/tbe-tech/page.tsx | 67 + app/[lng]/procurement/(procurement)/tbe/page.tsx | 113 + .../(procurement)/tech-project-avl/page.tsx | 85 + .../(procurement)/tech-vendor-candidates/page.tsx | 78 + .../tech-vendors/[id]/info/items/page.tsx | 48 + .../tech-vendors/[id]/info/layout.tsx | 82 + .../(procurement)/tech-vendors/[id]/info/page.tsx | 55 + .../tech-vendors/[id]/info/rfq-history/page.tsx | 55 + .../(procurement)/tech-vendors/page.tsx | 58 + .../(procurement)/vendor-candidates/page.tsx | 78 + .../(procurement)/vendor-check-list/page.tsx | 74 + .../(procurement)/vendor-investigation/page.tsx | 65 + .../procurement/(procurement)/vendor-type/page.tsx | 70 + .../(procurement)/vendors/[id]/info/items/page.tsx | 56 + .../(procurement)/vendors/[id]/info/layout.tsx | 94 + .../vendors/[id]/info/materials/page.tsx | 56 + .../(procurement)/vendors/[id]/info/page.tsx | 56 + .../vendors/[id]/info/rfq-history/page.tsx | 55 + .../procurement/(procurement)/vendors/page.tsx | 78 + app/[lng]/procurement/page.tsx | 21 + app/[lng]/sales/(sales)/b-rfq/[id]/final/page.tsx | 0 .../sales/(sales)/b-rfq/[id]/initial/page.tsx | 52 + app/[lng]/sales/(sales)/b-rfq/[id]/layout.tsx | 87 + app/[lng]/sales/(sales)/b-rfq/[id]/page.tsx | 53 + app/[lng]/sales/(sales)/b-rfq/page.tsx | 79 + .../sales/(sales)/basic-contract-template/page.tsx | 74 + app/[lng]/sales/(sales)/basic-contract/page.tsx | 74 + app/[lng]/sales/(sales)/bid-projects/page.tsx | 74 + app/[lng]/sales/(sales)/bqcbe/page.tsx | 74 + app/[lng]/sales/(sales)/bqtbe/page.tsx | 72 + .../sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx | 56 + .../sales/(sales)/budgetary-rfq/[id]/layout.tsx | 90 + .../sales/(sales)/budgetary-rfq/[id]/page.tsx | 57 + .../sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx | 55 + app/[lng]/sales/(sales)/budgetary-rfq/page.tsx | 86 + .../(sales)/budgetary-tech-sales-hull/page.tsx | 61 + .../(sales)/budgetary-tech-sales-ship/page.tsx | 61 + .../(sales)/budgetary-tech-sales-top/page.tsx | 61 + .../sales/(sales)/budgetary/[id]/cbe/page.tsx | 56 + app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx | 90 + app/[lng]/sales/(sales)/budgetary/[id]/page.tsx | 57 + .../sales/(sales)/budgetary/[id]/tbe/page.tsx | 55 + app/[lng]/sales/(sales)/budgetary/page.tsx | 86 + app/[lng]/sales/(sales)/cbe-tech/page.tsx | 67 + app/[lng]/sales/(sales)/dashboard/page.tsx | 17 + .../sales/(sales)/email-template/[name]/page.tsx | 26 + app/[lng]/sales/(sales)/email-template/page.tsx | 19 + app/[lng]/sales/(sales)/equip-class/page.tsx | 75 + app/[lng]/sales/(sales)/esg-check-list/page.tsx | 74 + .../sales/(sales)/evaluation-check-list/page.tsx | 81 + .../sales/(sales)/evaluation-target-list/page.tsx | 115 + app/[lng]/sales/(sales)/evaluation/page.tsx | 181 + app/[lng]/sales/(sales)/faq/manage/actions.ts | 48 + app/[lng]/sales/(sales)/faq/manage/page.tsx | 38 + app/[lng]/sales/(sales)/faq/page.tsx | 62 + app/[lng]/sales/(sales)/form-list/page.tsx | 75 + app/[lng]/sales/(sales)/incoterms/page.tsx | 53 + app/[lng]/sales/(sales)/items-tech/layout.tsx | 38 + app/[lng]/sales/(sales)/items-tech/page.tsx | 67 + app/[lng]/sales/(sales)/items/page.tsx | 68 + app/[lng]/sales/(sales)/layout.tsx | 18 + app/[lng]/sales/(sales)/menu-list/page.tsx | 70 + .../sales/(sales)/payment-conditions/page.tsx | 53 + app/[lng]/sales/(sales)/po-rfq/page.tsx | 61 + app/[lng]/sales/(sales)/po/page.tsx | 65 + app/[lng]/sales/(sales)/poa/page.tsx | 61 + app/[lng]/sales/(sales)/pq-criteria/[id]/page.tsx | 81 + app/[lng]/sales/(sales)/pq-criteria/page.tsx | 70 + app/[lng]/sales/(sales)/pq/[vendorId]/page.tsx | 108 + app/[lng]/sales/(sales)/pq/page.tsx | 71 + .../pq_new/[vendorId]/[submissionId]/page.tsx | 215 + app/[lng]/sales/(sales)/pq_new/page.tsx | 96 + app/[lng]/sales/(sales)/project-gtc/page.tsx | 63 + app/[lng]/sales/(sales)/project-vendors/page.tsx | 74 + app/[lng]/sales/(sales)/projects/page.tsx | 75 + app/[lng]/sales/(sales)/report/page.tsx | 47 + app/[lng]/sales/(sales)/rfq-tech/[id]/cbe/page.tsx | 55 + app/[lng]/sales/(sales)/rfq-tech/[id]/layout.tsx | 89 + app/[lng]/sales/(sales)/rfq-tech/[id]/page.tsx | 55 + app/[lng]/sales/(sales)/rfq-tech/[id]/tbe/page.tsx | 55 + app/[lng]/sales/(sales)/rfq-tech/page.tsx | 76 + app/[lng]/sales/(sales)/rfq/[id]/cbe/page.tsx | 55 + app/[lng]/sales/(sales)/rfq/[id]/layout.tsx | 89 + app/[lng]/sales/(sales)/rfq/[id]/page.tsx | 55 + app/[lng]/sales/(sales)/rfq/[id]/tbe/page.tsx | 55 + app/[lng]/sales/(sales)/rfq/page.tsx | 80 + app/[lng]/sales/(sales)/settings/layout.tsx | 68 + app/[lng]/sales/(sales)/settings/page.tsx | 18 + .../sales/(sales)/settings/preferences/page.tsx | 17 + .../sales/(sales)/system/admin-users/page.tsx | 60 + app/[lng]/sales/(sales)/system/layout.tsx | 80 + app/[lng]/sales/(sales)/system/page.tsx | 56 + .../sales/(sales)/system/password-policy/page.tsx | 63 + .../sales/(sales)/system/permissions/page.tsx | 17 + app/[lng]/sales/(sales)/system/roles/page.tsx | 68 + app/[lng]/sales/(sales)/tag-numbering/page.tsx | 74 + app/[lng]/sales/(sales)/tasks/page.tsx | 63 + app/[lng]/sales/(sales)/tbe-tech/page.tsx | 67 + app/[lng]/sales/(sales)/tbe/page.tsx | 113 + app/[lng]/sales/(sales)/tech-project-avl/page.tsx | 85 + .../sales/(sales)/tech-vendor-candidates/page.tsx | 78 + .../(sales)/tech-vendors/[id]/info/items/page.tsx | 48 + .../(sales)/tech-vendors/[id]/info/layout.tsx | 82 + .../sales/(sales)/tech-vendors/[id]/info/page.tsx | 55 + .../tech-vendors/[id]/info/rfq-history/page.tsx | 55 + app/[lng]/sales/(sales)/tech-vendors/page.tsx | 58 + app/[lng]/sales/(sales)/vendor-candidates/page.tsx | 78 + app/[lng]/sales/(sales)/vendor-check-list/page.tsx | 74 + .../sales/(sales)/vendor-investigation/page.tsx | 65 + app/[lng]/sales/(sales)/vendor-type/page.tsx | 70 + .../sales/(sales)/vendors/[id]/info/items/page.tsx | 56 + .../sales/(sales)/vendors/[id]/info/layout.tsx | 94 + .../(sales)/vendors/[id]/info/materials/page.tsx | 56 + app/[lng]/sales/(sales)/vendors/[id]/info/page.tsx | 56 + .../(sales)/vendors/[id]/info/rfq-history/page.tsx | 55 + app/[lng]/sales/(sales)/vendors/page.tsx | 78 + app/[lng]/sales/page.tsx | 21 + app/api/auth/[...nextauth]/saml/provider.ts | 3 +- app/api/menu-assignments/active-status/route.ts | 32 + components/layout/GroupedMenuRender.tsx | 53 +- components/layout/Header.tsx | 117 +- components/layout/MobileMenu.tsx | 87 +- components/layout/user-profile-badge.tsx | 62 + config/menuConfig.ts | 499 +- config/userAccessColumnsConfig.ts | 65 + db/migrations/0170_curved_chimera.sql | 1661 + db/migrations/0171_brainy_slayback.sql | 5 + db/migrations/0172_strong_betty_brant.sql | 2 + db/migrations/0173_soft_sasquatch.sql | 1 + db/migrations/meta/0170_snapshot.json | 30938 ++++++++++++++++++ db/migrations/meta/0171_snapshot.json | 30942 ++++++++++++++++++ db/migrations/meta/0172_snapshot.json | 30948 ++++++++++++++++++ db/migrations/meta/0173_snapshot.json | 30955 +++++++++++++++++++ db/migrations/meta/_journal.json | 28 + db/schema/evaluationTarget.ts | 2 +- db/schema/index.ts | 1 + db/schema/menu.ts | 55 + db/schema/users.ts | 72 +- hooks/use-active-menus.ts | 68 + lib/admin-users/repository.ts | 32 + lib/admin-users/validations.ts | 21 +- lib/menu-list/servcie.ts | 239 + lib/menu-list/table/initialize-button.tsx | 42 + lib/menu-list/table/manager-select.tsx | 192 + lib/menu-list/table/menu-list-table.tsx | 280 + lib/users/access-control/assign-domain-dialog.tsx | 253 + lib/users/access-control/domain-stats-cards.tsx | 232 + lib/users/access-control/users-table-columns.tsx | 149 + .../access-control/users-table-toolbar-actions.tsx | 51 + lib/users/access-control/users-table.tsx | 166 + lib/users/auth/verifyCredentails.ts | 5 +- lib/users/service.ts | 250 +- middleware.ts | 118 +- 331 files changed, 148063 insertions(+), 159 deletions(-) create mode 100644 app/[lng]/engineering/(engineering)/b-rfq/[id]/final/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/b-rfq/[id]/initial/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/b-rfq/[id]/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/b-rfq/[id]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/b-rfq/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/basic-contract-template/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/basic-contract/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/bid-projects/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/bqcbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/bqtbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-rfq/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-tech-sales-hull/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-tech-sales-ship/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary-tech-sales-top/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary/[id]/cbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary/[id]/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary/[id]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary/[id]/tbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/budgetary/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/cbe-tech/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/dashboard/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/email-template/[name]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/email-template/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/equip-class/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/esg-check-list/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/evaluation-check-list/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/evaluation-target-list/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/evaluation/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/faq/manage/actions.ts create mode 100644 app/[lng]/engineering/(engineering)/faq/manage/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/faq/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/form-list/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/incoterms/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/items-tech/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/items-tech/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/items/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/menu-list/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/payment-conditions/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/po-rfq/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/po/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/poa/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/pq-criteria/[id]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/pq-criteria/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/pq/[vendorId]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/pq/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/pq_new/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/project-gtc/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/project-vendors/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/projects/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/report/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq-tech/[id]/cbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq-tech/[id]/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq-tech/[id]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq-tech/[id]/tbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq-tech/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq/[id]/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq/[id]/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/rfq/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/settings/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/settings/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/settings/preferences/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/system/admin-users/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/system/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/system/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/system/password-policy/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/system/permissions/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/system/roles/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tag-numbering/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tasks/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tbe-tech/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tbe/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-vendor-candidates/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/tech-vendors/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-candidates/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendor-type/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendors/[id]/info/materials/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendors/[id]/info/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/engineering/(engineering)/vendors/page.tsx create mode 100644 app/[lng]/engineering/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/menu-access/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/menu-list/page.tsx create mode 100644 app/[lng]/pending/layout.tsx create mode 100644 app/[lng]/pending/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/b-rfq/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/basic-contract/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/bid-projects/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/bqcbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/bqtbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-tech-sales-hull/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-tech-sales-ship/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary-tech-sales-top/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/budgetary/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/cbe-tech/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/dashboard/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/email-template/[name]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/email-template/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/equip-class/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/esg-check-list/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/evaluation/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/faq/manage/actions.ts create mode 100644 app/[lng]/procurement/(procurement)/faq/manage/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/faq/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/form-list/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/incoterms/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/items-tech/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/items-tech/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/items/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/menu-list/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/payment-conditions/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/po-rfq/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/po/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/poa/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/pq-criteria/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/pq/[vendorId]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/pq/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/pq_new/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/project-gtc/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/project-vendors/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/projects/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/report/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq-tech/[id]/cbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq-tech/[id]/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq-tech/[id]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq-tech/[id]/tbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq-tech/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/rfq/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/settings/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/settings/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/settings/preferences/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/system/admin-users/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/system/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/system/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/system/password-policy/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/system/permissions/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/system/roles/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tag-numbering/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tasks/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tbe-tech/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tbe/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-project-avl/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-vendor-candidates/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/tech-vendors/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendor-type/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/procurement/(procurement)/vendors/page.tsx create mode 100644 app/[lng]/procurement/page.tsx create mode 100644 app/[lng]/sales/(sales)/b-rfq/[id]/final/page.tsx create mode 100644 app/[lng]/sales/(sales)/b-rfq/[id]/initial/page.tsx create mode 100644 app/[lng]/sales/(sales)/b-rfq/[id]/layout.tsx create mode 100644 app/[lng]/sales/(sales)/b-rfq/[id]/page.tsx create mode 100644 app/[lng]/sales/(sales)/b-rfq/page.tsx create mode 100644 app/[lng]/sales/(sales)/basic-contract-template/page.tsx create mode 100644 app/[lng]/sales/(sales)/basic-contract/page.tsx create mode 100644 app/[lng]/sales/(sales)/bid-projects/page.tsx create mode 100644 app/[lng]/sales/(sales)/bqcbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/bqtbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-rfq/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/budgetary/page.tsx create mode 100644 app/[lng]/sales/(sales)/cbe-tech/page.tsx create mode 100644 app/[lng]/sales/(sales)/dashboard/page.tsx create mode 100644 app/[lng]/sales/(sales)/email-template/[name]/page.tsx create mode 100644 app/[lng]/sales/(sales)/email-template/page.tsx create mode 100644 app/[lng]/sales/(sales)/equip-class/page.tsx create mode 100644 app/[lng]/sales/(sales)/esg-check-list/page.tsx create mode 100644 app/[lng]/sales/(sales)/evaluation-check-list/page.tsx create mode 100644 app/[lng]/sales/(sales)/evaluation-target-list/page.tsx create mode 100644 app/[lng]/sales/(sales)/evaluation/page.tsx create mode 100644 app/[lng]/sales/(sales)/faq/manage/actions.ts create mode 100644 app/[lng]/sales/(sales)/faq/manage/page.tsx create mode 100644 app/[lng]/sales/(sales)/faq/page.tsx create mode 100644 app/[lng]/sales/(sales)/form-list/page.tsx create mode 100644 app/[lng]/sales/(sales)/incoterms/page.tsx create mode 100644 app/[lng]/sales/(sales)/items-tech/layout.tsx create mode 100644 app/[lng]/sales/(sales)/items-tech/page.tsx create mode 100644 app/[lng]/sales/(sales)/items/page.tsx create mode 100644 app/[lng]/sales/(sales)/layout.tsx create mode 100644 app/[lng]/sales/(sales)/menu-list/page.tsx create mode 100644 app/[lng]/sales/(sales)/payment-conditions/page.tsx create mode 100644 app/[lng]/sales/(sales)/po-rfq/page.tsx create mode 100644 app/[lng]/sales/(sales)/po/page.tsx create mode 100644 app/[lng]/sales/(sales)/poa/page.tsx create mode 100644 app/[lng]/sales/(sales)/pq-criteria/[id]/page.tsx create mode 100644 app/[lng]/sales/(sales)/pq-criteria/page.tsx create mode 100644 app/[lng]/sales/(sales)/pq/[vendorId]/page.tsx create mode 100644 app/[lng]/sales/(sales)/pq/page.tsx create mode 100644 app/[lng]/sales/(sales)/pq_new/[vendorId]/[submissionId]/page.tsx create mode 100644 app/[lng]/sales/(sales)/pq_new/page.tsx create mode 100644 app/[lng]/sales/(sales)/project-gtc/page.tsx create mode 100644 app/[lng]/sales/(sales)/project-vendors/page.tsx create mode 100644 app/[lng]/sales/(sales)/projects/page.tsx create mode 100644 app/[lng]/sales/(sales)/report/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq-tech/[id]/cbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq-tech/[id]/layout.tsx create mode 100644 app/[lng]/sales/(sales)/rfq-tech/[id]/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq-tech/[id]/tbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq-tech/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq/[id]/layout.tsx create mode 100644 app/[lng]/sales/(sales)/rfq/[id]/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/rfq/page.tsx create mode 100644 app/[lng]/sales/(sales)/settings/layout.tsx create mode 100644 app/[lng]/sales/(sales)/settings/page.tsx create mode 100644 app/[lng]/sales/(sales)/settings/preferences/page.tsx create mode 100644 app/[lng]/sales/(sales)/system/admin-users/page.tsx create mode 100644 app/[lng]/sales/(sales)/system/layout.tsx create mode 100644 app/[lng]/sales/(sales)/system/page.tsx create mode 100644 app/[lng]/sales/(sales)/system/password-policy/page.tsx create mode 100644 app/[lng]/sales/(sales)/system/permissions/page.tsx create mode 100644 app/[lng]/sales/(sales)/system/roles/page.tsx create mode 100644 app/[lng]/sales/(sales)/tag-numbering/page.tsx create mode 100644 app/[lng]/sales/(sales)/tasks/page.tsx create mode 100644 app/[lng]/sales/(sales)/tbe-tech/page.tsx create mode 100644 app/[lng]/sales/(sales)/tbe/page.tsx create mode 100644 app/[lng]/sales/(sales)/tech-project-avl/page.tsx create mode 100644 app/[lng]/sales/(sales)/tech-vendor-candidates/page.tsx create mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx create mode 100644 app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/sales/(sales)/tech-vendors/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendor-candidates/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendor-check-list/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendor-investigation/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendor-type/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/sales/(sales)/vendors/[id]/info/materials/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendors/[id]/info/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/sales/(sales)/vendors/page.tsx create mode 100644 app/[lng]/sales/page.tsx create mode 100644 app/api/menu-assignments/active-status/route.ts create mode 100644 components/layout/user-profile-badge.tsx create mode 100644 config/userAccessColumnsConfig.ts create mode 100644 db/migrations/0170_curved_chimera.sql create mode 100644 db/migrations/0171_brainy_slayback.sql create mode 100644 db/migrations/0172_strong_betty_brant.sql create mode 100644 db/migrations/0173_soft_sasquatch.sql create mode 100644 db/migrations/meta/0170_snapshot.json create mode 100644 db/migrations/meta/0171_snapshot.json create mode 100644 db/migrations/meta/0172_snapshot.json create mode 100644 db/migrations/meta/0173_snapshot.json create mode 100644 db/schema/menu.ts create mode 100644 hooks/use-active-menus.ts create mode 100644 lib/menu-list/servcie.ts create mode 100644 lib/menu-list/table/initialize-button.tsx create mode 100644 lib/menu-list/table/manager-select.tsx create mode 100644 lib/menu-list/table/menu-list-table.tsx create mode 100644 lib/users/access-control/assign-domain-dialog.tsx create mode 100644 lib/users/access-control/domain-stats-cards.tsx create mode 100644 lib/users/access-control/users-table-columns.tsx create mode 100644 lib/users/access-control/users-table-toolbar-actions.tsx create mode 100644 lib/users/access-control/users-table.tsx diff --git a/app/[lng]/engineering/(engineering)/b-rfq/[id]/final/page.tsx b/app/[lng]/engineering/(engineering)/b-rfq/[id]/final/page.tsx new file mode 100644 index 00000000..e69de29b diff --git a/app/[lng]/engineering/(engineering)/b-rfq/[id]/initial/page.tsx b/app/[lng]/engineering/(engineering)/b-rfq/[id]/initial/page.tsx new file mode 100644 index 00000000..1af65fbc --- /dev/null +++ b/app/[lng]/engineering/(engineering)/b-rfq/[id]/initial/page.tsx @@ -0,0 +1,52 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { InitialRfqDetailTable } from "@/lib/b-rfq/initial/initial-rfq-detail-table" +import { getInitialRfqDetail } from "@/lib/b-rfq/service" +import { searchParamsInitialRfqDetailCache } from "@/lib/b-rfq/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsInitialRfqDetailCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getInitialRfqDetail({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ Initial RFQ List +

+

+ 설계로부터 받은 RFQ 문서와 구매 RFQ 문서 및 사전 계약자료를 Vendor에 발송하기 위한 RFQ 생성 및 관리하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/b-rfq/[id]/layout.tsx b/app/[lng]/engineering/(engineering)/b-rfq/[id]/layout.tsx new file mode 100644 index 00000000..8dad7676 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/b-rfq/[id]/layout.tsx @@ -0,0 +1,87 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import { RfqDashboardView } from "@/db/schema" +import { findBRfqById } from "@/lib/b-rfq/service" + +export const metadata: Metadata = { + title: "견적 RFQ 상세", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqDashboardView | null = await findBRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "견적/입찰 문서관리", + href: `/${lng}/evcp/b-rfq/${id}`, + }, + { + title: "Initial RFQ 발송", + href: `/${lng}/evcp/b-rfq/${id}/initial`, + }, + { + title: "Final RFQ 발송", + href: `/${lng}/evcp/b-rfq/${id}/final`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}` + : "Loading RFQ..."} +

+ +

+ PR발행 전 RFQ를 생성하여 관리하는 화면입니다. +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/b-rfq/[id]/page.tsx b/app/[lng]/engineering/(engineering)/b-rfq/[id]/page.tsx new file mode 100644 index 00000000..26dc45fb --- /dev/null +++ b/app/[lng]/engineering/(engineering)/b-rfq/[id]/page.tsx @@ -0,0 +1,53 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations" +import { getRfqAttachments } from "@/lib/b-rfq/service" +import { RfqAttachmentsTable } from "@/lib/b-rfq/attachment/attachment-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqAttachmentsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getRfqAttachments({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ 견적 RFQ 문서관리 +

+

+ 설계로부터 받은 RFQ 문서와 구매 RFQ 문서를 관리하고 Vendor 회신을 점검/관리하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/b-rfq/page.tsx b/app/[lng]/engineering/(engineering)/b-rfq/page.tsx new file mode 100644 index 00000000..a66d7b58 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/b-rfq/page.tsx @@ -0,0 +1,79 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsRFQDashboardCache } from "@/lib/b-rfq/validations" +import { getRFQDashboard } from "@/lib/b-rfq/service" +import { RFQDashboardTable } from "@/lib/b-rfq/summary-table/summary-rfq-table" + +export const metadata: Metadata = { + title: "견적 RFQ", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsRFQDashboardCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getRFQDashboard({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + console.log(search, "견적") + + return ( + +
+
+
+

+ 견적 RFQ +

+
+
+
+ + {/* Items처럼 직접 테이블 렌더링 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/basic-contract-template/page.tsx b/app/[lng]/engineering/(engineering)/basic-contract-template/page.tsx new file mode 100644 index 00000000..adc57ed9 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/basic-contract-template/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContractTemplates } from "@/lib/basic-contract/service" +import { searchParamsTemplatesCache } from "@/lib/basic-contract/validations" +import { BasicContractTemplateTable } from "@/lib/basic-contract/template/basic-contract-template" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsTemplatesCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBasicContractTemplates({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 기본계약서 템플릿 관리 +

+

+ 기본계약서를 비롯하여 초기 서명이 필요한 문서를 등록하고 편집할 수 있습니다. 활성화된 템플릿이 서명 요청의 리스트에 나타나게 됩니다..{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/basic-contract/page.tsx b/app/[lng]/engineering/(engineering)/basic-contract/page.tsx new file mode 100644 index 00000000..a043e530 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/basic-contract/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContracts } from "@/lib/basic-contract/service" +import { searchParamsCache } from "@/lib/basic-contract/validations" +import { BasicContractsTable } from "@/lib/basic-contract/status/basic-contract-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBasicContracts({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 기본계약서 서명 현황 +

+

+ 기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명된 문서들을 다운로드할 수 있습니다. {" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/bid-projects/page.tsx b/app/[lng]/engineering/(engineering)/bid-projects/page.tsx new file mode 100644 index 00000000..2039e5b2 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/bid-projects/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBidProjectLists } from "@/lib/bidding-projects/service" +import { searchParamsBidProjectsCache } from "@/lib/bidding-projects/validation" +import { BidProjectsTable } from "@/lib/bidding-projects/table/projects-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsBidProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBidProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 견적 프로젝트 리스트 +

+

+ SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/bqcbe/page.tsx b/app/[lng]/engineering/(engineering)/bqcbe/page.tsx new file mode 100644 index 00000000..ae503feb --- /dev/null +++ b/app/[lng]/engineering/(engineering)/bqcbe/page.tsx @@ -0,0 +1,74 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllCBE } from "@/lib/rfqs/service" +import { searchParamsCBECache } from "@/lib/rfqs/validations" + +import { AllCbeTable } from "@/lib/cbe/table/cbe-table" + +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllCBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/bqtbe/page.tsx b/app/[lng]/engineering/(engineering)/bqtbe/page.tsx new file mode 100644 index 00000000..4989c235 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/bqtbe/page.tsx @@ -0,0 +1,72 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..956facd3 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/layout.tsx new file mode 100644 index 00000000..ba7c071c --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next" +import Link from "next/link" +import { ArrowLeft } from "lucide-react" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/page.tsx new file mode 100644 index 00000000..dd9df563 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..ec894e1c --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-rfq/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-rfq/page.tsx new file mode 100644 index 00000000..dc2a4a2b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-rfq/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE_BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-tech-sales-hull/page.tsx new file mode 100644 index 00000000..b1be29db --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-tech-sales-hull/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsHullCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesHullRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface HullRfqPageProps { + searchParams: Promise +} + +export default async function HullRfqPage(props: HullRfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 해양 HULL용 파라미터 파싱 + const search = searchParamsHullCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 해양 Hull RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesHullRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-해양 Hull RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-tech-sales-ship/page.tsx new file mode 100644 index 00000000..b7bf9d15 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-tech-sales-ship/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsShipCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesShipRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface RfqPageProps { + searchParams: Promise +} + +export default async function RfqPage(props: RfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 조선용 파라미터 파싱 + const search = searchParamsShipCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 조선 RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesShipRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-조선 RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary-tech-sales-top/page.tsx b/app/[lng]/engineering/(engineering)/budgetary-tech-sales-top/page.tsx new file mode 100644 index 00000000..f84a9794 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary-tech-sales-top/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsTopCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesTopRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface HullRfqPageProps { + searchParams: Promise +} + +export default async function HullRfqPage(props: HullRfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 해양 TOP용 파라미터 파싱 + const search = searchParamsTopCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 해양 TOP RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesTopRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-해양 TOP RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary/[id]/cbe/page.tsx b/app/[lng]/engineering/(engineering)/budgetary/[id]/cbe/page.tsx new file mode 100644 index 00000000..956facd3 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary/[id]/layout.tsx b/app/[lng]/engineering/(engineering)/budgetary/[id]/layout.tsx new file mode 100644 index 00000000..b0711c66 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary/[id]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next" +import Link from "next/link" +import { ArrowLeft } from "lucide-react" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+ +
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary/[id]/page.tsx b/app/[lng]/engineering/(engineering)/budgetary/[id]/page.tsx new file mode 100644 index 00000000..dd9df563 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary/[id]/tbe/page.tsx b/app/[lng]/engineering/(engineering)/budgetary/[id]/tbe/page.tsx new file mode 100644 index 00000000..ec894e1c --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/budgetary/page.tsx b/app/[lng]/engineering/(engineering)/budgetary/page.tsx new file mode 100644 index 00000000..04550353 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/budgetary/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/cbe-tech/page.tsx b/app/[lng]/engineering/(engineering)/cbe-tech/page.tsx new file mode 100644 index 00000000..4dadc58f --- /dev/null +++ b/app/[lng]/engineering/(engineering)/cbe-tech/page.tsx @@ -0,0 +1,67 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllCBE } from "@/lib/rfqs-tech/service" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { AllCbeTable } from "@/lib/cbe-tech/table/cbe-table" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + + // SearchParams 파싱 (Zod) + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllCBE({ + ...search, + filters: validFilters, + }) + ]) + + return ( + +
+
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/dashboard/page.tsx b/app/[lng]/engineering/(engineering)/dashboard/page.tsx new file mode 100644 index 00000000..1d61dc16 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/dashboard/page.tsx @@ -0,0 +1,17 @@ +// app/invalid-access/page.tsx + +export default function InvalidAccessPage() { + return ( +
+

부적절한 접근입니다

+

+ 협력업체(Vendor)가 EVCP 화면에 접속하거나
+ SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. +

+

+ 접근 권한이 없으므로, 다른 화면으로 이동해 주세요. +

+
+ ); + } + \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/email-template/[name]/page.tsx b/app/[lng]/engineering/(engineering)/email-template/[name]/page.tsx new file mode 100644 index 00000000..cccc10fc --- /dev/null +++ b/app/[lng]/engineering/(engineering)/email-template/[name]/page.tsx @@ -0,0 +1,26 @@ +import { getTemplateAction } from '@/lib/mail/service'; +import MailTemplateEditorClient from '@/components/mail/mail-template-editor-client'; + +interface EditMailTemplatePageProps { + params: { + name: string; + lng: string; + }; +} + +export default async function EditMailTemplatePage({ params }: EditMailTemplatePageProps) { + const { name: templateName } = await params; + + // 서버에서 초기 템플릿 데이터 가져오기 + const result = await getTemplateAction(templateName); + const initialTemplate = result.success ? result.data : null; + + return ( +
+ +
+ ); +} diff --git a/app/[lng]/engineering/(engineering)/email-template/page.tsx b/app/[lng]/engineering/(engineering)/email-template/page.tsx new file mode 100644 index 00000000..1ef3de6c --- /dev/null +++ b/app/[lng]/engineering/(engineering)/email-template/page.tsx @@ -0,0 +1,19 @@ +import { getTemplatesAction } from '@/lib/mail/service'; +import MailTemplatesClient from '@/components/mail/mail-templates-client'; + +export default async function MailTemplatesPage() { + // 서버에서 초기 데이터 가져오기 + const result = await getTemplatesAction(); + const initialData = result.success ? result.data : []; + + return ( +
+
+

메일 템플릿 관리

+

이메일 템플릿을 관리할 수 있습니다.

+
+ + +
+ ); +} diff --git a/app/[lng]/engineering/(engineering)/equip-class/page.tsx b/app/[lng]/engineering/(engineering)/equip-class/page.tsx new file mode 100644 index 00000000..cfa8f133 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/equip-class/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/equip-class/validation" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" +import { getTagClassists } from "@/lib/equip-class/service" +import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagClassists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 객체 클래스 목록 from S-EDP +

+

+ 객체 클래스 목록을 확인할 수 있습니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/esg-check-list/page.tsx b/app/[lng]/engineering/(engineering)/esg-check-list/page.tsx new file mode 100644 index 00000000..515751d5 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/esg-check-list/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getEsgEvaluations } from "@/lib/esg-check-list/service" +import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" +import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = getEsgEvaluationsSchema.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getEsgEvaluations({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ ESG 자가진단표 +

+

+ 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/evaluation-check-list/page.tsx b/app/[lng]/engineering/(engineering)/evaluation-check-list/page.tsx new file mode 100644 index 00000000..a660c492 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/evaluation-check-list/page.tsx @@ -0,0 +1,81 @@ +/* IMPORT */ +import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton'; +import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service'; +import { getValidFilters } from '@/lib/data-table'; +import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table'; +import { searchParamsCache } from '@/lib/evaluation-criteria/validations'; +import { Shell } from '@/components/shell'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Suspense } from 'react'; +import { type SearchParams } from '@/types/table'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +interface EvaluationCriteriaPageProps { + searchParams: Promise +} + +// ---------------------------------------------------------------------------------------------------- + +/* REGULAR EVALUATION CRITERIA PAGE */ +async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) { + const searchParams = await props.searchParams; + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + const promises = Promise.all([ + getRegEvalCriteria({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+
+

+ 협력업체 평가기준표 +

+

+ 협력업체 평가에 사용되는 평가기준표를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} + +// ---------------------------------------------------------------------------------------------------- + +/* EXPORT */ +export default EvaluationCriteriaPage; \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/evaluation-target-list/page.tsx b/app/[lng]/engineering/(engineering)/evaluation-target-list/page.tsx new file mode 100644 index 00000000..088ae75b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/evaluation-target-list/page.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" + +import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" +import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" +import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" + +export const metadata: Metadata = { + title: "협력업체 평가 대상 확정", + description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", +} + +interface EvaluationTargetsPageProps { + searchParams: Promise +} + + + +export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsEvaluationTargetsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 (필터에서 가져오거나 기본값 사용) + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getEvaluationTargets({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + + {/* 간소화된 헤더 */} +
+
+
+

+ 협력업체 평가 대상 확정 +

+ + {currentEvaluationYear}년도 + + +
+
+
+ + {/* 메인 테이블 (통계는 테이블 내부로 이동) */} + + } + > + {currentEvaluationYear && + +} + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/evaluation/page.tsx b/app/[lng]/engineering/(engineering)/evaluation/page.tsx new file mode 100644 index 00000000..ead61077 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/evaluation/page.tsx @@ -0,0 +1,181 @@ +// ================================================================ +// 4. PERIODIC EVALUATIONS PAGE +// ================================================================ + +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" +import { getPeriodicEvaluations } from "@/lib/evaluation/service" +import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" + +export const metadata: Metadata = { + title: "협력업체 정기평가", + description: "협력업체 정기평가 진행 현황을 관리합니다.", +} + +interface PeriodicEvaluationsPageProps { + searchParams: Promise +} + +// 프로세스 안내 팝오버 컴포넌트 +function ProcessGuidePopover() { + return ( + + + + + +
+
+

정기평가 프로세스

+

+ 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. +

+
+
+
+
+ 1 +
+
+

평가 대상 확정

+

평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.

+
+
+
+
+ 2 +
+
+

업체 자료 제출

+

각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.

+
+
+
+
+ 3 +
+
+

평가자 검토

+

지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.

+
+
+
+
+ 4 +
+
+

최종 확정

+

모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.

+
+
+
+
+
+
+ ) +} + +// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 +function getDefaultEvaluationYear() { + return new Date().getFullYear() +} + + + +export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsEvaluationsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters || []) + + // 기본 필터 처리 + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPeriodicEvaluations({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + + {/* 헤더 */} +
+
+
+

+ 협력업체 정기평가 +

+ + {currentEvaluationYear}년도 + +
+
+
+ + {/* 메인 테이블 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/manage/actions.ts b/app/[lng]/engineering/(engineering)/faq/manage/actions.ts new file mode 100644 index 00000000..bc443a8a --- /dev/null +++ b/app/[lng]/engineering/(engineering)/faq/manage/actions.ts @@ -0,0 +1,48 @@ +'use server'; + +import { promises as fs } from 'fs'; +import path from 'path'; +import { FaqCategory } from '@/components/faq/FaqCard'; +import { fallbackLng } from '@/i18n/settings'; + +const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); + +export async function updateFaqData(lng: string, newData: FaqCategory[]) { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + const updatedData = { + ...allData, + [lng]: newData + }; + + const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; + await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); + + return { success: true }; + } catch (error) { + console.error('FAQ 데이터 업데이트 중 오류 발생:', error); + return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; + } +} + +export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + return { data: allData[lng] || allData[fallbackLng] || [] }; + } catch (error) { + console.error('FAQ 데이터 읽기 중 오류 발생:', error); + return { data: [] }; + } +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/manage/page.tsx b/app/[lng]/engineering/(engineering)/faq/manage/page.tsx new file mode 100644 index 00000000..011bbfa4 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/faq/manage/page.tsx @@ -0,0 +1,38 @@ +import { FaqManager } from '@/components/faq/FaqManager'; +import { getFaqData, updateFaqData } from './actions'; +import { revalidatePath } from 'next/cache'; +import { FaqCategory } from '@/components/faq/FaqCard'; + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqManagePage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const { data } = await getFaqData(lng); + + async function handleSave(newData: FaqCategory[]) { + 'use server'; + await updateFaqData(lng, newData); + revalidatePath(`/${lng}/evcp/faq`); + } + + return ( +
+
+
+
+

FAQ Management

+

+ Manage FAQ categories and items for {lng.toUpperCase()} language. +

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/page.tsx b/app/[lng]/engineering/(engineering)/faq/page.tsx new file mode 100644 index 00000000..9b62b7e4 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/faq/page.tsx @@ -0,0 +1,62 @@ +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { faqCategories } from "@/config/faqDataConfig" +import { FaqCard } from "@/components/faq/FaqCard" +import { Button } from "@/components/ui/button" +import { Settings } from "lucide-react" +import Link from "next/link" +import { fallbackLng } from "@/i18n/settings" + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqPage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; + + return ( +
+
+
+
+
+

Frequently Asked Questions

+

+ Find answers to common questions about using the EVCP system. +

+
+ + + +
+ + + + + {localizedFaqCategories.map((category) => ( + + {category.label} + + ))} + + + {localizedFaqCategories.map((category) => ( + + {category.items.map((item, index) => ( + + ))} + + ))} + +
+
+
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/form-list/page.tsx b/app/[lng]/engineering/(engineering)/form-list/page.tsx new file mode 100644 index 00000000..a6cf7d9e --- /dev/null +++ b/app/[lng]/engineering/(engineering)/form-list/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/form-list/validation" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getFormLists } from "@/lib/form-list/service" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getFormLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 레지스터 목록 from S-EDP +

+

+ 협력업체 데이터 입력을 위한 레지스터 목록 리스트입니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/incoterms/page.tsx b/app/[lng]/engineering/(engineering)/incoterms/page.tsx new file mode 100644 index 00000000..57a19009 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/incoterms/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { SearchParamsCache } from "@/lib/incoterms/validations"; +import { getIncoterms } from "@/lib/incoterms/service"; +import { IncotermsTable } from "@/lib/incoterms/table/incoterms-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = SearchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getIncoterms({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

인코텀즈 관리

+

+ 인코텀즈(Incoterms)를 등록, 수정, 삭제할 수 있습니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/items-tech/layout.tsx b/app/[lng]/engineering/(engineering)/items-tech/layout.tsx new file mode 100644 index 00000000..d375059b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/items-tech/layout.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { ItemTechContainer } from "@/components/items-tech/item-tech-container" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default function ItemsShipLayout({ + children, +}: { + children: React.ReactNode +}) { + // 아이템 타입 정의 + const itemTypes = [ + { id: "ship", name: "조선 아이템" }, + { id: "top", name: "해양 TOP" }, + { id: "hull", name: "해양 HULL" }, + ] + + return ( + + + } + > + + {children} + + + + ) +} diff --git a/app/[lng]/engineering/(engineering)/items-tech/page.tsx b/app/[lng]/engineering/(engineering)/items-tech/page.tsx new file mode 100644 index 00000000..55ac9c63 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/items-tech/page.tsx @@ -0,0 +1,67 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations" +import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service" +import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table" +import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table" + +// 대소문자 문제 해결 - 실제 파일명에 맞게 import +import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage({ searchParams }: IndexPageProps) { + const params = await searchParams + const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params) + const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params) + const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params) + const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || []) + const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || []) + const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || []) + + + // URL에서 아이템 타입 가져오기 + const itemType = params.type || "ship" + + return ( +
+ {itemType === "ship" && ( + result)} + /> + )} + + {itemType === "top" && ( + result)} + /> + )} + + {itemType === "hull" && ( + result)} + /> + )} +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/items/page.tsx b/app/[lng]/engineering/(engineering)/items/page.tsx new file mode 100644 index 00000000..0c44bf0a --- /dev/null +++ b/app/[lng]/engineering/(engineering)/items/page.tsx @@ -0,0 +1,68 @@ +// app/items/page.tsx (업데이트) +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/items/validations" +import { getItems } from "@/lib/items/service" +import { ItemsTable } from "@/lib/items/table/items-table" +import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // pageSize 기반으로 모드 자동 결정 + const isInfiniteMode = search.perPage >= 1_000_000 + + // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 + // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 + const promises = isInfiniteMode + ? undefined + : Promise.all([ + getItems(search), // searchParamsCache의 결과를 그대로 사용 + ]) + + return ( + +
+
+
+

+ 패키지 정보 +

+

+ S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. +

+
+
+ +
+ + }> + {/* DateRangePicker 등 추가 컴포넌트 */} + + + + } + > + {/* 통합된 ItemsTable 컴포넌트 사용 */} + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/layout.tsx b/app/[lng]/engineering/(engineering)/layout.tsx new file mode 100644 index 00000000..82b53307 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/layout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Header } from '@/components/layout/Header'; +import { SiteFooter } from '@/components/layout/Footer'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( +
+ {/*
*/} +
+
+
+ {children} +
+
+ +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/menu-list/page.tsx b/app/[lng]/engineering/(engineering)/menu-list/page.tsx new file mode 100644 index 00000000..84138320 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/menu-list/page.tsx @@ -0,0 +1,70 @@ +// app/evcp/menu-list/page.tsx + +import { Suspense } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { RefreshCw, Settings } from "lucide-react"; +import { getActiveUsers, getMenuAssignments } from "@/lib/menu-list/servcie"; +import { InitializeButton } from "@/lib/menu-list/table/initialize-button"; +import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; +import { Shell } from "@/components/shell" +import * as React from "react" + +export default async function MenuListPage() { + // 초기 데이터 로드 + const [menusResult, usersResult] = await Promise.all([ + getMenuAssignments(), + getActiveUsers() + ]); + + return ( + +
+
+
+

+ 메뉴 관리 +

+

+ 각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. +

+
+
+ +
+ + + + + + + + 메뉴 리스트 + + + 시스템의 모든 메뉴와 담당자 정보를 확인할 수 있습니다. + {menusResult.data?.length > 0 && ( + + 총 {menusResult.data.length}개의 메뉴 + + )} + + + + 로딩 중...
}> + + + + + + + + ); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/payment-conditions/page.tsx b/app/[lng]/engineering/(engineering)/payment-conditions/page.tsx new file mode 100644 index 00000000..b9aedfbb --- /dev/null +++ b/app/[lng]/engineering/(engineering)/payment-conditions/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { SearchParamsCache } from "@/lib/payment-terms/validations"; +import { getPaymentTerms } from "@/lib/payment-terms/service"; +import { PaymentTermsTable } from "@/lib/payment-terms/table/payment-terms-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = SearchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getPaymentTerms({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

결제 조건 관리

+

+ 결제 조건(Payment Terms)을 등록, 수정, 삭제할 수 있습니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/po-rfq/page.tsx b/app/[lng]/engineering/(engineering)/po-rfq/page.tsx new file mode 100644 index 00000000..bdeae25e --- /dev/null +++ b/app/[lng]/engineering/(engineering)/po-rfq/page.tsx @@ -0,0 +1,61 @@ +import { getPORfqs } from "@/lib/procurement-rfqs/services" +import { searchParamsCache } from "@/lib/procurement-rfqs/validations" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/procurement-rfqs/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface RfqPageProps { + searchParams: Promise +} + +export default async function RfqPage(props: RfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 파라미터 파싱 + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // RFQ 서버는 기본필터와 고급필터를 분리해서 받으므로 그대로 전달 + const promises = Promise.all([ + getPORfqs({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 발주용 견적 +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/po/page.tsx b/app/[lng]/engineering/(engineering)/po/page.tsx new file mode 100644 index 00000000..7868e231 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/po/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getPOs } from "@/lib/po/service" +import { searchParamsCache } from "@/lib/po/validations" +import { PoListsTable } from "@/lib/po/table/po-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getPOs({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ PO 확인 및 전자서명 +

+

+ 기간계 시스템으로부터 PO를 확인하고 협력업체에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/poa/page.tsx b/app/[lng]/engineering/(engineering)/poa/page.tsx new file mode 100644 index 00000000..dec5e05b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/poa/page.tsx @@ -0,0 +1,61 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getChangeOrders } from "@/lib/poa/service" +import { searchParamsCache } from "@/lib/poa/validations" +import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getChangeOrders({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +
+
+
+

+ 변경 PO 확인 및 전자서명 +

+

+ 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. +

+
+
+
+ + }> + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/pq-criteria/[id]/page.tsx b/app/[lng]/engineering/(engineering)/pq-criteria/[id]/page.tsx new file mode 100644 index 00000000..55b1e9df --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq-criteria/[id]/page.tsx @@ -0,0 +1,81 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" +import { notFound } from "next/navigation" + +interface ProjectPageProps { + params: { id: string } + searchParams: Promise +} + +export default async function ProjectPage(props: ProjectPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const projectId = parseInt(id, 10) + + // 유효하지 않은 projectId 확인 + if (isNaN(projectId)) { + notFound() + } + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + const validFilters = getValidFilters(search.filters) + + // 프로젝트별 PQ 데이터 가져오기 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, projectId, false) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/pq-criteria/page.tsx b/app/[lng]/engineering/(engineering)/pq-criteria/page.tsx new file mode 100644 index 00000000..7785b541 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq-criteria/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + + const validFilters = getValidFilters(search.filters) + + // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, null, true) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/pq/[vendorId]/page.tsx b/app/[lng]/engineering/(engineering)/pq/[vendorId]/page.tsx new file mode 100644 index 00000000..76bcfe59 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq/[vendorId]/page.tsx @@ -0,0 +1,108 @@ +import * as React from "react" +import { Shell } from "@/components/shell" +import { type SearchParams } from "@/types/table" +import { getPQDataByVendorId, getVendorPQsList, loadGeneralPQData, loadProjectPQAction, loadProjectPQData } from "@/lib/pq/service" +import { Vendor } from "@/db/schema/vendors" +import { findVendorById } from "@/lib/vendors/service" +import VendorPQAdminReview from "@/components/pq/pq-review-detail" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" + +interface IndexPageProps { + params: { + vendorId: string + } + searchParams: Promise +} + +export default async function PQReviewPage(props: IndexPageProps) { + const resolvedParams = await props.params + const vendorId = Number(resolvedParams.vendorId) + + // Fetch the vendor data + const vendor: Vendor | null = await findVendorById(vendorId) + if (!vendor) return
Vendor not found
+ + // Get list of all PQs (general + project-specific) for this vendor + const pqsList = await getVendorPQsList(vendorId) + + // Determine default active PQ to display + // If query param projectId exists, use that, otherwise use general PQ if available + const searchParams = await props.searchParams + const activeProjectId = searchParams.projectId ? Number(searchParams.projectId) : undefined + + // If no projectId query param, default to general PQ or first project PQ + const defaultTabId = activeProjectId ? + `project-${activeProjectId}` : + (pqsList.hasGeneralPq ? 'general' : `project-${pqsList.projectPQs[0]?.projectId}`) + + // Fetch PQ data for the active tab + let pqData; + if (activeProjectId) { + // Get project-specific PQ data + pqData = await getPQDataByVendorId(vendorId, activeProjectId) + } else { + // Get general PQ data + pqData = await getPQDataByVendorId(vendorId) + } + + return ( + + {pqsList.hasGeneralPq || pqsList.projectPQs.length > 0 ? ( + +
+

+ {vendor.vendorName} PQ Review +

+ + + {pqsList.hasGeneralPq && ( + + General PQ Standard + + )} + + {pqsList.projectPQs.map((project) => ( + + {project.projectName} {project.status} + + ))} + +
+ + {/* Tab content for General PQ */} + {pqsList.hasGeneralPq && ( + + + + )} + + {/* Tab content for each Project PQ */} + {pqsList.projectPQs.map((project) => ( + + + + ))} +
+ ) : ( +
+

No PQ submissions found for this vendor

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/pq/page.tsx b/app/[lng]/engineering/(engineering)/pq/page.tsx new file mode 100644 index 00000000..46b22b12 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq/page.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorsInPQ } from "@/lib/pq/service" +import { searchParamsCache } from "@/lib/vendors/validations" +import { VendorsPQReviewTable } from "@/lib/pq/pq-review-table/vendors-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInPQ({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Pre-Qualification Review +

+

+ 벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. + +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx new file mode 100644 index 00000000..28ce3128 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -0,0 +1,215 @@ +import * as React from "react" +import { Metadata } from "next" +import Link from "next/link" +import { notFound } from "next/navigation" +import { ArrowLeft } from "lucide-react" +import { Shell } from "@/components/shell" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Separator } from "@/components/ui/separator" +import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" +import { unstable_noStore as noStore } from 'next/cache' +import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" + +export const metadata: Metadata = { + title: "PQ 검토", + description: "협력업체의 Pre-Qualification 답변을 검토합니다.", +} + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic" + +interface PQReviewPageProps { + params: Promise<{ + vendorId: string; + submissionId: string; + }> +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + // 캐시 비활성화 + noStore() + + const params = await props.params + const vendorId = parseInt(params.vendorId, 10) + const submissionId = parseInt(params.submissionId, 10) + + try { + // PQ Submission 정보 조회 + const pqSubmission = await getPQById(submissionId, vendorId) + + // PQ 데이터 조회 (질문과 답변) + const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) + + // 프로젝트 정보 (프로젝트 PQ인 경우) + const projectInfo = pqSubmission.projectId ? { + id: pqSubmission.projectId, + projectCode: pqSubmission.projectCode || '', + projectName: pqSubmission.projectName || '', + status: pqSubmission.status, + submittedAt: pqSubmission.submittedAt, + } : null + + // PQ 유형 및 상태 레이블 + const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : "프로젝트 PQ" + const statusLabel = getStatusLabel(pqSubmission.status) + const statusVariant = getStatusVariant(pqSubmission.status) + + // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) + const canReview = pqSubmission.status === "SUBMITTED" + + return ( + +
+
+ +
+

+ {pqSubmission.vendorName} - {typeLabel} +

+
+ {statusLabel} + {projectInfo && ( + + {projectInfo.projectName} ({projectInfo.projectCode}) + + )} +
+
+
+
+ + {/* 상태별 알림 */} + {pqSubmission.status === "SUBMITTED" && ( + + 제출 완료 + + 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. + + + )} + + {pqSubmission.status === "APPROVED" && ( + + 승인됨 + + {formatDate(pqSubmission.approvedAt)}에 승인되었습니다. + + + )} + + {pqSubmission.status === "REJECTED" && ( + + 거부됨 + + {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다. + {pqSubmission.rejectReason && ( +
+ 사유: {pqSubmission.rejectReason} +
+ )} +
+
+ )} + + + + {/* PQ 검토 컴포넌트 */} + + + PQ 검토 + 협력업체 정보 + + + + + + + +
+

협력업체 정보

+
+
+

업체명

+

{pqSubmission.vendorName}

+
+
+

업체 코드

+

{pqSubmission.vendorCode}

+
+
+

상태

+

{pqSubmission.vendorStatus}

+
+ {/* 필요시 추가 정보 표시 */} +
+
+
+
+
+ ) + } catch (error) { + console.error("Error loading PQ:", error) + notFound() + } +} + +// 상태 레이블 함수 +function getStatusLabel(status: string): string { + switch (status) { + case "REQUESTED": + return "요청됨"; + case "IN_PROGRESS": + return "진행 중"; + case "SUBMITTED": + return "제출됨"; + case "APPROVED": + return "승인됨"; + case "REJECTED": + return "거부됨"; + default: + return status; + } +} + +// 상태별 Badge 스타일 +function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { + switch (status) { + case "REQUESTED": + return "outline"; + case "IN_PROGRESS": + return "secondary"; + case "SUBMITTED": + return "default"; + case "APPROVED": + return "success"; + case "REJECTED": + return "destructive"; + default: + return "outline"; + } +} + +// 날짜 형식화 함수 +function formatDate(date: Date | null) { + if (!date) return "날짜 없음"; + return new Date(date).toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + }); +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/pq_new/page.tsx b/app/[lng]/engineering/(engineering)/pq_new/page.tsx new file mode 100644 index 00000000..6598349b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/pq_new/page.tsx @@ -0,0 +1,96 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsPQReviewCache } from "@/lib/pq/validations" +import { getPQSubmissions } from "@/lib/pq/service" +import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" + +export const metadata: Metadata = { + title: "PQ 검토/실사 의뢰", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsPQReviewCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 디버깅 로그 추가 + console.log("=== PQ Page Debug ==="); + console.log("Raw searchParams:", searchParams); + console.log("Raw basicFilters param:", searchParams.basicFilters); + console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); + console.log("Parsed search:", search); + console.log("search.filters:", search.filters); + console.log("search.basicFilters:", search.basicFilters); + console.log("search.pqBasicFilters:", search.pqBasicFilters); + console.log("validFilters:", validFilters); + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { + // 하위 호환성을 위해 기존 이름도 지원 + basicFilters = search.pqBasicFilters + console.log("Using search.pqBasicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + console.log("Final allFilters:", allFilters); + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; + console.log("Final joinOperator:", joinOperator); + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPQSubmissions({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + +
+
+
+

+ PQ 검토/실사 의뢰 +

+
+
+
+ + {/* Items처럼 직접 테이블 렌더링 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/project-gtc/page.tsx b/app/[lng]/engineering/(engineering)/project-gtc/page.tsx new file mode 100644 index 00000000..8e12a489 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/project-gtc/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getProjectGtcList } from "@/lib/project-gtc/service" +import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" +import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = projectGtcSearchParamsSchema.parse(searchParams) + + const promises = Promise.all([ + getProjectGtcList({ + page: search.page, + perPage: search.perPage, + search: search.search, + sort: search.sort, + }), + ]) + + return ( + +
+
+
+

+ Project GTC +

+

+ 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. + 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. +

+
+
+
+ + }> + {/* 추가 기능이 필요하면 여기에 추가 */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/project-vendors/page.tsx b/app/[lng]/engineering/(engineering)/project-vendors/page.tsx new file mode 100644 index 00000000..dcc66071 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/project-vendors/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" +import { getProjecTAVL } from "@/lib/project-avl/service" +import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchProjectAVLParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjecTAVL({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 프로젝트 AVL 리스트 +

+

+ 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/projects/page.tsx b/app/[lng]/engineering/(engineering)/projects/page.tsx new file mode 100644 index 00000000..0320f259 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/projects/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getProjectLists } from "@/lib/projects/service" +import { ProjectsTable } from "@/lib/projects/table/projects-table" +import { searchParamsProjectsCache } from "@/lib/projects/validation" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Project List from S-EDP +

+

+ S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/report/page.tsx b/app/[lng]/engineering/(engineering)/report/page.tsx new file mode 100644 index 00000000..3efaa7c3 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/report/page.tsx @@ -0,0 +1,47 @@ +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + + +export default async function IndexPage() { + + + return ( + +
+
+

+ Dashboard +

+

+ 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. +

+
+
+ + }> + {/* */} + + + + } + > + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq-tech/[id]/cbe/page.tsx b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/cbe/page.tsx new file mode 100644 index 00000000..84379caf --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/cbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { getCBE } from "@/lib/rfqs-tech/service" +import { CbeTable } from "@/lib/rfqs-tech/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq-tech/[id]/layout.tsx b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/layout.tsx new file mode 100644 index 00000000..0bb62fe0 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/layout.tsx @@ -0,0 +1,89 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs-tech/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq-tech/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq-tech/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq-tech/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq-tech/[id]/page.tsx b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/page.tsx new file mode 100644 index 00000000..007270a1 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs-tech/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs-tech/validations" +import { MatchedVendorsTable } from "@/lib/rfqs-tech/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq-tech/[id]/tbe/page.tsx b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/tbe/page.tsx new file mode 100644 index 00000000..4b226cdc --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq-tech/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { TbeTable } from "@/lib/rfqs-tech/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq-tech/page.tsx b/app/[lng]/engineering/(engineering)/rfq-tech/page.tsx new file mode 100644 index 00000000..f35b3632 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq-tech/page.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs-tech/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs-tech/service" +import { RfqsTable } from "@/lib/rfqs-tech/table/rfqs-table" +import { getAllOffshoreItems } from "@/lib/items-tech/service" + +interface RfqPageProps { + searchParams: Promise; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + title = "기술영업 해양 RFQ", + description = "기술영업 해양 RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + }), + getRfqStatusCounts(), + getAllOffshoreItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq/[id]/cbe/page.tsx b/app/[lng]/engineering/(engineering)/rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..fb288a98 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq/[id]/cbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCBECache } from "@/lib/rfqs/validations" +import { getCBE } from "@/lib/rfqs/service" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq/[id]/layout.tsx b/app/[lng]/engineering/(engineering)/rfq/[id]/layout.tsx new file mode 100644 index 00000000..9a03efa4 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq/[id]/layout.tsx @@ -0,0 +1,89 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq/[id]/page.tsx b/app/[lng]/engineering/(engineering)/rfq/[id]/page.tsx new file mode 100644 index 00000000..1a9f4b18 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq/[id]/tbe/page.tsx b/app/[lng]/engineering/(engineering)/rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..76eea302 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/rfq/page.tsx b/app/[lng]/engineering/(engineering)/rfq/page.tsx new file mode 100644 index 00000000..3417b0bf --- /dev/null +++ b/app/[lng]/engineering/(engineering)/rfq/page.tsx @@ -0,0 +1,80 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE, + title = "RFQ", + description = "RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/settings/layout.tsx b/app/[lng]/engineering/(engineering)/settings/layout.tsx new file mode 100644 index 00000000..6f373567 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/settings/layout.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "Settings", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Account", + href: `/${lng}/evcp/settings`, + }, + { + title: "Preferences", + href: `/${lng}/evcp/settings/preferences`, + } + + + ] + + + return ( + <> +
+
+
+
+

Settings

+

+ Manage your account settings and preferences. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/engineering/(engineering)/settings/page.tsx b/app/[lng]/engineering/(engineering)/settings/page.tsx new file mode 100644 index 00000000..a6eaac90 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/settings/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/components/settings/account-form" + +export default function SettingsAccountPage() { + return ( +
+
+

Account

+

+ Update your account settings. Set your preferred language and + timezone. +

+
+ + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/settings/preferences/page.tsx b/app/[lng]/engineering/(engineering)/settings/preferences/page.tsx new file mode 100644 index 00000000..e2a88021 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/settings/preferences/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/components/settings/appearance-form" + +export default function SettingsAppearancePage() { + return ( +
+
+

Preference

+

+ Customize the preference of the app. +

+
+ + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/system/admin-users/page.tsx b/app/[lng]/engineering/(engineering)/system/admin-users/page.tsx new file mode 100644 index 00000000..11a9e9fb --- /dev/null +++ b/app/[lng]/engineering/(engineering)/system/admin-users/page.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" +import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsers({ + ...search, + filters: validFilters, + }), + getUserCountGroupByCompany(), + getUserCountGroupByRole(), + getAllCompanies(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

Vendor Admin User Management

+

+ 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/engineering/(engineering)/system/layout.tsx b/app/[lng]/engineering/(engineering)/system/layout.tsx new file mode 100644 index 00000000..7e8f69d0 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/system/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "System Setting", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "삼성중공업 사용자", + href: `/${lng}/evcp/system`, + }, + { + title: "Roles", + href: `/${lng}/evcp/system/roles`, + }, + { + title: "권한 통제", + href: `/${lng}/evcp/system/permissions`, + }, + { + title: "협력업체 사용자", + href: `/${lng}/evcp/system/admin-users`, + }, + + { + title: "비밀번호 정책", + href: `/${lng}/evcp/system/password-policy`, + }, + + ] + + + return ( + <> +
+
+
+
+

시스템 설정

+

+ 사용자, 롤, 접근 권한을 관리하세요. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/engineering/(engineering)/system/page.tsx b/app/[lng]/engineering/(engineering)/system/page.tsx new file mode 100644 index 00000000..fe0a262c --- /dev/null +++ b/app/[lng]/engineering/(engineering)/system/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import * as React from "react" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllRoles, getUsersEVCP } from "@/lib/users/service" +import { getUserCountGroupByRole } from "@/lib/admin-users/service" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { UserTable } from "@/lib/users/table/users-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function SystemUserPage(props: IndexPageProps) { + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsersEVCP({ + ...search, + filters: validFilters, + }), + getUserCountGroupByRole(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

SHI Users

+

+ 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. +

+
+ + +
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/system/password-policy/page.tsx b/app/[lng]/engineering/(engineering)/system/password-policy/page.tsx new file mode 100644 index 00000000..0f14fefe --- /dev/null +++ b/app/[lng]/engineering/(engineering)/system/password-policy/page.tsx @@ -0,0 +1,63 @@ +// app/admin/password-policy/page.tsx + +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { AlertTriangle } from "lucide-react" +import SecuritySettingsTable from "@/components/system/passwordPolicy" +import { getSecuritySettings } from "@/lib/password-policy/service" + + +export default async function PasswordPolicyPage() { + try { + // 보안 설정 데이터 로드 + const securitySettings = await getSecuritySettings() + + return ( + + } + > +
+
+

협력업체 사용자 비밀번호 정책 설정

+

+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. +

+
+ + +
+
+ ) + } catch (error) { + console.error('Failed to load security settings:', error) + + return ( +
+
+

협력업체 사용자 비밀번호 정책 설정

+

+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. +

+
+ + + + + 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. + + +
+ ) + } +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/system/permissions/page.tsx b/app/[lng]/engineering/(engineering)/system/permissions/page.tsx new file mode 100644 index 00000000..6aa2b693 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/system/permissions/page.tsx @@ -0,0 +1,17 @@ +import PermissionsTree from "@/components/system/permissionsTree" +import { Separator } from "@/components/ui/separator" + +export default function PermissionsPage() { + return ( +
+
+

Permissions

+

+ Set permissions to the menu by Role +

+
+ + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/system/roles/page.tsx b/app/[lng]/engineering/(engineering)/system/roles/page.tsx new file mode 100644 index 00000000..fe074600 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/system/roles/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/roles/validations" +import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" +import { RolesTable } from "@/lib/roles/table/roles-table" +import { getRolesWithCount } from "@/lib/roles/services" +import { getUsersAll } from "@/lib/users/service" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const search2 = searchParamsCache2.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRolesWithCount({ + ...search, + filters: validFilters, + }), + + + ]) + + + const promises2 = Promise.all([ + getUsersAll({ + ...search2, + filters: validFilters, + }, "evcp"), + ]) + + + return ( + + } + > +
+
+

Role Management

+

+ 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx b/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx new file mode 100644 index 00000000..44695259 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/tag-numbering/validation" +import { getTagNumbering } from "@/lib/tag-numbering/service" +import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagNumbering({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 태그 타입 목록 from S-EDP +

+

+ 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/tasks/page.tsx b/app/[lng]/engineering/(engineering)/tasks/page.tsx new file mode 100644 index 00000000..91b946fb --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tasks/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Shell } from "@/components/shell" + +import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" +import { TasksTable } from "@/lib/tasks/table/tasks-table" +import { + getTaskPriorityCounts, + getTasks, + getTaskStatusCounts, +} from "@/lib/tasks/service" +import { searchParamsCache } from "@/lib/tasks/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTasks({ + ...search, + filters: validFilters, + }), + getTaskStatusCounts(), + getTaskPriorityCounts(), + ]) + + return ( + + }> + + + + } + > + + + + ) +} diff --git a/app/[lng]/engineering/(engineering)/tbe-tech/page.tsx b/app/[lng]/engineering/(engineering)/tbe-tech/page.tsx new file mode 100644 index 00000000..17b01ce2 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tbe-tech/page.tsx @@ -0,0 +1,67 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { AllTbeTable } from "@/lib/tbe-tech/table/tbe-table" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + }) + ]) + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tbe/page.tsx b/app/[lng]/engineering/(engineering)/tbe/page.tsx new file mode 100644 index 00000000..1a7fdf86 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tbe/page.tsx @@ -0,0 +1,113 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +// 타입별 페이지 설명 구성 (Budgetary 제외) +const typeConfig: Record = { + "purchase": { + title: "Purchase RFQ Technical Bid Evaluation", + description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE + }, + "purchase-budgetary": { + title: "Purchase Budgetary RFQ Technical Bid Evaluation", + description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE_BUDGETARY + } +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + // 기본값으로 'purchase' 사용 + const typeParam = searchParams?.type as string || 'purchase' + + // 유효한 타입인지 확인하고 기본값 설정 + const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' + const rfqType = typeConfig[validType].rfqType + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + }) + ]) + + // 페이지 경로 생성 함수 - 단순화 + const getTabUrl = (type: string) => { + return `/${lng}/evcp/tbe?type=${type}`; + } + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + {/* 타입 선택 탭 (Budgetary 제외) */} + + + + Purchase + + + Purchase Budgetary + + + +
+

+ {typeConfig[validType].description} +

+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx b/app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx new file mode 100644 index 00000000..d942c5c5 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx @@ -0,0 +1,85 @@ +import * as React from "react" +import { redirect } from "next/navigation" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { SearchParams } from "@/types/table" +import { searchParamsCache } from "@/lib/tech-project-avl/validations" +import { Skeleton } from "@/components/ui/skeleton" +import { Shell } from "@/components/shell" +import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table" +import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Ellipsis } from "lucide-react" + +export interface PageProps { + params: Promise<{ lng: string }> + searchParams: Promise +} + +export default async function AcceptedQuotationsPage({ + params, + searchParams, +}: PageProps) { + const { lng } = await params + + const session = await getServerSession(authOptions) + if (!session) { + redirect(`/${lng}/auth/signin`) + } + + const search = await searchParams + const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search) + const validFilters = getValidFilters(filters ?? []) + + const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({ + page, + perPage: perPage ?? 10, + sort, + search: searchText, + filters: validFilters, + }) + + return ( + +
+
+
+

+ 승인된 견적서(해양TOP,HULL) +

+

+ 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다. +

+
+
+
+ + }> + {/* Date range picker can be added here if needed */} + + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/tech-vendor-candidates/page.tsx b/app/[lng]/engineering/(engineering)/tech-vendor-candidates/page.tsx new file mode 100644 index 00000000..3923863a --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-vendor-candidates/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/tech-vendor-candidates/service" +import { searchParamsTechCandidateCache } from "@/lib/tech-vendor-candidates/validations" +import { VendorCandidateTable as TechVendorCandidateTable } from "@/lib/tech-vendor-candidates/table/candidates-table" +import { DateRangePicker } from "@/components/date-range-picker" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsTechCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. +

+
+
+
+ + {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} +
+ {/* 수집일 기간 설정: */} + }> + + +
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/items/page.tsx b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..69c36576 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/items/page.tsx @@ -0,0 +1,48 @@ +// import { Separator } from "@/components/ui/separator" +// import { getTechVendorById, getVendorItemsByType } from "@/lib/tech-vendors/service" +// import { type SearchParams } from "@/types/table" +// import { TechVendorItemsTable } from "@/lib/tech-vendors/items-table/item-table" + +// interface IndexPageProps { +// // Next.js 13 App Router에서 기본으로 주어지는 객체들 +// params: { +// lng: string +// id: string +// } +// searchParams: Promise +// } + +// export default async function TechVendorItemsPage(props: IndexPageProps) { +// const resolvedParams = await props.params +// const id = resolvedParams.id + +// const idAsNumber = Number(id) + +// // 벤더 정보 가져오기 (벤더 타입 필요) +// const vendorInfo = await getTechVendorById(idAsNumber) +// const vendorType = vendorInfo.data?.techVendorType || "조선" + +// const promises = getVendorItemsByType(idAsNumber, vendorType) + +// // 4) 렌더링 +// return ( +//
+//
+//

+// 공급품목 +//

+//

+// 기술영업 벤더의 공급 가능한 품목을 확인하세요. +//

+//
+// +//
+// +//
+//
+// ) +// } \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/layout.tsx b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..7c389720 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/layout.tsx @@ -0,0 +1,82 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findTechVendorById } from "@/lib/tech-vendors/service" +import { TechVendor } from "@/db/schema/techVendors" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +export const metadata: Metadata = { + title: "Tech Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const vendor: TechVendor | null = await findTechVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "연락처", + href: `/${lng}/evcp/tech-vendors/${id}/info`, + }, + // { + // title: "자재 리스트", + // href: `/${lng}/evcp/tech-vendors/${id}/info/items`, + // }, + // { + // title: "견적 히스토리", + // href: `/${lng}/evcp/tech-vendors/${id}/info/rfq-history`, + // }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

기술영업 벤더 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/page.tsx b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/page.tsx new file mode 100644 index 00000000..a57d6df7 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getTechVendorContacts } from "@/lib/tech-vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/tech-vendors/validations" +import { TechVendorContactsTable } from "@/lib/tech-vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getTechVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..4ed2b39f --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +// import { Separator } from "@/components/ui/separator" +// import { getRfqHistory } from "@/lib/vendors/service" +// import { type SearchParams } from "@/types/table" +// import { getValidFilters } from "@/lib/data-table" +// import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +// import { TechVendorRfqHistoryTable } from "@/lib/tech-vendors/rfq-history-table/rfq-history-table" + +// interface IndexPageProps { +// // Next.js 13 App Router에서 기본으로 주어지는 객체들 +// params: { +// lng: string +// id: string +// } +// searchParams: Promise +// } + +// export default async function RfqHistoryPage(props: IndexPageProps) { +// const resolvedParams = await props.params +// const lng = resolvedParams.lng +// const id = resolvedParams.id + +// const idAsNumber = Number(id) + +// // 2) SearchParams 파싱 (Zod) +// // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 +// const searchParams = await props.searchParams +// const search = searchParamsRfqHistoryCache.parse(searchParams) +// const validFilters = getValidFilters(search.filters) + +// const promises = Promise.all([ +// getRfqHistory({ +// ...search, +// filters: validFilters, +// }, +// idAsNumber) +// ]) + +// // 4) 렌더링 +// return ( +//
+//
+//

+// RFQ History +//

+//

+// 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. +//

+//
+// +//
+// +//
+//
+// ) +// } \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/tech-vendors/page.tsx b/app/[lng]/engineering/(engineering)/tech-vendors/page.tsx new file mode 100644 index 00000000..8f542f59 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/tech-vendors/page.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/tech-vendors/validations" +import { getTechVendors, getTechVendorStatusCounts } from "@/lib/tech-vendors/service" +import { TechVendorsTable } from "@/lib/tech-vendors/table/tech-vendors-table" +import { TechVendorContainer } from "@/components/tech-vendors/tech-vendor-container" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + // 벤더 타입 정의 + const vendorTypes = [ + { id: "all", name: "전체", value: "" }, + { id: "ship", name: "조선", value: "조선" }, + { id: "top", name: "해양TOP", value: "해양TOP" }, + { id: "hull", name: "해양HULL", value: "해양HULL" }, + ] + + const promises = Promise.all([ + getTechVendors({ + ...search, + filters: validFilters, + }), + getTechVendorStatusCounts(), + ]) + + return ( + + + } + > + + + + + + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-candidates/page.tsx b/app/[lng]/engineering/(engineering)/vendor-candidates/page.tsx new file mode 100644 index 00000000..a6e00b1b --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-candidates/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" +import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" +import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" +import { DateRangePicker } from "@/components/date-range-picker" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. +

+
+
+
+ + {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} +
+ {/* 수집일 기간 설정: */} + }> + + +
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx b/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx new file mode 100644 index 00000000..3fd7e425 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" +import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" +import { getGeneralEvaluations } from "@/lib/general-check-list/service" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = getGenralEvaluationsSchema.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getGeneralEvaluations({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 협력업체 정기평가 체크리스트 +

+

+ 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx b/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx new file mode 100644 index 00000000..c59de869 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" +import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" +import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsInvestigationCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInvestigation({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Vendor Investigation Management +

+

+ 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/vendor-type/page.tsx b/app/[lng]/engineering/(engineering)/vendor-type/page.tsx new file mode 100644 index 00000000..997c0f82 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendor-type/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/vendor-type/validations" +import { VendorTypesTable } from "@/lib/vendor-type/table/vendorTypes-table" +import { getVendorTypes } from "@/lib/vendor-type/service" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorTypes({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 업체 유형 +

+

+ 업체 유형을 등록하고 관리할 수 있습니다.{" "} + +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/(engineering)/vendors/[id]/info/items/page.tsx b/app/[lng]/engineering/(engineering)/vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..5d5838c6 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendors/[id]/info/items/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorItems } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsItemCache } from "@/lib/vendors/validations" +import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsItemCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorItems({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ 공급품목(패키지) +

+

+ {/* 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. */} +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendors/[id]/info/layout.tsx b/app/[lng]/engineering/(engineering)/vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..7e2cd4f6 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendors/[id]/info/layout.tsx @@ -0,0 +1,94 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Vendor } from "@/db/schema/vendors" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const vendor: Vendor | null = await findVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "연락처", + href: `/${lng}/evcp/vendors/${id}/info`, + }, + { + title: "공급품목(패키지)", + href: `/${lng}/evcp/vendors/${id}/info/items`, + }, + { + title: "공급품목(자재그룹)", + href: `/${lng}/evcp/vendors/${id}/info/materials`, + }, + { + title: "견적 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, + }, + { + title: "입찰 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/bid-history`, + }, + { + title: "계약 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/contract-history`, + }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

협력업체 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendors/[id]/info/materials/page.tsx b/app/[lng]/engineering/(engineering)/vendors/[id]/info/materials/page.tsx new file mode 100644 index 00000000..0ebb66ba --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendors/[id]/info/materials/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsMaterialCache } from "@/lib/vendors/validations" +import { getVendorMaterials } from "@/lib/vendors/service" +import { VendorMaterialsTable } from "@/lib/vendors/materials-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMaterialCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorMaterials({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ 공급품목(자재 그룹) +

+

+ {/* 딜리버리가 가능한 공급품목(자재 그룹)을 확인할 수 있습니다. */} +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendors/[id]/info/page.tsx b/app/[lng]/engineering/(engineering)/vendors/[id]/info/page.tsx new file mode 100644 index 00000000..6279e924 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendors/[id]/info/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorContacts } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/vendors/validations" +import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/engineering/(engineering)/vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..c7f8f8b6 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getRfqHistory } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqHistoryPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqHistoryCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqHistory({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ RFQ History +

+

+ 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendors/page.tsx b/app/[lng]/engineering/(engineering)/vendors/page.tsx new file mode 100644 index 00000000..52af0709 --- /dev/null +++ b/app/[lng]/engineering/(engineering)/vendors/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + +import { searchParamsCache } from "@/lib/vendors/validations" +import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" +import { VendorsTable } from "@/lib/vendors/table/vendors-table" +import { Ellipsis } from "lucide-react" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendors({ + ...search, + filters: validFilters, + }), + getVendorStatusCounts(), + ]) + + return ( + + +
+
+
+

+ 협력업체 리스트 +

+

+ 협력업체에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 협력업체 코드를 따올 수 있습니다. +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/engineering/page.tsx b/app/[lng]/engineering/page.tsx new file mode 100644 index 00000000..f9662cb7 --- /dev/null +++ b/app/[lng]/engineering/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next" +import { Suspense } from "react" +import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" +import { LoginFormSHI } from "@/components/login/login-form-shi" + +export const metadata: Metadata = { + title: "eVCP Portal", + description: "", +} + +export default function AuthenticationPage() { + + + return ( + <> + }> + + + + ) +} diff --git a/app/[lng]/evcp/(evcp)/menu-access/page.tsx b/app/[lng]/evcp/(evcp)/menu-access/page.tsx new file mode 100644 index 00000000..5c87f754 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/menu-access/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { searchParamsUsersCache } from "@/lib/admin-users/validations" +import { getUsersNotPartners } from "@/lib/users/service"; +import { UserAccessControlTable } from "@/lib/users/access-control/users-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = searchParamsUsersCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getUsersNotPartners({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

메뉴 접근제어 관리

+

+ 화면, 메뉴별로 접근 통제를 할 수 있습니다. 도메인을 설정하면 해당 도메인에 대한 접근만 가능합니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/menu-list/page.tsx b/app/[lng]/evcp/(evcp)/menu-list/page.tsx new file mode 100644 index 00000000..84138320 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/menu-list/page.tsx @@ -0,0 +1,70 @@ +// app/evcp/menu-list/page.tsx + +import { Suspense } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { RefreshCw, Settings } from "lucide-react"; +import { getActiveUsers, getMenuAssignments } from "@/lib/menu-list/servcie"; +import { InitializeButton } from "@/lib/menu-list/table/initialize-button"; +import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; +import { Shell } from "@/components/shell" +import * as React from "react" + +export default async function MenuListPage() { + // 초기 데이터 로드 + const [menusResult, usersResult] = await Promise.all([ + getMenuAssignments(), + getActiveUsers() + ]); + + return ( + +
+
+
+

+ 메뉴 관리 +

+

+ 각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. +

+
+
+ +
+ + + + + + + + 메뉴 리스트 + + + 시스템의 모든 메뉴와 담당자 정보를 확인할 수 있습니다. + {menusResult.data?.length > 0 && ( + + 총 {menusResult.data.length}개의 메뉴 + + )} + + + + 로딩 중...}> + + + + + +
+ + ); +} \ No newline at end of file diff --git a/app/[lng]/pending/layout.tsx b/app/[lng]/pending/layout.tsx new file mode 100644 index 00000000..2f767d1d --- /dev/null +++ b/app/[lng]/pending/layout.tsx @@ -0,0 +1,42 @@ +import { UserProfileBadge } from "@/components/layout/user-profile-badge" +import Image from "next/image" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { getServerSession } from "next-auth/next" +export default async function PendingLayout({ + children, +}: { + children: React.ReactNode +}) { + const session = await getServerSession(authOptions) + + return ( +
+ {/* 헤더 없음 - 단순한 로고만 */} +
+
+
+ EVCP Logo + eVCP +
+ + {/* 간단한 사용자 정보만 */} + +
+
+ + {/* 메인 컨텐츠 */} +
+ {children} +
+
+ ) +} + + + diff --git a/app/[lng]/pending/page.tsx b/app/[lng]/pending/page.tsx new file mode 100644 index 00000000..0800e5d2 --- /dev/null +++ b/app/[lng]/pending/page.tsx @@ -0,0 +1,129 @@ +// app/pending/page.tsx +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { + UserCheck, + Clock, + HelpCircle, + FileText, + Mail, + BookOpen +} from "lucide-react" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { getServerSession } from "next-auth/next" + +export default async function PendingPage() { + const session = await getServerSession(authOptions) + + + return ( +
+ {/* 환영 카드 */} + +
+
+ +
+ +
+

+ 환영합니다, {session?.user?.name}님! +

+

+ eVCP 시스템에 가입해주셔서 감사합니다. +

+
+
+
+ + {/* 상태 안내 */} + +
+
+ +
+ +
+

+ 계정 승인 대기 중 +

+

+ 귀하의 계정이 현재 승인 대기 중입니다. 담당자가 검토 후 적절한 권한을 부여해드릴 예정입니다. +

+ +
+

다음 단계:

+
    +
  • • 관리자가 귀하의 소속 부서를 확인합니다
  • +
  • • 적절한 시스템 권한이 부여됩니다
  • +
  • • 이메일로 승인 완료 알림을 받게 됩니다
  • +
+
+
+
+
+ + {/* 연락처 정보 */} +
+ +
+
+ +
+
+

도움이 필요하신가요?

+

+ 시스템 사용법이나 계정 관련 문의사항이 있으시면 언제든 연락해주세요. +

+ +
+
+
+ + +
+
+ +
+
+

사용자 가이드

+

+ 미리 시스템 사용법을 확인하고 싶으시다면 가이드를 참고하세요. +

+ +
+
+
+
+ + {/* 시스템 정보 */} + +

eVCP 시스템 소개

+
+
+

구매관리

+

견적, 입찰, 발주 관리

+
+
+

기술영업

+

프로젝트 영업 지원

+
+
+

설계관리

+

설계 기준정보 확인 및 TBE

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx new file mode 100644 index 00000000..e69de29b diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx new file mode 100644 index 00000000..1af65fbc --- /dev/null +++ b/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx @@ -0,0 +1,52 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { InitialRfqDetailTable } from "@/lib/b-rfq/initial/initial-rfq-detail-table" +import { getInitialRfqDetail } from "@/lib/b-rfq/service" +import { searchParamsInitialRfqDetailCache } from "@/lib/b-rfq/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsInitialRfqDetailCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getInitialRfqDetail({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ Initial RFQ List +

+

+ 설계로부터 받은 RFQ 문서와 구매 RFQ 문서 및 사전 계약자료를 Vendor에 발송하기 위한 RFQ 생성 및 관리하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx new file mode 100644 index 00000000..8dad7676 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx @@ -0,0 +1,87 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import { RfqDashboardView } from "@/db/schema" +import { findBRfqById } from "@/lib/b-rfq/service" + +export const metadata: Metadata = { + title: "견적 RFQ 상세", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqDashboardView | null = await findBRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "견적/입찰 문서관리", + href: `/${lng}/evcp/b-rfq/${id}`, + }, + { + title: "Initial RFQ 발송", + href: `/${lng}/evcp/b-rfq/${id}/initial`, + }, + { + title: "Final RFQ 발송", + href: `/${lng}/evcp/b-rfq/${id}/final`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}` + : "Loading RFQ..."} +

+ +

+ PR발행 전 RFQ를 생성하여 관리하는 화면입니다. +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx new file mode 100644 index 00000000..26dc45fb --- /dev/null +++ b/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx @@ -0,0 +1,53 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations" +import { getRfqAttachments } from "@/lib/b-rfq/service" +import { RfqAttachmentsTable } from "@/lib/b-rfq/attachment/attachment-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqAttachmentsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getRfqAttachments({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ 견적 RFQ 문서관리 +

+

+ 설계로부터 받은 RFQ 문서와 구매 RFQ 문서를 관리하고 Vendor 회신을 점검/관리하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/b-rfq/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/page.tsx new file mode 100644 index 00000000..a66d7b58 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/b-rfq/page.tsx @@ -0,0 +1,79 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsRFQDashboardCache } from "@/lib/b-rfq/validations" +import { getRFQDashboard } from "@/lib/b-rfq/service" +import { RFQDashboardTable } from "@/lib/b-rfq/summary-table/summary-rfq-table" + +export const metadata: Metadata = { + title: "견적 RFQ", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsRFQDashboardCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getRFQDashboard({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + console.log(search, "견적") + + return ( + +
+
+
+

+ 견적 RFQ +

+
+
+
+ + {/* Items처럼 직접 테이블 렌더링 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx b/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx new file mode 100644 index 00000000..adc57ed9 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContractTemplates } from "@/lib/basic-contract/service" +import { searchParamsTemplatesCache } from "@/lib/basic-contract/validations" +import { BasicContractTemplateTable } from "@/lib/basic-contract/template/basic-contract-template" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsTemplatesCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBasicContractTemplates({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 기본계약서 템플릿 관리 +

+

+ 기본계약서를 비롯하여 초기 서명이 필요한 문서를 등록하고 편집할 수 있습니다. 활성화된 템플릿이 서명 요청의 리스트에 나타나게 됩니다..{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/basic-contract/page.tsx b/app/[lng]/procurement/(procurement)/basic-contract/page.tsx new file mode 100644 index 00000000..a043e530 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/basic-contract/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContracts } from "@/lib/basic-contract/service" +import { searchParamsCache } from "@/lib/basic-contract/validations" +import { BasicContractsTable } from "@/lib/basic-contract/status/basic-contract-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBasicContracts({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 기본계약서 서명 현황 +

+

+ 기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명된 문서들을 다운로드할 수 있습니다. {" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/bid-projects/page.tsx b/app/[lng]/procurement/(procurement)/bid-projects/page.tsx new file mode 100644 index 00000000..2039e5b2 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/bid-projects/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBidProjectLists } from "@/lib/bidding-projects/service" +import { searchParamsBidProjectsCache } from "@/lib/bidding-projects/validation" +import { BidProjectsTable } from "@/lib/bidding-projects/table/projects-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsBidProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBidProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 견적 프로젝트 리스트 +

+

+ SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/bqcbe/page.tsx b/app/[lng]/procurement/(procurement)/bqcbe/page.tsx new file mode 100644 index 00000000..ae503feb --- /dev/null +++ b/app/[lng]/procurement/(procurement)/bqcbe/page.tsx @@ -0,0 +1,74 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllCBE } from "@/lib/rfqs/service" +import { searchParamsCBECache } from "@/lib/rfqs/validations" + +import { AllCbeTable } from "@/lib/cbe/table/cbe-table" + +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllCBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/bqtbe/page.tsx b/app/[lng]/procurement/(procurement)/bqtbe/page.tsx new file mode 100644 index 00000000..4989c235 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/bqtbe/page.tsx @@ -0,0 +1,72 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..956facd3 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx new file mode 100644 index 00000000..ba7c071c --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next" +import Link from "next/link" +import { ArrowLeft } from "lucide-react" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx new file mode 100644 index 00000000..dd9df563 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..ec894e1c --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx new file mode 100644 index 00000000..dc2a4a2b --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE_BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-tech-sales-hull/page.tsx new file mode 100644 index 00000000..b1be29db --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-tech-sales-hull/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsHullCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesHullRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface HullRfqPageProps { + searchParams: Promise +} + +export default async function HullRfqPage(props: HullRfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 해양 HULL용 파라미터 파싱 + const search = searchParamsHullCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 해양 Hull RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesHullRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-해양 Hull RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-tech-sales-ship/page.tsx new file mode 100644 index 00000000..b7bf9d15 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-tech-sales-ship/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsShipCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesShipRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface RfqPageProps { + searchParams: Promise +} + +export default async function RfqPage(props: RfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 조선용 파라미터 파싱 + const search = searchParamsShipCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 조선 RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesShipRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-조선 RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary-tech-sales-top/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-tech-sales-top/page.tsx new file mode 100644 index 00000000..f84a9794 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary-tech-sales-top/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsTopCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesTopRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface HullRfqPageProps { + searchParams: Promise +} + +export default async function HullRfqPage(props: HullRfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 해양 TOP용 파라미터 파싱 + const search = searchParamsTopCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 해양 TOP RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesTopRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-해양 TOP RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx new file mode 100644 index 00000000..956facd3 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx new file mode 100644 index 00000000..b0711c66 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next" +import Link from "next/link" +import { ArrowLeft } from "lucide-react" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+ +
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx new file mode 100644 index 00000000..dd9df563 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx new file mode 100644 index 00000000..ec894e1c --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/page.tsx new file mode 100644 index 00000000..04550353 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/budgetary/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/cbe-tech/page.tsx b/app/[lng]/procurement/(procurement)/cbe-tech/page.tsx new file mode 100644 index 00000000..4dadc58f --- /dev/null +++ b/app/[lng]/procurement/(procurement)/cbe-tech/page.tsx @@ -0,0 +1,67 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllCBE } from "@/lib/rfqs-tech/service" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { AllCbeTable } from "@/lib/cbe-tech/table/cbe-table" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + + // SearchParams 파싱 (Zod) + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllCBE({ + ...search, + filters: validFilters, + }) + ]) + + return ( + +
+
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/dashboard/page.tsx b/app/[lng]/procurement/(procurement)/dashboard/page.tsx new file mode 100644 index 00000000..1d61dc16 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/dashboard/page.tsx @@ -0,0 +1,17 @@ +// app/invalid-access/page.tsx + +export default function InvalidAccessPage() { + return ( +
+

부적절한 접근입니다

+

+ 협력업체(Vendor)가 EVCP 화면에 접속하거나
+ SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. +

+

+ 접근 권한이 없으므로, 다른 화면으로 이동해 주세요. +

+
+ ); + } + \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/email-template/[name]/page.tsx b/app/[lng]/procurement/(procurement)/email-template/[name]/page.tsx new file mode 100644 index 00000000..cccc10fc --- /dev/null +++ b/app/[lng]/procurement/(procurement)/email-template/[name]/page.tsx @@ -0,0 +1,26 @@ +import { getTemplateAction } from '@/lib/mail/service'; +import MailTemplateEditorClient from '@/components/mail/mail-template-editor-client'; + +interface EditMailTemplatePageProps { + params: { + name: string; + lng: string; + }; +} + +export default async function EditMailTemplatePage({ params }: EditMailTemplatePageProps) { + const { name: templateName } = await params; + + // 서버에서 초기 템플릿 데이터 가져오기 + const result = await getTemplateAction(templateName); + const initialTemplate = result.success ? result.data : null; + + return ( +
+ +
+ ); +} diff --git a/app/[lng]/procurement/(procurement)/email-template/page.tsx b/app/[lng]/procurement/(procurement)/email-template/page.tsx new file mode 100644 index 00000000..1ef3de6c --- /dev/null +++ b/app/[lng]/procurement/(procurement)/email-template/page.tsx @@ -0,0 +1,19 @@ +import { getTemplatesAction } from '@/lib/mail/service'; +import MailTemplatesClient from '@/components/mail/mail-templates-client'; + +export default async function MailTemplatesPage() { + // 서버에서 초기 데이터 가져오기 + const result = await getTemplatesAction(); + const initialData = result.success ? result.data : []; + + return ( +
+
+

메일 템플릿 관리

+

이메일 템플릿을 관리할 수 있습니다.

+
+ + +
+ ); +} diff --git a/app/[lng]/procurement/(procurement)/equip-class/page.tsx b/app/[lng]/procurement/(procurement)/equip-class/page.tsx new file mode 100644 index 00000000..cfa8f133 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/equip-class/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/equip-class/validation" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" +import { getTagClassists } from "@/lib/equip-class/service" +import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagClassists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 객체 클래스 목록 from S-EDP +

+

+ 객체 클래스 목록을 확인할 수 있습니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx b/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx new file mode 100644 index 00000000..515751d5 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getEsgEvaluations } from "@/lib/esg-check-list/service" +import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" +import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = getEsgEvaluationsSchema.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getEsgEvaluations({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ ESG 자가진단표 +

+

+ 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx new file mode 100644 index 00000000..a660c492 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx @@ -0,0 +1,81 @@ +/* IMPORT */ +import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton'; +import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service'; +import { getValidFilters } from '@/lib/data-table'; +import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table'; +import { searchParamsCache } from '@/lib/evaluation-criteria/validations'; +import { Shell } from '@/components/shell'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Suspense } from 'react'; +import { type SearchParams } from '@/types/table'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +interface EvaluationCriteriaPageProps { + searchParams: Promise +} + +// ---------------------------------------------------------------------------------------------------- + +/* REGULAR EVALUATION CRITERIA PAGE */ +async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) { + const searchParams = await props.searchParams; + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + const promises = Promise.all([ + getRegEvalCriteria({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+
+

+ 협력업체 평가기준표 +

+

+ 협력업체 평가에 사용되는 평가기준표를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} + +// ---------------------------------------------------------------------------------------------------- + +/* EXPORT */ +export default EvaluationCriteriaPage; \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx new file mode 100644 index 00000000..088ae75b --- /dev/null +++ b/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" + +import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" +import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" +import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" + +export const metadata: Metadata = { + title: "협력업체 평가 대상 확정", + description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", +} + +interface EvaluationTargetsPageProps { + searchParams: Promise +} + + + +export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsEvaluationTargetsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 (필터에서 가져오거나 기본값 사용) + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getEvaluationTargets({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + + {/* 간소화된 헤더 */} +
+
+
+

+ 협력업체 평가 대상 확정 +

+ + {currentEvaluationYear}년도 + + +
+
+
+ + {/* 메인 테이블 (통계는 테이블 내부로 이동) */} + + } + > + {currentEvaluationYear && + +} + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation/page.tsx b/app/[lng]/procurement/(procurement)/evaluation/page.tsx new file mode 100644 index 00000000..ead61077 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/evaluation/page.tsx @@ -0,0 +1,181 @@ +// ================================================================ +// 4. PERIODIC EVALUATIONS PAGE +// ================================================================ + +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" +import { getPeriodicEvaluations } from "@/lib/evaluation/service" +import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" + +export const metadata: Metadata = { + title: "협력업체 정기평가", + description: "협력업체 정기평가 진행 현황을 관리합니다.", +} + +interface PeriodicEvaluationsPageProps { + searchParams: Promise +} + +// 프로세스 안내 팝오버 컴포넌트 +function ProcessGuidePopover() { + return ( + + + + + +
+
+

정기평가 프로세스

+

+ 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. +

+
+
+
+
+ 1 +
+
+

평가 대상 확정

+

평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.

+
+
+
+
+ 2 +
+
+

업체 자료 제출

+

각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.

+
+
+
+
+ 3 +
+
+

평가자 검토

+

지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.

+
+
+
+
+ 4 +
+
+

최종 확정

+

모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.

+
+
+
+
+
+
+ ) +} + +// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 +function getDefaultEvaluationYear() { + return new Date().getFullYear() +} + + + +export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsEvaluationsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters || []) + + // 기본 필터 처리 + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPeriodicEvaluations({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + + {/* 헤더 */} +
+
+
+

+ 협력업체 정기평가 +

+ + {currentEvaluationYear}년도 + +
+
+
+ + {/* 메인 테이블 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/manage/actions.ts b/app/[lng]/procurement/(procurement)/faq/manage/actions.ts new file mode 100644 index 00000000..bc443a8a --- /dev/null +++ b/app/[lng]/procurement/(procurement)/faq/manage/actions.ts @@ -0,0 +1,48 @@ +'use server'; + +import { promises as fs } from 'fs'; +import path from 'path'; +import { FaqCategory } from '@/components/faq/FaqCard'; +import { fallbackLng } from '@/i18n/settings'; + +const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); + +export async function updateFaqData(lng: string, newData: FaqCategory[]) { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + const updatedData = { + ...allData, + [lng]: newData + }; + + const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; + await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); + + return { success: true }; + } catch (error) { + console.error('FAQ 데이터 업데이트 중 오류 발생:', error); + return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; + } +} + +export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + return { data: allData[lng] || allData[fallbackLng] || [] }; + } catch (error) { + console.error('FAQ 데이터 읽기 중 오류 발생:', error); + return { data: [] }; + } +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/manage/page.tsx b/app/[lng]/procurement/(procurement)/faq/manage/page.tsx new file mode 100644 index 00000000..011bbfa4 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/faq/manage/page.tsx @@ -0,0 +1,38 @@ +import { FaqManager } from '@/components/faq/FaqManager'; +import { getFaqData, updateFaqData } from './actions'; +import { revalidatePath } from 'next/cache'; +import { FaqCategory } from '@/components/faq/FaqCard'; + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqManagePage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const { data } = await getFaqData(lng); + + async function handleSave(newData: FaqCategory[]) { + 'use server'; + await updateFaqData(lng, newData); + revalidatePath(`/${lng}/evcp/faq`); + } + + return ( +
+
+
+
+

FAQ Management

+

+ Manage FAQ categories and items for {lng.toUpperCase()} language. +

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/page.tsx b/app/[lng]/procurement/(procurement)/faq/page.tsx new file mode 100644 index 00000000..9b62b7e4 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/faq/page.tsx @@ -0,0 +1,62 @@ +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { faqCategories } from "@/config/faqDataConfig" +import { FaqCard } from "@/components/faq/FaqCard" +import { Button } from "@/components/ui/button" +import { Settings } from "lucide-react" +import Link from "next/link" +import { fallbackLng } from "@/i18n/settings" + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqPage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; + + return ( +
+
+
+
+
+

Frequently Asked Questions

+

+ Find answers to common questions about using the EVCP system. +

+
+ + + +
+ + + + + {localizedFaqCategories.map((category) => ( + + {category.label} + + ))} + + + {localizedFaqCategories.map((category) => ( + + {category.items.map((item, index) => ( + + ))} + + ))} + +
+
+
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/form-list/page.tsx b/app/[lng]/procurement/(procurement)/form-list/page.tsx new file mode 100644 index 00000000..a6cf7d9e --- /dev/null +++ b/app/[lng]/procurement/(procurement)/form-list/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/form-list/validation" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getFormLists } from "@/lib/form-list/service" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getFormLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 레지스터 목록 from S-EDP +

+

+ 협력업체 데이터 입력을 위한 레지스터 목록 리스트입니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/incoterms/page.tsx b/app/[lng]/procurement/(procurement)/incoterms/page.tsx new file mode 100644 index 00000000..57a19009 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/incoterms/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { SearchParamsCache } from "@/lib/incoterms/validations"; +import { getIncoterms } from "@/lib/incoterms/service"; +import { IncotermsTable } from "@/lib/incoterms/table/incoterms-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = SearchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getIncoterms({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

인코텀즈 관리

+

+ 인코텀즈(Incoterms)를 등록, 수정, 삭제할 수 있습니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/items-tech/layout.tsx b/app/[lng]/procurement/(procurement)/items-tech/layout.tsx new file mode 100644 index 00000000..d375059b --- /dev/null +++ b/app/[lng]/procurement/(procurement)/items-tech/layout.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { ItemTechContainer } from "@/components/items-tech/item-tech-container" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default function ItemsShipLayout({ + children, +}: { + children: React.ReactNode +}) { + // 아이템 타입 정의 + const itemTypes = [ + { id: "ship", name: "조선 아이템" }, + { id: "top", name: "해양 TOP" }, + { id: "hull", name: "해양 HULL" }, + ] + + return ( + + + } + > + + {children} + + + + ) +} diff --git a/app/[lng]/procurement/(procurement)/items-tech/page.tsx b/app/[lng]/procurement/(procurement)/items-tech/page.tsx new file mode 100644 index 00000000..55ac9c63 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/items-tech/page.tsx @@ -0,0 +1,67 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations" +import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service" +import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table" +import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table" + +// 대소문자 문제 해결 - 실제 파일명에 맞게 import +import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage({ searchParams }: IndexPageProps) { + const params = await searchParams + const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params) + const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params) + const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params) + const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || []) + const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || []) + const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || []) + + + // URL에서 아이템 타입 가져오기 + const itemType = params.type || "ship" + + return ( +
+ {itemType === "ship" && ( + result)} + /> + )} + + {itemType === "top" && ( + result)} + /> + )} + + {itemType === "hull" && ( + result)} + /> + )} +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/items/page.tsx b/app/[lng]/procurement/(procurement)/items/page.tsx new file mode 100644 index 00000000..0c44bf0a --- /dev/null +++ b/app/[lng]/procurement/(procurement)/items/page.tsx @@ -0,0 +1,68 @@ +// app/items/page.tsx (업데이트) +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/items/validations" +import { getItems } from "@/lib/items/service" +import { ItemsTable } from "@/lib/items/table/items-table" +import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // pageSize 기반으로 모드 자동 결정 + const isInfiniteMode = search.perPage >= 1_000_000 + + // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 + // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 + const promises = isInfiniteMode + ? undefined + : Promise.all([ + getItems(search), // searchParamsCache의 결과를 그대로 사용 + ]) + + return ( + +
+
+
+

+ 패키지 정보 +

+

+ S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. +

+
+
+ +
+ + }> + {/* DateRangePicker 등 추가 컴포넌트 */} + + + + } + > + {/* 통합된 ItemsTable 컴포넌트 사용 */} + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/layout.tsx b/app/[lng]/procurement/(procurement)/layout.tsx new file mode 100644 index 00000000..82b53307 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/layout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Header } from '@/components/layout/Header'; +import { SiteFooter } from '@/components/layout/Footer'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( +
+ {/*
*/} +
+
+
+ {children} +
+
+ +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/menu-list/page.tsx b/app/[lng]/procurement/(procurement)/menu-list/page.tsx new file mode 100644 index 00000000..84138320 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/menu-list/page.tsx @@ -0,0 +1,70 @@ +// app/evcp/menu-list/page.tsx + +import { Suspense } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { RefreshCw, Settings } from "lucide-react"; +import { getActiveUsers, getMenuAssignments } from "@/lib/menu-list/servcie"; +import { InitializeButton } from "@/lib/menu-list/table/initialize-button"; +import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; +import { Shell } from "@/components/shell" +import * as React from "react" + +export default async function MenuListPage() { + // 초기 데이터 로드 + const [menusResult, usersResult] = await Promise.all([ + getMenuAssignments(), + getActiveUsers() + ]); + + return ( + +
+
+
+

+ 메뉴 관리 +

+

+ 각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. +

+
+
+ +
+ + + + + + + + 메뉴 리스트 + + + 시스템의 모든 메뉴와 담당자 정보를 확인할 수 있습니다. + {menusResult.data?.length > 0 && ( + + 총 {menusResult.data.length}개의 메뉴 + + )} + + + + 로딩 중...
}> + + + + + + + + ); +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx b/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx new file mode 100644 index 00000000..b9aedfbb --- /dev/null +++ b/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { SearchParamsCache } from "@/lib/payment-terms/validations"; +import { getPaymentTerms } from "@/lib/payment-terms/service"; +import { PaymentTermsTable } from "@/lib/payment-terms/table/payment-terms-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = SearchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getPaymentTerms({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

결제 조건 관리

+

+ 결제 조건(Payment Terms)을 등록, 수정, 삭제할 수 있습니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/po-rfq/page.tsx b/app/[lng]/procurement/(procurement)/po-rfq/page.tsx new file mode 100644 index 00000000..bdeae25e --- /dev/null +++ b/app/[lng]/procurement/(procurement)/po-rfq/page.tsx @@ -0,0 +1,61 @@ +import { getPORfqs } from "@/lib/procurement-rfqs/services" +import { searchParamsCache } from "@/lib/procurement-rfqs/validations" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/procurement-rfqs/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface RfqPageProps { + searchParams: Promise +} + +export default async function RfqPage(props: RfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 파라미터 파싱 + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // RFQ 서버는 기본필터와 고급필터를 분리해서 받으므로 그대로 전달 + const promises = Promise.all([ + getPORfqs({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 발주용 견적 +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/po/page.tsx b/app/[lng]/procurement/(procurement)/po/page.tsx new file mode 100644 index 00000000..7868e231 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/po/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getPOs } from "@/lib/po/service" +import { searchParamsCache } from "@/lib/po/validations" +import { PoListsTable } from "@/lib/po/table/po-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getPOs({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ PO 확인 및 전자서명 +

+

+ 기간계 시스템으로부터 PO를 확인하고 협력업체에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/poa/page.tsx b/app/[lng]/procurement/(procurement)/poa/page.tsx new file mode 100644 index 00000000..dec5e05b --- /dev/null +++ b/app/[lng]/procurement/(procurement)/poa/page.tsx @@ -0,0 +1,61 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getChangeOrders } from "@/lib/poa/service" +import { searchParamsCache } from "@/lib/poa/validations" +import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getChangeOrders({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +
+
+
+

+ 변경 PO 확인 및 전자서명 +

+

+ 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. +

+
+
+
+ + }> + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx new file mode 100644 index 00000000..55b1e9df --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq-criteria/[id]/page.tsx @@ -0,0 +1,81 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" +import { notFound } from "next/navigation" + +interface ProjectPageProps { + params: { id: string } + searchParams: Promise +} + +export default async function ProjectPage(props: ProjectPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const projectId = parseInt(id, 10) + + // 유효하지 않은 projectId 확인 + if (isNaN(projectId)) { + notFound() + } + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + const validFilters = getValidFilters(search.filters) + + // 프로젝트별 PQ 데이터 가져오기 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, projectId, false) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx new file mode 100644 index 00000000..7785b541 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + + const validFilters = getValidFilters(search.filters) + + // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, null, true) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq/[vendorId]/page.tsx b/app/[lng]/procurement/(procurement)/pq/[vendorId]/page.tsx new file mode 100644 index 00000000..76bcfe59 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq/[vendorId]/page.tsx @@ -0,0 +1,108 @@ +import * as React from "react" +import { Shell } from "@/components/shell" +import { type SearchParams } from "@/types/table" +import { getPQDataByVendorId, getVendorPQsList, loadGeneralPQData, loadProjectPQAction, loadProjectPQData } from "@/lib/pq/service" +import { Vendor } from "@/db/schema/vendors" +import { findVendorById } from "@/lib/vendors/service" +import VendorPQAdminReview from "@/components/pq/pq-review-detail" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" + +interface IndexPageProps { + params: { + vendorId: string + } + searchParams: Promise +} + +export default async function PQReviewPage(props: IndexPageProps) { + const resolvedParams = await props.params + const vendorId = Number(resolvedParams.vendorId) + + // Fetch the vendor data + const vendor: Vendor | null = await findVendorById(vendorId) + if (!vendor) return
Vendor not found
+ + // Get list of all PQs (general + project-specific) for this vendor + const pqsList = await getVendorPQsList(vendorId) + + // Determine default active PQ to display + // If query param projectId exists, use that, otherwise use general PQ if available + const searchParams = await props.searchParams + const activeProjectId = searchParams.projectId ? Number(searchParams.projectId) : undefined + + // If no projectId query param, default to general PQ or first project PQ + const defaultTabId = activeProjectId ? + `project-${activeProjectId}` : + (pqsList.hasGeneralPq ? 'general' : `project-${pqsList.projectPQs[0]?.projectId}`) + + // Fetch PQ data for the active tab + let pqData; + if (activeProjectId) { + // Get project-specific PQ data + pqData = await getPQDataByVendorId(vendorId, activeProjectId) + } else { + // Get general PQ data + pqData = await getPQDataByVendorId(vendorId) + } + + return ( + + {pqsList.hasGeneralPq || pqsList.projectPQs.length > 0 ? ( + +
+

+ {vendor.vendorName} PQ Review +

+ + + {pqsList.hasGeneralPq && ( + + General PQ Standard + + )} + + {pqsList.projectPQs.map((project) => ( + + {project.projectName} {project.status} + + ))} + +
+ + {/* Tab content for General PQ */} + {pqsList.hasGeneralPq && ( + + + + )} + + {/* Tab content for each Project PQ */} + {pqsList.projectPQs.map((project) => ( + + + + ))} +
+ ) : ( +
+

No PQ submissions found for this vendor

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq/page.tsx b/app/[lng]/procurement/(procurement)/pq/page.tsx new file mode 100644 index 00000000..46b22b12 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq/page.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorsInPQ } from "@/lib/pq/service" +import { searchParamsCache } from "@/lib/vendors/validations" +import { VendorsPQReviewTable } from "@/lib/pq/pq-review-table/vendors-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInPQ({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Pre-Qualification Review +

+

+ 벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. + +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx new file mode 100644 index 00000000..28ce3128 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -0,0 +1,215 @@ +import * as React from "react" +import { Metadata } from "next" +import Link from "next/link" +import { notFound } from "next/navigation" +import { ArrowLeft } from "lucide-react" +import { Shell } from "@/components/shell" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Separator } from "@/components/ui/separator" +import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" +import { unstable_noStore as noStore } from 'next/cache' +import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" + +export const metadata: Metadata = { + title: "PQ 검토", + description: "협력업체의 Pre-Qualification 답변을 검토합니다.", +} + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic" + +interface PQReviewPageProps { + params: Promise<{ + vendorId: string; + submissionId: string; + }> +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + // 캐시 비활성화 + noStore() + + const params = await props.params + const vendorId = parseInt(params.vendorId, 10) + const submissionId = parseInt(params.submissionId, 10) + + try { + // PQ Submission 정보 조회 + const pqSubmission = await getPQById(submissionId, vendorId) + + // PQ 데이터 조회 (질문과 답변) + const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) + + // 프로젝트 정보 (프로젝트 PQ인 경우) + const projectInfo = pqSubmission.projectId ? { + id: pqSubmission.projectId, + projectCode: pqSubmission.projectCode || '', + projectName: pqSubmission.projectName || '', + status: pqSubmission.status, + submittedAt: pqSubmission.submittedAt, + } : null + + // PQ 유형 및 상태 레이블 + const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : "프로젝트 PQ" + const statusLabel = getStatusLabel(pqSubmission.status) + const statusVariant = getStatusVariant(pqSubmission.status) + + // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) + const canReview = pqSubmission.status === "SUBMITTED" + + return ( + +
+
+ +
+

+ {pqSubmission.vendorName} - {typeLabel} +

+
+ {statusLabel} + {projectInfo && ( + + {projectInfo.projectName} ({projectInfo.projectCode}) + + )} +
+
+
+
+ + {/* 상태별 알림 */} + {pqSubmission.status === "SUBMITTED" && ( + + 제출 완료 + + 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. + + + )} + + {pqSubmission.status === "APPROVED" && ( + + 승인됨 + + {formatDate(pqSubmission.approvedAt)}에 승인되었습니다. + + + )} + + {pqSubmission.status === "REJECTED" && ( + + 거부됨 + + {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다. + {pqSubmission.rejectReason && ( +
+ 사유: {pqSubmission.rejectReason} +
+ )} +
+
+ )} + + + + {/* PQ 검토 컴포넌트 */} + + + PQ 검토 + 협력업체 정보 + + + + + + + +
+

협력업체 정보

+
+
+

업체명

+

{pqSubmission.vendorName}

+
+
+

업체 코드

+

{pqSubmission.vendorCode}

+
+
+

상태

+

{pqSubmission.vendorStatus}

+
+ {/* 필요시 추가 정보 표시 */} +
+
+
+
+
+ ) + } catch (error) { + console.error("Error loading PQ:", error) + notFound() + } +} + +// 상태 레이블 함수 +function getStatusLabel(status: string): string { + switch (status) { + case "REQUESTED": + return "요청됨"; + case "IN_PROGRESS": + return "진행 중"; + case "SUBMITTED": + return "제출됨"; + case "APPROVED": + return "승인됨"; + case "REJECTED": + return "거부됨"; + default: + return status; + } +} + +// 상태별 Badge 스타일 +function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { + switch (status) { + case "REQUESTED": + return "outline"; + case "IN_PROGRESS": + return "secondary"; + case "SUBMITTED": + return "default"; + case "APPROVED": + return "success"; + case "REJECTED": + return "destructive"; + default: + return "outline"; + } +} + +// 날짜 형식화 함수 +function formatDate(date: Date | null) { + if (!date) return "날짜 없음"; + return new Date(date).toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + }); +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/page.tsx new file mode 100644 index 00000000..6598349b --- /dev/null +++ b/app/[lng]/procurement/(procurement)/pq_new/page.tsx @@ -0,0 +1,96 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsPQReviewCache } from "@/lib/pq/validations" +import { getPQSubmissions } from "@/lib/pq/service" +import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" + +export const metadata: Metadata = { + title: "PQ 검토/실사 의뢰", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsPQReviewCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 디버깅 로그 추가 + console.log("=== PQ Page Debug ==="); + console.log("Raw searchParams:", searchParams); + console.log("Raw basicFilters param:", searchParams.basicFilters); + console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); + console.log("Parsed search:", search); + console.log("search.filters:", search.filters); + console.log("search.basicFilters:", search.basicFilters); + console.log("search.pqBasicFilters:", search.pqBasicFilters); + console.log("validFilters:", validFilters); + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { + // 하위 호환성을 위해 기존 이름도 지원 + basicFilters = search.pqBasicFilters + console.log("Using search.pqBasicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + console.log("Final allFilters:", allFilters); + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; + console.log("Final joinOperator:", joinOperator); + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPQSubmissions({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + +
+
+
+

+ PQ 검토/실사 의뢰 +

+
+
+
+ + {/* Items처럼 직접 테이블 렌더링 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/project-gtc/page.tsx b/app/[lng]/procurement/(procurement)/project-gtc/page.tsx new file mode 100644 index 00000000..8e12a489 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/project-gtc/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getProjectGtcList } from "@/lib/project-gtc/service" +import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" +import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = projectGtcSearchParamsSchema.parse(searchParams) + + const promises = Promise.all([ + getProjectGtcList({ + page: search.page, + perPage: search.perPage, + search: search.search, + sort: search.sort, + }), + ]) + + return ( + +
+
+
+

+ Project GTC +

+

+ 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. + 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. +

+
+
+
+ + }> + {/* 추가 기능이 필요하면 여기에 추가 */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/project-vendors/page.tsx b/app/[lng]/procurement/(procurement)/project-vendors/page.tsx new file mode 100644 index 00000000..dcc66071 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/project-vendors/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" +import { getProjecTAVL } from "@/lib/project-avl/service" +import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchProjectAVLParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjecTAVL({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 프로젝트 AVL 리스트 +

+

+ 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/projects/page.tsx b/app/[lng]/procurement/(procurement)/projects/page.tsx new file mode 100644 index 00000000..0320f259 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/projects/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getProjectLists } from "@/lib/projects/service" +import { ProjectsTable } from "@/lib/projects/table/projects-table" +import { searchParamsProjectsCache } from "@/lib/projects/validation" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Project List from S-EDP +

+

+ S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/report/page.tsx b/app/[lng]/procurement/(procurement)/report/page.tsx new file mode 100644 index 00000000..3efaa7c3 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/report/page.tsx @@ -0,0 +1,47 @@ +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + + +export default async function IndexPage() { + + + return ( + +
+
+

+ Dashboard +

+

+ 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. +

+
+
+ + }> + {/* */} + + + + } + > + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq-tech/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/cbe/page.tsx new file mode 100644 index 00000000..84379caf --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/cbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { getCBE } from "@/lib/rfqs-tech/service" +import { CbeTable } from "@/lib/rfqs-tech/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq-tech/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/layout.tsx new file mode 100644 index 00000000..0bb62fe0 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/layout.tsx @@ -0,0 +1,89 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs-tech/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq-tech/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq-tech/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq-tech/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq-tech/[id]/page.tsx b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/page.tsx new file mode 100644 index 00000000..007270a1 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs-tech/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs-tech/validations" +import { MatchedVendorsTable } from "@/lib/rfqs-tech/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq-tech/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/tbe/page.tsx new file mode 100644 index 00000000..4b226cdc --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq-tech/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { TbeTable } from "@/lib/rfqs-tech/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq-tech/page.tsx b/app/[lng]/procurement/(procurement)/rfq-tech/page.tsx new file mode 100644 index 00000000..f35b3632 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq-tech/page.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs-tech/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs-tech/service" +import { RfqsTable } from "@/lib/rfqs-tech/table/rfqs-table" +import { getAllOffshoreItems } from "@/lib/items-tech/service" + +interface RfqPageProps { + searchParams: Promise; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + title = "기술영업 해양 RFQ", + description = "기술영업 해양 RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + }), + getRfqStatusCounts(), + getAllOffshoreItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..fb288a98 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCBECache } from "@/lib/rfqs/validations" +import { getCBE } from "@/lib/rfqs/service" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx new file mode 100644 index 00000000..9a03efa4 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx @@ -0,0 +1,89 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx new file mode 100644 index 00000000..1a9f4b18 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..76eea302 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/rfq/page.tsx b/app/[lng]/procurement/(procurement)/rfq/page.tsx new file mode 100644 index 00000000..3417b0bf --- /dev/null +++ b/app/[lng]/procurement/(procurement)/rfq/page.tsx @@ -0,0 +1,80 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE, + title = "RFQ", + description = "RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/settings/layout.tsx b/app/[lng]/procurement/(procurement)/settings/layout.tsx new file mode 100644 index 00000000..6f373567 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/settings/layout.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "Settings", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Account", + href: `/${lng}/evcp/settings`, + }, + { + title: "Preferences", + href: `/${lng}/evcp/settings/preferences`, + } + + + ] + + + return ( + <> +
+
+
+
+

Settings

+

+ Manage your account settings and preferences. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/procurement/(procurement)/settings/page.tsx b/app/[lng]/procurement/(procurement)/settings/page.tsx new file mode 100644 index 00000000..a6eaac90 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/settings/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/components/settings/account-form" + +export default function SettingsAccountPage() { + return ( +
+
+

Account

+

+ Update your account settings. Set your preferred language and + timezone. +

+
+ + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx b/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx new file mode 100644 index 00000000..e2a88021 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/components/settings/appearance-form" + +export default function SettingsAppearancePage() { + return ( +
+
+

Preference

+

+ Customize the preference of the app. +

+
+ + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx b/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx new file mode 100644 index 00000000..11a9e9fb --- /dev/null +++ b/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" +import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsers({ + ...search, + filters: validFilters, + }), + getUserCountGroupByCompany(), + getUserCountGroupByRole(), + getAllCompanies(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

Vendor Admin User Management

+

+ 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/procurement/(procurement)/system/layout.tsx b/app/[lng]/procurement/(procurement)/system/layout.tsx new file mode 100644 index 00000000..7e8f69d0 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/system/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "System Setting", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "삼성중공업 사용자", + href: `/${lng}/evcp/system`, + }, + { + title: "Roles", + href: `/${lng}/evcp/system/roles`, + }, + { + title: "권한 통제", + href: `/${lng}/evcp/system/permissions`, + }, + { + title: "협력업체 사용자", + href: `/${lng}/evcp/system/admin-users`, + }, + + { + title: "비밀번호 정책", + href: `/${lng}/evcp/system/password-policy`, + }, + + ] + + + return ( + <> +
+
+
+
+

시스템 설정

+

+ 사용자, 롤, 접근 권한을 관리하세요. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/procurement/(procurement)/system/page.tsx b/app/[lng]/procurement/(procurement)/system/page.tsx new file mode 100644 index 00000000..fe0a262c --- /dev/null +++ b/app/[lng]/procurement/(procurement)/system/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import * as React from "react" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllRoles, getUsersEVCP } from "@/lib/users/service" +import { getUserCountGroupByRole } from "@/lib/admin-users/service" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { UserTable } from "@/lib/users/table/users-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function SystemUserPage(props: IndexPageProps) { + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsersEVCP({ + ...search, + filters: validFilters, + }), + getUserCountGroupByRole(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

SHI Users

+

+ 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. +

+
+ + +
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx b/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx new file mode 100644 index 00000000..0f14fefe --- /dev/null +++ b/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx @@ -0,0 +1,63 @@ +// app/admin/password-policy/page.tsx + +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { AlertTriangle } from "lucide-react" +import SecuritySettingsTable from "@/components/system/passwordPolicy" +import { getSecuritySettings } from "@/lib/password-policy/service" + + +export default async function PasswordPolicyPage() { + try { + // 보안 설정 데이터 로드 + const securitySettings = await getSecuritySettings() + + return ( + + } + > +
+
+

협력업체 사용자 비밀번호 정책 설정

+

+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. +

+
+ + +
+
+ ) + } catch (error) { + console.error('Failed to load security settings:', error) + + return ( +
+
+

협력업체 사용자 비밀번호 정책 설정

+

+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. +

+
+ + + + + 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. + + +
+ ) + } +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/system/permissions/page.tsx b/app/[lng]/procurement/(procurement)/system/permissions/page.tsx new file mode 100644 index 00000000..6aa2b693 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/system/permissions/page.tsx @@ -0,0 +1,17 @@ +import PermissionsTree from "@/components/system/permissionsTree" +import { Separator } from "@/components/ui/separator" + +export default function PermissionsPage() { + return ( +
+
+

Permissions

+

+ Set permissions to the menu by Role +

+
+ + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/system/roles/page.tsx b/app/[lng]/procurement/(procurement)/system/roles/page.tsx new file mode 100644 index 00000000..fe074600 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/system/roles/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/roles/validations" +import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" +import { RolesTable } from "@/lib/roles/table/roles-table" +import { getRolesWithCount } from "@/lib/roles/services" +import { getUsersAll } from "@/lib/users/service" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const search2 = searchParamsCache2.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRolesWithCount({ + ...search, + filters: validFilters, + }), + + + ]) + + + const promises2 = Promise.all([ + getUsersAll({ + ...search2, + filters: validFilters, + }, "evcp"), + ]) + + + return ( + + } + > +
+
+

Role Management

+

+ 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/procurement/(procurement)/tag-numbering/page.tsx b/app/[lng]/procurement/(procurement)/tag-numbering/page.tsx new file mode 100644 index 00000000..44695259 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tag-numbering/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/tag-numbering/validation" +import { getTagNumbering } from "@/lib/tag-numbering/service" +import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagNumbering({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 태그 타입 목록 from S-EDP +

+

+ 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/tasks/page.tsx b/app/[lng]/procurement/(procurement)/tasks/page.tsx new file mode 100644 index 00000000..91b946fb --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tasks/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Shell } from "@/components/shell" + +import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" +import { TasksTable } from "@/lib/tasks/table/tasks-table" +import { + getTaskPriorityCounts, + getTasks, + getTaskStatusCounts, +} from "@/lib/tasks/service" +import { searchParamsCache } from "@/lib/tasks/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTasks({ + ...search, + filters: validFilters, + }), + getTaskStatusCounts(), + getTaskPriorityCounts(), + ]) + + return ( + + }> + + + + } + > + + + + ) +} diff --git a/app/[lng]/procurement/(procurement)/tbe-tech/page.tsx b/app/[lng]/procurement/(procurement)/tbe-tech/page.tsx new file mode 100644 index 00000000..17b01ce2 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tbe-tech/page.tsx @@ -0,0 +1,67 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { AllTbeTable } from "@/lib/tbe-tech/table/tbe-table" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + }) + ]) + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tbe/page.tsx b/app/[lng]/procurement/(procurement)/tbe/page.tsx new file mode 100644 index 00000000..1a7fdf86 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tbe/page.tsx @@ -0,0 +1,113 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +// 타입별 페이지 설명 구성 (Budgetary 제외) +const typeConfig: Record = { + "purchase": { + title: "Purchase RFQ Technical Bid Evaluation", + description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE + }, + "purchase-budgetary": { + title: "Purchase Budgetary RFQ Technical Bid Evaluation", + description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE_BUDGETARY + } +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + // 기본값으로 'purchase' 사용 + const typeParam = searchParams?.type as string || 'purchase' + + // 유효한 타입인지 확인하고 기본값 설정 + const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' + const rfqType = typeConfig[validType].rfqType + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + }) + ]) + + // 페이지 경로 생성 함수 - 단순화 + const getTabUrl = (type: string) => { + return `/${lng}/evcp/tbe?type=${type}`; + } + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + {/* 타입 선택 탭 (Budgetary 제외) */} + + + + Purchase + + + Purchase Budgetary + + + +
+

+ {typeConfig[validType].description} +

+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tech-project-avl/page.tsx b/app/[lng]/procurement/(procurement)/tech-project-avl/page.tsx new file mode 100644 index 00000000..d942c5c5 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-project-avl/page.tsx @@ -0,0 +1,85 @@ +import * as React from "react" +import { redirect } from "next/navigation" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { SearchParams } from "@/types/table" +import { searchParamsCache } from "@/lib/tech-project-avl/validations" +import { Skeleton } from "@/components/ui/skeleton" +import { Shell } from "@/components/shell" +import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table" +import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Ellipsis } from "lucide-react" + +export interface PageProps { + params: Promise<{ lng: string }> + searchParams: Promise +} + +export default async function AcceptedQuotationsPage({ + params, + searchParams, +}: PageProps) { + const { lng } = await params + + const session = await getServerSession(authOptions) + if (!session) { + redirect(`/${lng}/auth/signin`) + } + + const search = await searchParams + const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search) + const validFilters = getValidFilters(filters ?? []) + + const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({ + page, + perPage: perPage ?? 10, + sort, + search: searchText, + filters: validFilters, + }) + + return ( + +
+
+
+

+ 승인된 견적서(해양TOP,HULL) +

+

+ 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다. +

+
+
+
+ + }> + {/* Date range picker can be added here if needed */} + + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/tech-vendor-candidates/page.tsx b/app/[lng]/procurement/(procurement)/tech-vendor-candidates/page.tsx new file mode 100644 index 00000000..3923863a --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-vendor-candidates/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/tech-vendor-candidates/service" +import { searchParamsTechCandidateCache } from "@/lib/tech-vendor-candidates/validations" +import { VendorCandidateTable as TechVendorCandidateTable } from "@/lib/tech-vendor-candidates/table/candidates-table" +import { DateRangePicker } from "@/components/date-range-picker" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsTechCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. +

+
+
+
+ + {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} +
+ {/* 수집일 기간 설정: */} + }> + + +
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/items/page.tsx b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..69c36576 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/items/page.tsx @@ -0,0 +1,48 @@ +// import { Separator } from "@/components/ui/separator" +// import { getTechVendorById, getVendorItemsByType } from "@/lib/tech-vendors/service" +// import { type SearchParams } from "@/types/table" +// import { TechVendorItemsTable } from "@/lib/tech-vendors/items-table/item-table" + +// interface IndexPageProps { +// // Next.js 13 App Router에서 기본으로 주어지는 객체들 +// params: { +// lng: string +// id: string +// } +// searchParams: Promise +// } + +// export default async function TechVendorItemsPage(props: IndexPageProps) { +// const resolvedParams = await props.params +// const id = resolvedParams.id + +// const idAsNumber = Number(id) + +// // 벤더 정보 가져오기 (벤더 타입 필요) +// const vendorInfo = await getTechVendorById(idAsNumber) +// const vendorType = vendorInfo.data?.techVendorType || "조선" + +// const promises = getVendorItemsByType(idAsNumber, vendorType) + +// // 4) 렌더링 +// return ( +//
+//
+//

+// 공급품목 +//

+//

+// 기술영업 벤더의 공급 가능한 품목을 확인하세요. +//

+//
+// +//
+// +//
+//
+// ) +// } \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/layout.tsx b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..7c389720 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/layout.tsx @@ -0,0 +1,82 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findTechVendorById } from "@/lib/tech-vendors/service" +import { TechVendor } from "@/db/schema/techVendors" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +export const metadata: Metadata = { + title: "Tech Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const vendor: TechVendor | null = await findTechVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "연락처", + href: `/${lng}/evcp/tech-vendors/${id}/info`, + }, + // { + // title: "자재 리스트", + // href: `/${lng}/evcp/tech-vendors/${id}/info/items`, + // }, + // { + // title: "견적 히스토리", + // href: `/${lng}/evcp/tech-vendors/${id}/info/rfq-history`, + // }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

기술영업 벤더 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/page.tsx b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/page.tsx new file mode 100644 index 00000000..a57d6df7 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getTechVendorContacts } from "@/lib/tech-vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/tech-vendors/validations" +import { TechVendorContactsTable } from "@/lib/tech-vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getTechVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..4ed2b39f --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +// import { Separator } from "@/components/ui/separator" +// import { getRfqHistory } from "@/lib/vendors/service" +// import { type SearchParams } from "@/types/table" +// import { getValidFilters } from "@/lib/data-table" +// import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +// import { TechVendorRfqHistoryTable } from "@/lib/tech-vendors/rfq-history-table/rfq-history-table" + +// interface IndexPageProps { +// // Next.js 13 App Router에서 기본으로 주어지는 객체들 +// params: { +// lng: string +// id: string +// } +// searchParams: Promise +// } + +// export default async function RfqHistoryPage(props: IndexPageProps) { +// const resolvedParams = await props.params +// const lng = resolvedParams.lng +// const id = resolvedParams.id + +// const idAsNumber = Number(id) + +// // 2) SearchParams 파싱 (Zod) +// // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 +// const searchParams = await props.searchParams +// const search = searchParamsRfqHistoryCache.parse(searchParams) +// const validFilters = getValidFilters(search.filters) + +// const promises = Promise.all([ +// getRfqHistory({ +// ...search, +// filters: validFilters, +// }, +// idAsNumber) +// ]) + +// // 4) 렌더링 +// return ( +//
+//
+//

+// RFQ History +//

+//

+// 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. +//

+//
+// +//
+// +//
+//
+// ) +// } \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/tech-vendors/page.tsx b/app/[lng]/procurement/(procurement)/tech-vendors/page.tsx new file mode 100644 index 00000000..8f542f59 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/tech-vendors/page.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/tech-vendors/validations" +import { getTechVendors, getTechVendorStatusCounts } from "@/lib/tech-vendors/service" +import { TechVendorsTable } from "@/lib/tech-vendors/table/tech-vendors-table" +import { TechVendorContainer } from "@/components/tech-vendors/tech-vendor-container" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + // 벤더 타입 정의 + const vendorTypes = [ + { id: "all", name: "전체", value: "" }, + { id: "ship", name: "조선", value: "조선" }, + { id: "top", name: "해양TOP", value: "해양TOP" }, + { id: "hull", name: "해양HULL", value: "해양HULL" }, + ] + + const promises = Promise.all([ + getTechVendors({ + ...search, + filters: validFilters, + }), + getTechVendorStatusCounts(), + ]) + + return ( + + + } + > + + + + + + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx b/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx new file mode 100644 index 00000000..a6e00b1b --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" +import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" +import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" +import { DateRangePicker } from "@/components/date-range-picker" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. +

+
+
+
+ + {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} +
+ {/* 수집일 기간 설정: */} + }> + + +
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx b/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx new file mode 100644 index 00000000..3fd7e425 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" +import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" +import { getGeneralEvaluations } from "@/lib/general-check-list/service" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = getGenralEvaluationsSchema.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getGeneralEvaluations({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 협력업체 정기평가 체크리스트 +

+

+ 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx b/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx new file mode 100644 index 00000000..c59de869 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" +import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" +import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsInvestigationCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInvestigation({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Vendor Investigation Management +

+

+ 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/vendor-type/page.tsx b/app/[lng]/procurement/(procurement)/vendor-type/page.tsx new file mode 100644 index 00000000..997c0f82 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendor-type/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/vendor-type/validations" +import { VendorTypesTable } from "@/lib/vendor-type/table/vendorTypes-table" +import { getVendorTypes } from "@/lib/vendor-type/service" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorTypes({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 업체 유형 +

+

+ 업체 유형을 등록하고 관리할 수 있습니다.{" "} + +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..5d5838c6 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorItems } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsItemCache } from "@/lib/vendors/validations" +import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsItemCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorItems({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ 공급품목(패키지) +

+

+ {/* 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. */} +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..7e2cd4f6 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx @@ -0,0 +1,94 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Vendor } from "@/db/schema/vendors" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const vendor: Vendor | null = await findVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "연락처", + href: `/${lng}/evcp/vendors/${id}/info`, + }, + { + title: "공급품목(패키지)", + href: `/${lng}/evcp/vendors/${id}/info/items`, + }, + { + title: "공급품목(자재그룹)", + href: `/${lng}/evcp/vendors/${id}/info/materials`, + }, + { + title: "견적 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, + }, + { + title: "입찰 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/bid-history`, + }, + { + title: "계약 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/contract-history`, + }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

협력업체 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx new file mode 100644 index 00000000..0ebb66ba --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsMaterialCache } from "@/lib/vendors/validations" +import { getVendorMaterials } from "@/lib/vendors/service" +import { VendorMaterialsTable } from "@/lib/vendors/materials-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMaterialCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorMaterials({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ 공급품목(자재 그룹) +

+

+ {/* 딜리버리가 가능한 공급품목(자재 그룹)을 확인할 수 있습니다. */} +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx new file mode 100644 index 00000000..6279e924 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorContacts } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/vendors/validations" +import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..c7f8f8b6 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getRfqHistory } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqHistoryPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqHistoryCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqHistory({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ RFQ History +

+

+ 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/page.tsx b/app/[lng]/procurement/(procurement)/vendors/page.tsx new file mode 100644 index 00000000..52af0709 --- /dev/null +++ b/app/[lng]/procurement/(procurement)/vendors/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + +import { searchParamsCache } from "@/lib/vendors/validations" +import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" +import { VendorsTable } from "@/lib/vendors/table/vendors-table" +import { Ellipsis } from "lucide-react" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendors({ + ...search, + filters: validFilters, + }), + getVendorStatusCounts(), + ]) + + return ( + + +
+
+
+

+ 협력업체 리스트 +

+

+ 협력업체에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 협력업체 코드를 따올 수 있습니다. +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/procurement/page.tsx b/app/[lng]/procurement/page.tsx new file mode 100644 index 00000000..f9662cb7 --- /dev/null +++ b/app/[lng]/procurement/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next" +import { Suspense } from "react" +import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" +import { LoginFormSHI } from "@/components/login/login-form-shi" + +export const metadata: Metadata = { + title: "eVCP Portal", + description: "", +} + +export default function AuthenticationPage() { + + + return ( + <> + }> + + + + ) +} diff --git a/app/[lng]/sales/(sales)/b-rfq/[id]/final/page.tsx b/app/[lng]/sales/(sales)/b-rfq/[id]/final/page.tsx new file mode 100644 index 00000000..e69de29b diff --git a/app/[lng]/sales/(sales)/b-rfq/[id]/initial/page.tsx b/app/[lng]/sales/(sales)/b-rfq/[id]/initial/page.tsx new file mode 100644 index 00000000..1af65fbc --- /dev/null +++ b/app/[lng]/sales/(sales)/b-rfq/[id]/initial/page.tsx @@ -0,0 +1,52 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { InitialRfqDetailTable } from "@/lib/b-rfq/initial/initial-rfq-detail-table" +import { getInitialRfqDetail } from "@/lib/b-rfq/service" +import { searchParamsInitialRfqDetailCache } from "@/lib/b-rfq/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsInitialRfqDetailCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getInitialRfqDetail({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ Initial RFQ List +

+

+ 설계로부터 받은 RFQ 문서와 구매 RFQ 문서 및 사전 계약자료를 Vendor에 발송하기 위한 RFQ 생성 및 관리하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/b-rfq/[id]/layout.tsx b/app/[lng]/sales/(sales)/b-rfq/[id]/layout.tsx new file mode 100644 index 00000000..8dad7676 --- /dev/null +++ b/app/[lng]/sales/(sales)/b-rfq/[id]/layout.tsx @@ -0,0 +1,87 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import { RfqDashboardView } from "@/db/schema" +import { findBRfqById } from "@/lib/b-rfq/service" + +export const metadata: Metadata = { + title: "견적 RFQ 상세", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqDashboardView | null = await findBRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "견적/입찰 문서관리", + href: `/${lng}/evcp/b-rfq/${id}`, + }, + { + title: "Initial RFQ 발송", + href: `/${lng}/evcp/b-rfq/${id}/initial`, + }, + { + title: "Final RFQ 발송", + href: `/${lng}/evcp/b-rfq/${id}/final`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}` + : "Loading RFQ..."} +

+ +

+ PR발행 전 RFQ를 생성하여 관리하는 화면입니다. +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/b-rfq/[id]/page.tsx b/app/[lng]/sales/(sales)/b-rfq/[id]/page.tsx new file mode 100644 index 00000000..26dc45fb --- /dev/null +++ b/app/[lng]/sales/(sales)/b-rfq/[id]/page.tsx @@ -0,0 +1,53 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations" +import { getRfqAttachments } from "@/lib/b-rfq/service" +import { RfqAttachmentsTable } from "@/lib/b-rfq/attachment/attachment-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqAttachmentsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = getRfqAttachments({ + ...search, + filters: validFilters, + }, idAsNumber) + + // 4) 렌더링 + return ( +
+
+

+ 견적 RFQ 문서관리 +

+

+ 설계로부터 받은 RFQ 문서와 구매 RFQ 문서를 관리하고 Vendor 회신을 점검/관리하는 화면입니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/b-rfq/page.tsx b/app/[lng]/sales/(sales)/b-rfq/page.tsx new file mode 100644 index 00000000..a66d7b58 --- /dev/null +++ b/app/[lng]/sales/(sales)/b-rfq/page.tsx @@ -0,0 +1,79 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsRFQDashboardCache } from "@/lib/b-rfq/validations" +import { getRFQDashboard } from "@/lib/b-rfq/service" +import { RFQDashboardTable } from "@/lib/b-rfq/summary-table/summary-rfq-table" + +export const metadata: Metadata = { + title: "견적 RFQ", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsRFQDashboardCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getRFQDashboard({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + console.log(search, "견적") + + return ( + +
+
+
+

+ 견적 RFQ +

+
+
+
+ + {/* Items처럼 직접 테이블 렌더링 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/basic-contract-template/page.tsx b/app/[lng]/sales/(sales)/basic-contract-template/page.tsx new file mode 100644 index 00000000..adc57ed9 --- /dev/null +++ b/app/[lng]/sales/(sales)/basic-contract-template/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContractTemplates } from "@/lib/basic-contract/service" +import { searchParamsTemplatesCache } from "@/lib/basic-contract/validations" +import { BasicContractTemplateTable } from "@/lib/basic-contract/template/basic-contract-template" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsTemplatesCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBasicContractTemplates({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 기본계약서 템플릿 관리 +

+

+ 기본계약서를 비롯하여 초기 서명이 필요한 문서를 등록하고 편집할 수 있습니다. 활성화된 템플릿이 서명 요청의 리스트에 나타나게 됩니다..{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/basic-contract/page.tsx b/app/[lng]/sales/(sales)/basic-contract/page.tsx new file mode 100644 index 00000000..a043e530 --- /dev/null +++ b/app/[lng]/sales/(sales)/basic-contract/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContracts } from "@/lib/basic-contract/service" +import { searchParamsCache } from "@/lib/basic-contract/validations" +import { BasicContractsTable } from "@/lib/basic-contract/status/basic-contract-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBasicContracts({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 기본계약서 서명 현황 +

+

+ 기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명된 문서들을 다운로드할 수 있습니다. {" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/bid-projects/page.tsx b/app/[lng]/sales/(sales)/bid-projects/page.tsx new file mode 100644 index 00000000..2039e5b2 --- /dev/null +++ b/app/[lng]/sales/(sales)/bid-projects/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBidProjectLists } from "@/lib/bidding-projects/service" +import { searchParamsBidProjectsCache } from "@/lib/bidding-projects/validation" +import { BidProjectsTable } from "@/lib/bidding-projects/table/projects-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsBidProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getBidProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 견적 프로젝트 리스트 +

+

+ SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/bqcbe/page.tsx b/app/[lng]/sales/(sales)/bqcbe/page.tsx new file mode 100644 index 00000000..ae503feb --- /dev/null +++ b/app/[lng]/sales/(sales)/bqcbe/page.tsx @@ -0,0 +1,74 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllCBE } from "@/lib/rfqs/service" +import { searchParamsCBECache } from "@/lib/rfqs/validations" + +import { AllCbeTable } from "@/lib/cbe/table/cbe-table" + +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllCBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/bqtbe/page.tsx b/app/[lng]/sales/(sales)/bqtbe/page.tsx new file mode 100644 index 00000000..4989c235 --- /dev/null +++ b/app/[lng]/sales/(sales)/bqtbe/page.tsx @@ -0,0 +1,72 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..956facd3 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx new file mode 100644 index 00000000..ba7c071c --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next" +import Link from "next/link" +import { ArrowLeft } from "lucide-react" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx new file mode 100644 index 00000000..dd9df563 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..ec894e1c --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx new file mode 100644 index 00000000..dc2a4a2b --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE_BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx new file mode 100644 index 00000000..b1be29db --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsHullCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesHullRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface HullRfqPageProps { + searchParams: Promise +} + +export default async function HullRfqPage(props: HullRfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 해양 HULL용 파라미터 파싱 + const search = searchParamsHullCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 해양 Hull RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesHullRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-해양 Hull RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx new file mode 100644 index 00000000..b7bf9d15 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsShipCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesShipRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface RfqPageProps { + searchParams: Promise +} + +export default async function RfqPage(props: RfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 조선용 파라미터 파싱 + const search = searchParamsShipCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 조선 RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesShipRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-조선 RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx new file mode 100644 index 00000000..f84a9794 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx @@ -0,0 +1,61 @@ +import { searchParamsTopCache } from "@/lib/techsales-rfq/validations" +import { getTechSalesTopRfqsWithJoin } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface HullRfqPageProps { + searchParams: Promise +} + +export default async function HullRfqPage(props: HullRfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 해양 TOP용 파라미터 파싱 + const search = searchParamsTopCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 기술영업 해양 TOP RFQ 데이터를 Promise.all로 감싸서 전달 + const promises = Promise.all([ + getTechSalesTopRfqsWithJoin({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 기술영업-해양 TOP RFQ +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx new file mode 100644 index 00000000..956facd3 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx new file mode 100644 index 00000000..b0711c66 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next" +import Link from "next/link" +import { ArrowLeft } from "lucide-react" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+ +
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx new file mode 100644 index 00000000..dd9df563 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx new file mode 100644 index 00000000..ec894e1c --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/page.tsx b/app/[lng]/sales/(sales)/budgetary/page.tsx new file mode 100644 index 00000000..04550353 --- /dev/null +++ b/app/[lng]/sales/(sales)/budgetary/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/cbe-tech/page.tsx b/app/[lng]/sales/(sales)/cbe-tech/page.tsx new file mode 100644 index 00000000..4dadc58f --- /dev/null +++ b/app/[lng]/sales/(sales)/cbe-tech/page.tsx @@ -0,0 +1,67 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllCBE } from "@/lib/rfqs-tech/service" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { AllCbeTable } from "@/lib/cbe-tech/table/cbe-table" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + + // SearchParams 파싱 (Zod) + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllCBE({ + ...search, + filters: validFilters, + }) + ]) + + return ( + +
+
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/dashboard/page.tsx b/app/[lng]/sales/(sales)/dashboard/page.tsx new file mode 100644 index 00000000..1d61dc16 --- /dev/null +++ b/app/[lng]/sales/(sales)/dashboard/page.tsx @@ -0,0 +1,17 @@ +// app/invalid-access/page.tsx + +export default function InvalidAccessPage() { + return ( +
+

부적절한 접근입니다

+

+ 협력업체(Vendor)가 EVCP 화면에 접속하거나
+ SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. +

+

+ 접근 권한이 없으므로, 다른 화면으로 이동해 주세요. +

+
+ ); + } + \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/email-template/[name]/page.tsx b/app/[lng]/sales/(sales)/email-template/[name]/page.tsx new file mode 100644 index 00000000..cccc10fc --- /dev/null +++ b/app/[lng]/sales/(sales)/email-template/[name]/page.tsx @@ -0,0 +1,26 @@ +import { getTemplateAction } from '@/lib/mail/service'; +import MailTemplateEditorClient from '@/components/mail/mail-template-editor-client'; + +interface EditMailTemplatePageProps { + params: { + name: string; + lng: string; + }; +} + +export default async function EditMailTemplatePage({ params }: EditMailTemplatePageProps) { + const { name: templateName } = await params; + + // 서버에서 초기 템플릿 데이터 가져오기 + const result = await getTemplateAction(templateName); + const initialTemplate = result.success ? result.data : null; + + return ( +
+ +
+ ); +} diff --git a/app/[lng]/sales/(sales)/email-template/page.tsx b/app/[lng]/sales/(sales)/email-template/page.tsx new file mode 100644 index 00000000..1ef3de6c --- /dev/null +++ b/app/[lng]/sales/(sales)/email-template/page.tsx @@ -0,0 +1,19 @@ +import { getTemplatesAction } from '@/lib/mail/service'; +import MailTemplatesClient from '@/components/mail/mail-templates-client'; + +export default async function MailTemplatesPage() { + // 서버에서 초기 데이터 가져오기 + const result = await getTemplatesAction(); + const initialData = result.success ? result.data : []; + + return ( +
+
+

메일 템플릿 관리

+

이메일 템플릿을 관리할 수 있습니다.

+
+ + +
+ ); +} diff --git a/app/[lng]/sales/(sales)/equip-class/page.tsx b/app/[lng]/sales/(sales)/equip-class/page.tsx new file mode 100644 index 00000000..cfa8f133 --- /dev/null +++ b/app/[lng]/sales/(sales)/equip-class/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/equip-class/validation" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" +import { getTagClassists } from "@/lib/equip-class/service" +import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagClassists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 객체 클래스 목록 from S-EDP +

+

+ 객체 클래스 목록을 확인할 수 있습니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/esg-check-list/page.tsx b/app/[lng]/sales/(sales)/esg-check-list/page.tsx new file mode 100644 index 00000000..515751d5 --- /dev/null +++ b/app/[lng]/sales/(sales)/esg-check-list/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getEsgEvaluations } from "@/lib/esg-check-list/service" +import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" +import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = getEsgEvaluationsSchema.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getEsgEvaluations({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ ESG 자가진단표 +

+

+ 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx new file mode 100644 index 00000000..a660c492 --- /dev/null +++ b/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx @@ -0,0 +1,81 @@ +/* IMPORT */ +import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton'; +import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service'; +import { getValidFilters } from '@/lib/data-table'; +import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table'; +import { searchParamsCache } from '@/lib/evaluation-criteria/validations'; +import { Shell } from '@/components/shell'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Suspense } from 'react'; +import { type SearchParams } from '@/types/table'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +interface EvaluationCriteriaPageProps { + searchParams: Promise +} + +// ---------------------------------------------------------------------------------------------------- + +/* REGULAR EVALUATION CRITERIA PAGE */ +async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) { + const searchParams = await props.searchParams; + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + const promises = Promise.all([ + getRegEvalCriteria({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+
+

+ 협력업체 평가기준표 +

+

+ 협력업체 평가에 사용되는 평가기준표를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} + +// ---------------------------------------------------------------------------------------------------- + +/* EXPORT */ +export default EvaluationCriteriaPage; \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx new file mode 100644 index 00000000..088ae75b --- /dev/null +++ b/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" + +import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" +import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" +import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" + +export const metadata: Metadata = { + title: "협력업체 평가 대상 확정", + description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", +} + +interface EvaluationTargetsPageProps { + searchParams: Promise +} + + + +export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsEvaluationTargetsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 (필터에서 가져오거나 기본값 사용) + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getEvaluationTargets({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + + {/* 간소화된 헤더 */} +
+
+
+

+ 협력업체 평가 대상 확정 +

+ + {currentEvaluationYear}년도 + + +
+
+
+ + {/* 메인 테이블 (통계는 테이블 내부로 이동) */} + + } + > + {currentEvaluationYear && + +} + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation/page.tsx b/app/[lng]/sales/(sales)/evaluation/page.tsx new file mode 100644 index 00000000..ead61077 --- /dev/null +++ b/app/[lng]/sales/(sales)/evaluation/page.tsx @@ -0,0 +1,181 @@ +// ================================================================ +// 4. PERIODIC EVALUATIONS PAGE +// ================================================================ + +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" +import { getPeriodicEvaluations } from "@/lib/evaluation/service" +import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" + +export const metadata: Metadata = { + title: "협력업체 정기평가", + description: "협력업체 정기평가 진행 현황을 관리합니다.", +} + +interface PeriodicEvaluationsPageProps { + searchParams: Promise +} + +// 프로세스 안내 팝오버 컴포넌트 +function ProcessGuidePopover() { + return ( + + + + + +
+
+

정기평가 프로세스

+

+ 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. +

+
+
+
+
+ 1 +
+
+

평가 대상 확정

+

평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.

+
+
+
+
+ 2 +
+
+

업체 자료 제출

+

각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.

+
+
+
+
+ 3 +
+
+

평가자 검토

+

지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.

+
+
+
+
+ 4 +
+
+

최종 확정

+

모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.

+
+
+
+
+
+
+ ) +} + +// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 +function getDefaultEvaluationYear() { + return new Date().getFullYear() +} + + + +export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsEvaluationsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters || []) + + // 기본 필터 처리 + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPeriodicEvaluations({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + + {/* 헤더 */} +
+
+
+

+ 협력업체 정기평가 +

+ + {currentEvaluationYear}년도 + +
+
+
+ + {/* 메인 테이블 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/actions.ts b/app/[lng]/sales/(sales)/faq/manage/actions.ts new file mode 100644 index 00000000..bc443a8a --- /dev/null +++ b/app/[lng]/sales/(sales)/faq/manage/actions.ts @@ -0,0 +1,48 @@ +'use server'; + +import { promises as fs } from 'fs'; +import path from 'path'; +import { FaqCategory } from '@/components/faq/FaqCard'; +import { fallbackLng } from '@/i18n/settings'; + +const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); + +export async function updateFaqData(lng: string, newData: FaqCategory[]) { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + const updatedData = { + ...allData, + [lng]: newData + }; + + const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; + await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); + + return { success: true }; + } catch (error) { + console.error('FAQ 데이터 업데이트 중 오류 발생:', error); + return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; + } +} + +export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + return { data: allData[lng] || allData[fallbackLng] || [] }; + } catch (error) { + console.error('FAQ 데이터 읽기 중 오류 발생:', error); + return { data: [] }; + } +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/page.tsx b/app/[lng]/sales/(sales)/faq/manage/page.tsx new file mode 100644 index 00000000..011bbfa4 --- /dev/null +++ b/app/[lng]/sales/(sales)/faq/manage/page.tsx @@ -0,0 +1,38 @@ +import { FaqManager } from '@/components/faq/FaqManager'; +import { getFaqData, updateFaqData } from './actions'; +import { revalidatePath } from 'next/cache'; +import { FaqCategory } from '@/components/faq/FaqCard'; + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqManagePage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const { data } = await getFaqData(lng); + + async function handleSave(newData: FaqCategory[]) { + 'use server'; + await updateFaqData(lng, newData); + revalidatePath(`/${lng}/evcp/faq`); + } + + return ( +
+
+
+
+

FAQ Management

+

+ Manage FAQ categories and items for {lng.toUpperCase()} language. +

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/page.tsx b/app/[lng]/sales/(sales)/faq/page.tsx new file mode 100644 index 00000000..9b62b7e4 --- /dev/null +++ b/app/[lng]/sales/(sales)/faq/page.tsx @@ -0,0 +1,62 @@ +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { faqCategories } from "@/config/faqDataConfig" +import { FaqCard } from "@/components/faq/FaqCard" +import { Button } from "@/components/ui/button" +import { Settings } from "lucide-react" +import Link from "next/link" +import { fallbackLng } from "@/i18n/settings" + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqPage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; + + return ( +
+
+
+
+
+

Frequently Asked Questions

+

+ Find answers to common questions about using the EVCP system. +

+
+ + + +
+ + + + + {localizedFaqCategories.map((category) => ( + + {category.label} + + ))} + + + {localizedFaqCategories.map((category) => ( + + {category.items.map((item, index) => ( + + ))} + + ))} + +
+
+
+ ) +} diff --git a/app/[lng]/sales/(sales)/form-list/page.tsx b/app/[lng]/sales/(sales)/form-list/page.tsx new file mode 100644 index 00000000..a6cf7d9e --- /dev/null +++ b/app/[lng]/sales/(sales)/form-list/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/form-list/validation" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getFormLists } from "@/lib/form-list/service" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getFormLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 레지스터 목록 from S-EDP +

+

+ 협력업체 데이터 입력을 위한 레지스터 목록 리스트입니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/incoterms/page.tsx b/app/[lng]/sales/(sales)/incoterms/page.tsx new file mode 100644 index 00000000..57a19009 --- /dev/null +++ b/app/[lng]/sales/(sales)/incoterms/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { SearchParamsCache } from "@/lib/incoterms/validations"; +import { getIncoterms } from "@/lib/incoterms/service"; +import { IncotermsTable } from "@/lib/incoterms/table/incoterms-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = SearchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getIncoterms({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

인코텀즈 관리

+

+ 인코텀즈(Incoterms)를 등록, 수정, 삭제할 수 있습니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/items-tech/layout.tsx b/app/[lng]/sales/(sales)/items-tech/layout.tsx new file mode 100644 index 00000000..d375059b --- /dev/null +++ b/app/[lng]/sales/(sales)/items-tech/layout.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { ItemTechContainer } from "@/components/items-tech/item-tech-container" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +// Layout 컴포넌트는 서버 컴포넌트입니다 +export default function ItemsShipLayout({ + children, +}: { + children: React.ReactNode +}) { + // 아이템 타입 정의 + const itemTypes = [ + { id: "ship", name: "조선 아이템" }, + { id: "top", name: "해양 TOP" }, + { id: "hull", name: "해양 HULL" }, + ] + + return ( + + + } + > + + {children} + + + + ) +} diff --git a/app/[lng]/sales/(sales)/items-tech/page.tsx b/app/[lng]/sales/(sales)/items-tech/page.tsx new file mode 100644 index 00000000..55ac9c63 --- /dev/null +++ b/app/[lng]/sales/(sales)/items-tech/page.tsx @@ -0,0 +1,67 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations" +import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service" +import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table" +import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table" + +// 대소문자 문제 해결 - 실제 파일명에 맞게 import +import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage({ searchParams }: IndexPageProps) { + const params = await searchParams + const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params) + const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params) + const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params) + const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || []) + const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || []) + const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || []) + + + // URL에서 아이템 타입 가져오기 + const itemType = params.type || "ship" + + return ( +
+ {itemType === "ship" && ( + result)} + /> + )} + + {itemType === "top" && ( + result)} + /> + )} + + {itemType === "hull" && ( + result)} + /> + )} +
+ ) +} diff --git a/app/[lng]/sales/(sales)/items/page.tsx b/app/[lng]/sales/(sales)/items/page.tsx new file mode 100644 index 00000000..0c44bf0a --- /dev/null +++ b/app/[lng]/sales/(sales)/items/page.tsx @@ -0,0 +1,68 @@ +// app/items/page.tsx (업데이트) +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/items/validations" +import { getItems } from "@/lib/items/service" +import { ItemsTable } from "@/lib/items/table/items-table" +import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // pageSize 기반으로 모드 자동 결정 + const isInfiniteMode = search.perPage >= 1_000_000 + + // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 + // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 + const promises = isInfiniteMode + ? undefined + : Promise.all([ + getItems(search), // searchParamsCache의 결과를 그대로 사용 + ]) + + return ( + +
+
+
+

+ 패키지 정보 +

+

+ S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. +

+
+
+ +
+ + }> + {/* DateRangePicker 등 추가 컴포넌트 */} + + + + } + > + {/* 통합된 ItemsTable 컴포넌트 사용 */} + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/layout.tsx b/app/[lng]/sales/(sales)/layout.tsx new file mode 100644 index 00000000..82b53307 --- /dev/null +++ b/app/[lng]/sales/(sales)/layout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Header } from '@/components/layout/Header'; +import { SiteFooter } from '@/components/layout/Footer'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( +
+ {/*
*/} +
+
+
+ {children} +
+
+ +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/menu-list/page.tsx b/app/[lng]/sales/(sales)/menu-list/page.tsx new file mode 100644 index 00000000..84138320 --- /dev/null +++ b/app/[lng]/sales/(sales)/menu-list/page.tsx @@ -0,0 +1,70 @@ +// app/evcp/menu-list/page.tsx + +import { Suspense } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { RefreshCw, Settings } from "lucide-react"; +import { getActiveUsers, getMenuAssignments } from "@/lib/menu-list/servcie"; +import { InitializeButton } from "@/lib/menu-list/table/initialize-button"; +import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; +import { Shell } from "@/components/shell" +import * as React from "react" + +export default async function MenuListPage() { + // 초기 데이터 로드 + const [menusResult, usersResult] = await Promise.all([ + getMenuAssignments(), + getActiveUsers() + ]); + + return ( + +
+
+
+

+ 메뉴 관리 +

+

+ 각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. +

+
+
+ +
+ + + + + + + + 메뉴 리스트 + + + 시스템의 모든 메뉴와 담당자 정보를 확인할 수 있습니다. + {menusResult.data?.length > 0 && ( + + 총 {menusResult.data.length}개의 메뉴 + + )} + + + + 로딩 중...
}> + + + + + + + + ); +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/payment-conditions/page.tsx b/app/[lng]/sales/(sales)/payment-conditions/page.tsx new file mode 100644 index 00000000..b9aedfbb --- /dev/null +++ b/app/[lng]/sales/(sales)/payment-conditions/page.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { type SearchParams } from "@/types/table"; +import { getValidFilters } from "@/lib/data-table"; +import { Shell } from "@/components/shell"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { SearchParamsCache } from "@/lib/payment-terms/validations"; +import { getPaymentTerms } from "@/lib/payment-terms/service"; +import { PaymentTermsTable } from "@/lib/payment-terms/table/payment-terms-table"; + +interface IndexPageProps { + searchParams: Promise; +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams; + const search = SearchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + const promises = Promise.all([ + getPaymentTerms({ + ...search, + filters: validFilters, + }), + ]); + + return ( + +
+
+

결제 조건 관리

+

+ 결제 조건(Payment Terms)을 등록, 수정, 삭제할 수 있습니다. +

+
+
+ }> + + } + > + + +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/po-rfq/page.tsx b/app/[lng]/sales/(sales)/po-rfq/page.tsx new file mode 100644 index 00000000..bdeae25e --- /dev/null +++ b/app/[lng]/sales/(sales)/po-rfq/page.tsx @@ -0,0 +1,61 @@ +import { getPORfqs } from "@/lib/procurement-rfqs/services" +import { searchParamsCache } from "@/lib/procurement-rfqs/validations" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RFQListTable } from "@/lib/procurement-rfqs/table/rfq-table" +import { type SearchParams } from "@/types/table" +import * as React from "react" + +interface RfqPageProps { + searchParams: Promise +} + +export default async function RfqPage(props: RfqPageProps) { + // searchParams를 await하여 resolve + const searchParams = await props.searchParams + + // 파라미터 파싱 + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // RFQ 서버는 기본필터와 고급필터를 분리해서 받으므로 그대로 전달 + const promises = Promise.all([ + getPORfqs({ + ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) + filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) + }) + ]) + + return ( + {/* fullscreen variant 사용 */} + {/* 고정 헤더 영역 */} +
+
+
+

+ 발주용 견적 +

+
+
+
+ + {/* 테이블 영역 - 남은 공간 모두 차지 */} +
+ + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/po/page.tsx b/app/[lng]/sales/(sales)/po/page.tsx new file mode 100644 index 00000000..7868e231 --- /dev/null +++ b/app/[lng]/sales/(sales)/po/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getPOs } from "@/lib/po/service" +import { searchParamsCache } from "@/lib/po/validations" +import { PoListsTable } from "@/lib/po/table/po-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getPOs({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ PO 확인 및 전자서명 +

+

+ 기간계 시스템으로부터 PO를 확인하고 협력업체에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/poa/page.tsx b/app/[lng]/sales/(sales)/poa/page.tsx new file mode 100644 index 00000000..dec5e05b --- /dev/null +++ b/app/[lng]/sales/(sales)/poa/page.tsx @@ -0,0 +1,61 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getChangeOrders } from "@/lib/poa/service" +import { searchParamsCache } from "@/lib/poa/validations" +import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getChangeOrders({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +
+
+
+

+ 변경 PO 확인 및 전자서명 +

+

+ 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. +

+
+
+
+ + }> + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/pq-criteria/[id]/page.tsx b/app/[lng]/sales/(sales)/pq-criteria/[id]/page.tsx new file mode 100644 index 00000000..55b1e9df --- /dev/null +++ b/app/[lng]/sales/(sales)/pq-criteria/[id]/page.tsx @@ -0,0 +1,81 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" +import { notFound } from "next/navigation" + +interface ProjectPageProps { + params: { id: string } + searchParams: Promise +} + +export default async function ProjectPage(props: ProjectPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const projectId = parseInt(id, 10) + + // 유효하지 않은 projectId 확인 + if (isNaN(projectId)) { + notFound() + } + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + const validFilters = getValidFilters(search.filters) + + // 프로젝트별 PQ 데이터 가져오기 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, projectId, false) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/pq-criteria/page.tsx b/app/[lng]/sales/(sales)/pq-criteria/page.tsx new file mode 100644 index 00000000..7785b541 --- /dev/null +++ b/app/[lng]/sales/(sales)/pq-criteria/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + + const validFilters = getValidFilters(search.filters) + + // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, null, true) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/pq/[vendorId]/page.tsx b/app/[lng]/sales/(sales)/pq/[vendorId]/page.tsx new file mode 100644 index 00000000..76bcfe59 --- /dev/null +++ b/app/[lng]/sales/(sales)/pq/[vendorId]/page.tsx @@ -0,0 +1,108 @@ +import * as React from "react" +import { Shell } from "@/components/shell" +import { type SearchParams } from "@/types/table" +import { getPQDataByVendorId, getVendorPQsList, loadGeneralPQData, loadProjectPQAction, loadProjectPQData } from "@/lib/pq/service" +import { Vendor } from "@/db/schema/vendors" +import { findVendorById } from "@/lib/vendors/service" +import VendorPQAdminReview from "@/components/pq/pq-review-detail" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" + +interface IndexPageProps { + params: { + vendorId: string + } + searchParams: Promise +} + +export default async function PQReviewPage(props: IndexPageProps) { + const resolvedParams = await props.params + const vendorId = Number(resolvedParams.vendorId) + + // Fetch the vendor data + const vendor: Vendor | null = await findVendorById(vendorId) + if (!vendor) return
Vendor not found
+ + // Get list of all PQs (general + project-specific) for this vendor + const pqsList = await getVendorPQsList(vendorId) + + // Determine default active PQ to display + // If query param projectId exists, use that, otherwise use general PQ if available + const searchParams = await props.searchParams + const activeProjectId = searchParams.projectId ? Number(searchParams.projectId) : undefined + + // If no projectId query param, default to general PQ or first project PQ + const defaultTabId = activeProjectId ? + `project-${activeProjectId}` : + (pqsList.hasGeneralPq ? 'general' : `project-${pqsList.projectPQs[0]?.projectId}`) + + // Fetch PQ data for the active tab + let pqData; + if (activeProjectId) { + // Get project-specific PQ data + pqData = await getPQDataByVendorId(vendorId, activeProjectId) + } else { + // Get general PQ data + pqData = await getPQDataByVendorId(vendorId) + } + + return ( + + {pqsList.hasGeneralPq || pqsList.projectPQs.length > 0 ? ( + +
+

+ {vendor.vendorName} PQ Review +

+ + + {pqsList.hasGeneralPq && ( + + General PQ Standard + + )} + + {pqsList.projectPQs.map((project) => ( + + {project.projectName} {project.status} + + ))} + +
+ + {/* Tab content for General PQ */} + {pqsList.hasGeneralPq && ( + + + + )} + + {/* Tab content for each Project PQ */} + {pqsList.projectPQs.map((project) => ( + + + + ))} +
+ ) : ( +
+

No PQ submissions found for this vendor

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/pq/page.tsx b/app/[lng]/sales/(sales)/pq/page.tsx new file mode 100644 index 00000000..46b22b12 --- /dev/null +++ b/app/[lng]/sales/(sales)/pq/page.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorsInPQ } from "@/lib/pq/service" +import { searchParamsCache } from "@/lib/vendors/validations" +import { VendorsPQReviewTable } from "@/lib/pq/pq-review-table/vendors-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInPQ({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Pre-Qualification Review +

+

+ 벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. + +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/sales/(sales)/pq_new/[vendorId]/[submissionId]/page.tsx new file mode 100644 index 00000000..28ce3128 --- /dev/null +++ b/app/[lng]/sales/(sales)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -0,0 +1,215 @@ +import * as React from "react" +import { Metadata } from "next" +import Link from "next/link" +import { notFound } from "next/navigation" +import { ArrowLeft } from "lucide-react" +import { Shell } from "@/components/shell" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Separator } from "@/components/ui/separator" +import { getPQById, getPQDataByVendorId } from "@/lib/pq/service" +import { unstable_noStore as noStore } from 'next/cache' +import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper" + +export const metadata: Metadata = { + title: "PQ 검토", + description: "협력업체의 Pre-Qualification 답변을 검토합니다.", +} + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic" + +interface PQReviewPageProps { + params: Promise<{ + vendorId: string; + submissionId: string; + }> +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + // 캐시 비활성화 + noStore() + + const params = await props.params + const vendorId = parseInt(params.vendorId, 10) + const submissionId = parseInt(params.submissionId, 10) + + try { + // PQ Submission 정보 조회 + const pqSubmission = await getPQById(submissionId, vendorId) + + // PQ 데이터 조회 (질문과 답변) + const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined) + + // 프로젝트 정보 (프로젝트 PQ인 경우) + const projectInfo = pqSubmission.projectId ? { + id: pqSubmission.projectId, + projectCode: pqSubmission.projectCode || '', + projectName: pqSubmission.projectName || '', + status: pqSubmission.status, + submittedAt: pqSubmission.submittedAt, + } : null + + // PQ 유형 및 상태 레이블 + const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" : "프로젝트 PQ" + const statusLabel = getStatusLabel(pqSubmission.status) + const statusVariant = getStatusVariant(pqSubmission.status) + + // 수정 가능 여부 (SUBMITTED 상태일 때만 가능) + const canReview = pqSubmission.status === "SUBMITTED" + + return ( + +
+
+ +
+

+ {pqSubmission.vendorName} - {typeLabel} +

+
+ {statusLabel} + {projectInfo && ( + + {projectInfo.projectName} ({projectInfo.projectCode}) + + )} +
+
+
+
+ + {/* 상태별 알림 */} + {pqSubmission.status === "SUBMITTED" && ( + + 제출 완료 + + 협력업체가 {formatDate(pqSubmission.submittedAt)}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다. + + + )} + + {pqSubmission.status === "APPROVED" && ( + + 승인됨 + + {formatDate(pqSubmission.approvedAt)}에 승인되었습니다. + + + )} + + {pqSubmission.status === "REJECTED" && ( + + 거부됨 + + {formatDate(pqSubmission.rejectedAt)}에 거부되었습니다. + {pqSubmission.rejectReason && ( +
+ 사유: {pqSubmission.rejectReason} +
+ )} +
+
+ )} + + + + {/* PQ 검토 컴포넌트 */} + + + PQ 검토 + 협력업체 정보 + + + + + + + +
+

협력업체 정보

+
+
+

업체명

+

{pqSubmission.vendorName}

+
+
+

업체 코드

+

{pqSubmission.vendorCode}

+
+
+

상태

+

{pqSubmission.vendorStatus}

+
+ {/* 필요시 추가 정보 표시 */} +
+
+
+
+
+ ) + } catch (error) { + console.error("Error loading PQ:", error) + notFound() + } +} + +// 상태 레이블 함수 +function getStatusLabel(status: string): string { + switch (status) { + case "REQUESTED": + return "요청됨"; + case "IN_PROGRESS": + return "진행 중"; + case "SUBMITTED": + return "제출됨"; + case "APPROVED": + return "승인됨"; + case "REJECTED": + return "거부됨"; + default: + return status; + } +} + +// 상태별 Badge 스타일 +function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" { + switch (status) { + case "REQUESTED": + return "outline"; + case "IN_PROGRESS": + return "secondary"; + case "SUBMITTED": + return "default"; + case "APPROVED": + return "success"; + case "REJECTED": + return "destructive"; + default: + return "outline"; + } +} + +// 날짜 형식화 함수 +function formatDate(date: Date | null) { + if (!date) return "날짜 없음"; + return new Date(date).toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + }); +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/pq_new/page.tsx b/app/[lng]/sales/(sales)/pq_new/page.tsx new file mode 100644 index 00000000..6598349b --- /dev/null +++ b/app/[lng]/sales/(sales)/pq_new/page.tsx @@ -0,0 +1,96 @@ +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { searchParamsPQReviewCache } from "@/lib/pq/validations" +import { getPQSubmissions } from "@/lib/pq/service" +import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table" + +export const metadata: Metadata = { + title: "PQ 검토/실사 의뢰", + description: "", +} + +interface PQReviewPageProps { + searchParams: Promise +} + +export default async function PQReviewPage(props: PQReviewPageProps) { + const searchParams = await props.searchParams + const search = searchParamsPQReviewCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 디버깅 로그 추가 + console.log("=== PQ Page Debug ==="); + console.log("Raw searchParams:", searchParams); + console.log("Raw basicFilters param:", searchParams.basicFilters); + console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters); + console.log("Parsed search:", search); + console.log("search.filters:", search.filters); + console.log("search.basicFilters:", search.basicFilters); + console.log("search.pqBasicFilters:", search.pqBasicFilters); + console.log("validFilters:", validFilters); + + // 기본 필터 처리 (통일된 이름 사용) + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + console.log("Using search.basicFilters:", basicFilters); + } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) { + // 하위 호환성을 위해 기존 이름도 지원 + basicFilters = search.pqBasicFilters + console.log("Using search.pqBasicFilters:", basicFilters); + } else { + console.log("No basic filters found"); + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + console.log("Final allFilters:", allFilters); + + // 조인 연산자도 통일된 이름 사용 + const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and'; + console.log("Final joinOperator:", joinOperator); + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPQSubmissions({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + +
+
+
+

+ PQ 검토/실사 의뢰 +

+
+
+
+ + {/* Items처럼 직접 테이블 렌더링 */} + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-gtc/page.tsx b/app/[lng]/sales/(sales)/project-gtc/page.tsx new file mode 100644 index 00000000..8e12a489 --- /dev/null +++ b/app/[lng]/sales/(sales)/project-gtc/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getProjectGtcList } from "@/lib/project-gtc/service" +import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" +import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = projectGtcSearchParamsSchema.parse(searchParams) + + const promises = Promise.all([ + getProjectGtcList({ + page: search.page, + perPage: search.perPage, + search: search.search, + sort: search.sort, + }), + ]) + + return ( + +
+
+
+

+ Project GTC +

+

+ 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. + 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. +

+
+
+
+ + }> + {/* 추가 기능이 필요하면 여기에 추가 */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-vendors/page.tsx b/app/[lng]/sales/(sales)/project-vendors/page.tsx new file mode 100644 index 00000000..dcc66071 --- /dev/null +++ b/app/[lng]/sales/(sales)/project-vendors/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" +import { getProjecTAVL } from "@/lib/project-avl/service" +import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchProjectAVLParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjecTAVL({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 프로젝트 AVL 리스트 +

+

+ 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/projects/page.tsx b/app/[lng]/sales/(sales)/projects/page.tsx new file mode 100644 index 00000000..0320f259 --- /dev/null +++ b/app/[lng]/sales/(sales)/projects/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getProjectLists } from "@/lib/projects/service" +import { ProjectsTable } from "@/lib/projects/table/projects-table" +import { searchParamsProjectsCache } from "@/lib/projects/validation" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Project List from S-EDP +

+

+ S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/report/page.tsx b/app/[lng]/sales/(sales)/report/page.tsx new file mode 100644 index 00000000..3efaa7c3 --- /dev/null +++ b/app/[lng]/sales/(sales)/report/page.tsx @@ -0,0 +1,47 @@ +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + + +export default async function IndexPage() { + + + return ( + +
+
+

+ Dashboard +

+

+ 각종 지표 등을 대시보드로 표현하거나 리포트를 출력할 수 있습니다. +

+
+
+ + }> + {/* */} + + + + } + > + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq-tech/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/rfq-tech/[id]/cbe/page.tsx new file mode 100644 index 00000000..84379caf --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq-tech/[id]/cbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { getCBE } from "@/lib/rfqs-tech/service" +import { CbeTable } from "@/lib/rfqs-tech/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq-tech/[id]/layout.tsx b/app/[lng]/sales/(sales)/rfq-tech/[id]/layout.tsx new file mode 100644 index 00000000..0bb62fe0 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq-tech/[id]/layout.tsx @@ -0,0 +1,89 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs-tech/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq-tech/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq-tech/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq-tech/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq-tech/[id]/page.tsx b/app/[lng]/sales/(sales)/rfq-tech/[id]/page.tsx new file mode 100644 index 00000000..007270a1 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq-tech/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs-tech/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs-tech/validations" +import { MatchedVendorsTable } from "@/lib/rfqs-tech/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq-tech/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/rfq-tech/[id]/tbe/page.tsx new file mode 100644 index 00000000..4b226cdc --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq-tech/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { TbeTable } from "@/lib/rfqs-tech/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq-tech/page.tsx b/app/[lng]/sales/(sales)/rfq-tech/page.tsx new file mode 100644 index 00000000..f35b3632 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq-tech/page.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs-tech/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs-tech/service" +import { RfqsTable } from "@/lib/rfqs-tech/table/rfqs-table" +import { getAllOffshoreItems } from "@/lib/items-tech/service" + +interface RfqPageProps { + searchParams: Promise; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + title = "기술영업 해양 RFQ", + description = "기술영업 해양 RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + }), + getRfqStatusCounts(), + getAllOffshoreItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..fb288a98 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq/[id]/cbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCBECache } from "@/lib/rfqs/validations" +import { getCBE } from "@/lib/rfqs/service" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 협력업체에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq/[id]/layout.tsx b/app/[lng]/sales/(sales)/rfq/[id]/layout.tsx new file mode 100644 index 00000000..9a03efa4 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq/[id]/layout.tsx @@ -0,0 +1,89 @@ +import { Metadata } from "next" +import Link from "next/link" +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { RfqViewWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string, id: string } +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{rfq && rfq?.dueDate && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq/[id]/page.tsx b/app/[lng]/sales/(sales)/rfq/[id]/page.tsx new file mode 100644 index 00000000..1a9f4b18 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..76eea302 --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/rfq/page.tsx b/app/[lng]/sales/(sales)/rfq/page.tsx new file mode 100644 index 00000000..3417b0bf --- /dev/null +++ b/app/[lng]/sales/(sales)/rfq/page.tsx @@ -0,0 +1,80 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE, + title = "RFQ", + description = "RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/settings/layout.tsx b/app/[lng]/sales/(sales)/settings/layout.tsx new file mode 100644 index 00000000..6f373567 --- /dev/null +++ b/app/[lng]/sales/(sales)/settings/layout.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "Settings", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Account", + href: `/${lng}/evcp/settings`, + }, + { + title: "Preferences", + href: `/${lng}/evcp/settings/preferences`, + } + + + ] + + + return ( + <> +
+
+
+
+

Settings

+

+ Manage your account settings and preferences. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/sales/(sales)/settings/page.tsx b/app/[lng]/sales/(sales)/settings/page.tsx new file mode 100644 index 00000000..a6eaac90 --- /dev/null +++ b/app/[lng]/sales/(sales)/settings/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/components/settings/account-form" + +export default function SettingsAccountPage() { + return ( +
+
+

Account

+

+ Update your account settings. Set your preferred language and + timezone. +

+
+ + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/settings/preferences/page.tsx b/app/[lng]/sales/(sales)/settings/preferences/page.tsx new file mode 100644 index 00000000..e2a88021 --- /dev/null +++ b/app/[lng]/sales/(sales)/settings/preferences/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/components/settings/appearance-form" + +export default function SettingsAppearancePage() { + return ( +
+
+

Preference

+

+ Customize the preference of the app. +

+
+ + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/system/admin-users/page.tsx b/app/[lng]/sales/(sales)/system/admin-users/page.tsx new file mode 100644 index 00000000..11a9e9fb --- /dev/null +++ b/app/[lng]/sales/(sales)/system/admin-users/page.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" +import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsers({ + ...search, + filters: validFilters, + }), + getUserCountGroupByCompany(), + getUserCountGroupByRole(), + getAllCompanies(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

Vendor Admin User Management

+

+ 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/sales/(sales)/system/layout.tsx b/app/[lng]/sales/(sales)/system/layout.tsx new file mode 100644 index 00000000..7e8f69d0 --- /dev/null +++ b/app/[lng]/sales/(sales)/system/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "System Setting", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "삼성중공업 사용자", + href: `/${lng}/evcp/system`, + }, + { + title: "Roles", + href: `/${lng}/evcp/system/roles`, + }, + { + title: "권한 통제", + href: `/${lng}/evcp/system/permissions`, + }, + { + title: "협력업체 사용자", + href: `/${lng}/evcp/system/admin-users`, + }, + + { + title: "비밀번호 정책", + href: `/${lng}/evcp/system/password-policy`, + }, + + ] + + + return ( + <> +
+
+
+
+

시스템 설정

+

+ 사용자, 롤, 접근 권한을 관리하세요. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/sales/(sales)/system/page.tsx b/app/[lng]/sales/(sales)/system/page.tsx new file mode 100644 index 00000000..fe0a262c --- /dev/null +++ b/app/[lng]/sales/(sales)/system/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import * as React from "react" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllRoles, getUsersEVCP } from "@/lib/users/service" +import { getUserCountGroupByRole } from "@/lib/admin-users/service" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { UserTable } from "@/lib/users/table/users-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function SystemUserPage(props: IndexPageProps) { + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsersEVCP({ + ...search, + filters: validFilters, + }), + getUserCountGroupByRole(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

SHI Users

+

+ 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. +

+
+ + +
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/password-policy/page.tsx b/app/[lng]/sales/(sales)/system/password-policy/page.tsx new file mode 100644 index 00000000..0f14fefe --- /dev/null +++ b/app/[lng]/sales/(sales)/system/password-policy/page.tsx @@ -0,0 +1,63 @@ +// app/admin/password-policy/page.tsx + +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { AlertTriangle } from "lucide-react" +import SecuritySettingsTable from "@/components/system/passwordPolicy" +import { getSecuritySettings } from "@/lib/password-policy/service" + + +export default async function PasswordPolicyPage() { + try { + // 보안 설정 데이터 로드 + const securitySettings = await getSecuritySettings() + + return ( + + } + > +
+
+

협력업체 사용자 비밀번호 정책 설정

+

+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. +

+
+ + +
+
+ ) + } catch (error) { + console.error('Failed to load security settings:', error) + + return ( +
+
+

협력업체 사용자 비밀번호 정책 설정

+

+ 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. +

+
+ + + + + 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. + + +
+ ) + } +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/permissions/page.tsx b/app/[lng]/sales/(sales)/system/permissions/page.tsx new file mode 100644 index 00000000..6aa2b693 --- /dev/null +++ b/app/[lng]/sales/(sales)/system/permissions/page.tsx @@ -0,0 +1,17 @@ +import PermissionsTree from "@/components/system/permissionsTree" +import { Separator } from "@/components/ui/separator" + +export default function PermissionsPage() { + return ( +
+
+

Permissions

+

+ Set permissions to the menu by Role +

+
+ + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/system/roles/page.tsx b/app/[lng]/sales/(sales)/system/roles/page.tsx new file mode 100644 index 00000000..fe074600 --- /dev/null +++ b/app/[lng]/sales/(sales)/system/roles/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/roles/validations" +import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" +import { RolesTable } from "@/lib/roles/table/roles-table" +import { getRolesWithCount } from "@/lib/roles/services" +import { getUsersAll } from "@/lib/users/service" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const search2 = searchParamsCache2.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRolesWithCount({ + ...search, + filters: validFilters, + }), + + + ]) + + + const promises2 = Promise.all([ + getUsersAll({ + ...search2, + filters: validFilters, + }, "evcp"), + ]) + + + return ( + + } + > +
+
+

Role Management

+

+ 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/sales/(sales)/tag-numbering/page.tsx b/app/[lng]/sales/(sales)/tag-numbering/page.tsx new file mode 100644 index 00000000..44695259 --- /dev/null +++ b/app/[lng]/sales/(sales)/tag-numbering/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/tag-numbering/validation" +import { getTagNumbering } from "@/lib/tag-numbering/service" +import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagNumbering({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 태그 타입 목록 from S-EDP +

+

+ 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/tasks/page.tsx b/app/[lng]/sales/(sales)/tasks/page.tsx new file mode 100644 index 00000000..91b946fb --- /dev/null +++ b/app/[lng]/sales/(sales)/tasks/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Shell } from "@/components/shell" + +import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" +import { TasksTable } from "@/lib/tasks/table/tasks-table" +import { + getTaskPriorityCounts, + getTasks, + getTaskStatusCounts, +} from "@/lib/tasks/service" +import { searchParamsCache } from "@/lib/tasks/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTasks({ + ...search, + filters: validFilters, + }), + getTaskStatusCounts(), + getTaskPriorityCounts(), + ]) + + return ( + + }> + + + + } + > + + + + ) +} diff --git a/app/[lng]/sales/(sales)/tbe-tech/page.tsx b/app/[lng]/sales/(sales)/tbe-tech/page.tsx new file mode 100644 index 00000000..17b01ce2 --- /dev/null +++ b/app/[lng]/sales/(sales)/tbe-tech/page.tsx @@ -0,0 +1,67 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { AllTbeTable } from "@/lib/tbe-tech/table/tbe-table" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + }) + ]) + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tbe/page.tsx b/app/[lng]/sales/(sales)/tbe/page.tsx new file mode 100644 index 00000000..1a7fdf86 --- /dev/null +++ b/app/[lng]/sales/(sales)/tbe/page.tsx @@ -0,0 +1,113 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +// 타입별 페이지 설명 구성 (Budgetary 제외) +const typeConfig: Record = { + "purchase": { + title: "Purchase RFQ Technical Bid Evaluation", + description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE + }, + "purchase-budgetary": { + title: "Purchase Budgetary RFQ Technical Bid Evaluation", + description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE_BUDGETARY + } +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + // 기본값으로 'purchase' 사용 + const typeParam = searchParams?.type as string || 'purchase' + + // 유효한 타입인지 확인하고 기본값 설정 + const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' + const rfqType = typeConfig[validType].rfqType + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + }) + ]) + + // 페이지 경로 생성 함수 - 단순화 + const getTabUrl = (type: string) => { + return `/${lng}/evcp/tbe?type=${type}`; + } + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 협력업체에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + {/* 타입 선택 탭 (Budgetary 제외) */} + + + + Purchase + + + Purchase Budgetary + + + +
+

+ {typeConfig[validType].description} +

+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx b/app/[lng]/sales/(sales)/tech-project-avl/page.tsx new file mode 100644 index 00000000..d942c5c5 --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-project-avl/page.tsx @@ -0,0 +1,85 @@ +import * as React from "react" +import { redirect } from "next/navigation" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { SearchParams } from "@/types/table" +import { searchParamsCache } from "@/lib/tech-project-avl/validations" +import { Skeleton } from "@/components/ui/skeleton" +import { Shell } from "@/components/shell" +import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table" +import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Ellipsis } from "lucide-react" + +export interface PageProps { + params: Promise<{ lng: string }> + searchParams: Promise +} + +export default async function AcceptedQuotationsPage({ + params, + searchParams, +}: PageProps) { + const { lng } = await params + + const session = await getServerSession(authOptions) + if (!session) { + redirect(`/${lng}/auth/signin`) + } + + const search = await searchParams + const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search) + const validFilters = getValidFilters(filters ?? []) + + const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({ + page, + perPage: perPage ?? 10, + sort, + search: searchText, + filters: validFilters, + }) + + return ( + +
+
+
+

+ 승인된 견적서(해양TOP,HULL) +

+

+ 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다. +

+
+
+
+ + }> + {/* Date range picker can be added here if needed */} + + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/tech-vendor-candidates/page.tsx b/app/[lng]/sales/(sales)/tech-vendor-candidates/page.tsx new file mode 100644 index 00000000..3923863a --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-vendor-candidates/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/tech-vendor-candidates/service" +import { searchParamsTechCandidateCache } from "@/lib/tech-vendor-candidates/validations" +import { VendorCandidateTable as TechVendorCandidateTable } from "@/lib/tech-vendor-candidates/table/candidates-table" +import { DateRangePicker } from "@/components/date-range-picker" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsTechCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. +

+
+
+
+ + {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} +
+ {/* 수집일 기간 설정: */} + }> + + +
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/items/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..69c36576 --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/items/page.tsx @@ -0,0 +1,48 @@ +// import { Separator } from "@/components/ui/separator" +// import { getTechVendorById, getVendorItemsByType } from "@/lib/tech-vendors/service" +// import { type SearchParams } from "@/types/table" +// import { TechVendorItemsTable } from "@/lib/tech-vendors/items-table/item-table" + +// interface IndexPageProps { +// // Next.js 13 App Router에서 기본으로 주어지는 객체들 +// params: { +// lng: string +// id: string +// } +// searchParams: Promise +// } + +// export default async function TechVendorItemsPage(props: IndexPageProps) { +// const resolvedParams = await props.params +// const id = resolvedParams.id + +// const idAsNumber = Number(id) + +// // 벤더 정보 가져오기 (벤더 타입 필요) +// const vendorInfo = await getTechVendorById(idAsNumber) +// const vendorType = vendorInfo.data?.techVendorType || "조선" + +// const promises = getVendorItemsByType(idAsNumber, vendorType) + +// // 4) 렌더링 +// return ( +//
+//
+//

+// 공급품목 +//

+//

+// 기술영업 벤더의 공급 가능한 품목을 확인하세요. +//

+//
+// +//
+// +//
+//
+// ) +// } \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..7c389720 --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx @@ -0,0 +1,82 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findTechVendorById } from "@/lib/tech-vendors/service" +import { TechVendor } from "@/db/schema/techVendors" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +export const metadata: Metadata = { + title: "Tech Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const vendor: TechVendor | null = await findTechVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "연락처", + href: `/${lng}/evcp/tech-vendors/${id}/info`, + }, + // { + // title: "자재 리스트", + // href: `/${lng}/evcp/tech-vendors/${id}/info/items`, + // }, + // { + // title: "견적 히스토리", + // href: `/${lng}/evcp/tech-vendors/${id}/info/rfq-history`, + // }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

기술영업 벤더 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx new file mode 100644 index 00000000..a57d6df7 --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getTechVendorContacts } from "@/lib/tech-vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/tech-vendors/validations" +import { TechVendorContactsTable } from "@/lib/tech-vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getTechVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..4ed2b39f --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +// import { Separator } from "@/components/ui/separator" +// import { getRfqHistory } from "@/lib/vendors/service" +// import { type SearchParams } from "@/types/table" +// import { getValidFilters } from "@/lib/data-table" +// import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +// import { TechVendorRfqHistoryTable } from "@/lib/tech-vendors/rfq-history-table/rfq-history-table" + +// interface IndexPageProps { +// // Next.js 13 App Router에서 기본으로 주어지는 객체들 +// params: { +// lng: string +// id: string +// } +// searchParams: Promise +// } + +// export default async function RfqHistoryPage(props: IndexPageProps) { +// const resolvedParams = await props.params +// const lng = resolvedParams.lng +// const id = resolvedParams.id + +// const idAsNumber = Number(id) + +// // 2) SearchParams 파싱 (Zod) +// // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 +// const searchParams = await props.searchParams +// const search = searchParamsRfqHistoryCache.parse(searchParams) +// const validFilters = getValidFilters(search.filters) + +// const promises = Promise.all([ +// getRfqHistory({ +// ...search, +// filters: validFilters, +// }, +// idAsNumber) +// ]) + +// // 4) 렌더링 +// return ( +//
+//
+//

+// RFQ History +//

+//

+// 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. +//

+//
+// +//
+// +//
+//
+// ) +// } \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/page.tsx new file mode 100644 index 00000000..8f542f59 --- /dev/null +++ b/app/[lng]/sales/(sales)/tech-vendors/page.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/tech-vendors/validations" +import { getTechVendors, getTechVendorStatusCounts } from "@/lib/tech-vendors/service" +import { TechVendorsTable } from "@/lib/tech-vendors/table/tech-vendors-table" +import { TechVendorContainer } from "@/components/tech-vendors/tech-vendor-container" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + // 벤더 타입 정의 + const vendorTypes = [ + { id: "all", name: "전체", value: "" }, + { id: "ship", name: "조선", value: "조선" }, + { id: "top", name: "해양TOP", value: "해양TOP" }, + { id: "hull", name: "해양HULL", value: "해양HULL" }, + ] + + const promises = Promise.all([ + getTechVendors({ + ...search, + filters: validFilters, + }), + getTechVendorStatusCounts(), + ]) + + return ( + + + } + > + + + + + + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx b/app/[lng]/sales/(sales)/vendor-candidates/page.tsx new file mode 100644 index 00000000..a6e00b1b --- /dev/null +++ b/app/[lng]/sales/(sales)/vendor-candidates/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" +import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" +import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" +import { DateRangePicker } from "@/components/date-range-picker" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. +

+
+
+
+ + {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} +
+ {/* 수집일 기간 설정: */} + }> + + +
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendor-check-list/page.tsx b/app/[lng]/sales/(sales)/vendor-check-list/page.tsx new file mode 100644 index 00000000..3fd7e425 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendor-check-list/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" +import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" +import { getGeneralEvaluations } from "@/lib/general-check-list/service" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = getGenralEvaluationsSchema.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getGeneralEvaluations({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 협력업체 정기평가 체크리스트 +

+

+ 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/vendor-investigation/page.tsx b/app/[lng]/sales/(sales)/vendor-investigation/page.tsx new file mode 100644 index 00000000..c59de869 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendor-investigation/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" +import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" +import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsInvestigationCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInvestigation({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Vendor Investigation Management +

+

+ 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/vendor-type/page.tsx b/app/[lng]/sales/(sales)/vendor-type/page.tsx new file mode 100644 index 00000000..997c0f82 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendor-type/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/vendor-type/validations" +import { VendorTypesTable } from "@/lib/vendor-type/table/vendorTypes-table" +import { getVendorTypes } from "@/lib/vendor-type/service" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorTypes({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ 업체 유형 +

+

+ 업체 유형을 등록하고 관리할 수 있습니다.{" "} + +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/(sales)/vendors/[id]/info/items/page.tsx b/app/[lng]/sales/(sales)/vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..5d5838c6 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendors/[id]/info/items/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorItems } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsItemCache } from "@/lib/vendors/validations" +import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsItemCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorItems({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ 공급품목(패키지) +

+

+ {/* 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. */} +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendors/[id]/info/layout.tsx b/app/[lng]/sales/(sales)/vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..7e2cd4f6 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendors/[id]/info/layout.tsx @@ -0,0 +1,94 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Vendor } from "@/db/schema/vendors" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string , id: string} +}) { + + // 1) URL 파라미터에서 id 추출, Number로 변환 + const resolvedParams = await params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + // 2) DB에서 해당 협력업체 정보 조회 + const vendor: Vendor | null = await findVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "연락처", + href: `/${lng}/evcp/vendors/${id}/info`, + }, + { + title: "공급품목(패키지)", + href: `/${lng}/evcp/vendors/${id}/info/items`, + }, + { + title: "공급품목(자재그룹)", + href: `/${lng}/evcp/vendors/${id}/info/materials`, + }, + { + title: "견적 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, + }, + { + title: "입찰 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/bid-history`, + }, + { + title: "계약 히스토리", + href: `/${lng}/evcp/vendors/${id}/info/contract-history`, + }, + ] + + return ( + <> +
+
+
+ {/* RFQ 목록으로 돌아가는 링크 추가 */} +
+ + + +
+
+ {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

협력업체 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendors/[id]/info/materials/page.tsx b/app/[lng]/sales/(sales)/vendors/[id]/info/materials/page.tsx new file mode 100644 index 00000000..0ebb66ba --- /dev/null +++ b/app/[lng]/sales/(sales)/vendors/[id]/info/materials/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsMaterialCache } from "@/lib/vendors/validations" +import { getVendorMaterials } from "@/lib/vendors/service" +import { VendorMaterialsTable } from "@/lib/vendors/materials-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMaterialCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorMaterials({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ 공급품목(자재 그룹) +

+

+ {/* 딜리버리가 가능한 공급품목(자재 그룹)을 확인할 수 있습니다. */} +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendors/[id]/info/page.tsx b/app/[lng]/sales/(sales)/vendors/[id]/info/page.tsx new file mode 100644 index 00000000..6279e924 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendors/[id]/info/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorContacts } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/vendors/validations" +import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/sales/(sales)/vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..c7f8f8b6 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getRfqHistory } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqHistoryPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqHistoryCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqHistory({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ RFQ History +

+

+ 협력업체의 RFQ 참여 이력을 확인할 수 있습니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendors/page.tsx b/app/[lng]/sales/(sales)/vendors/page.tsx new file mode 100644 index 00000000..52af0709 --- /dev/null +++ b/app/[lng]/sales/(sales)/vendors/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + +import { searchParamsCache } from "@/lib/vendors/validations" +import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" +import { VendorsTable } from "@/lib/vendors/table/vendors-table" +import { Ellipsis } from "lucide-react" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendors({ + ...search, + filters: validFilters, + }), + getVendorStatusCounts(), + ]) + + return ( + + +
+
+
+

+ 협력업체 리스트 +

+

+ 협력업체에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 협력업체 코드를 따올 수 있습니다. +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/sales/page.tsx b/app/[lng]/sales/page.tsx new file mode 100644 index 00000000..f9662cb7 --- /dev/null +++ b/app/[lng]/sales/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next" +import { Suspense } from "react" +import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" +import { LoginFormSHI } from "@/components/login/login-form-shi" + +export const metadata: Metadata = { + title: "eVCP Portal", + description: "", +} + +export default function AuthenticationPage() { + + + return ( + <> + }> + + + + ) +} diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts index dfe3d830..761c06f1 100644 --- a/app/api/auth/[...nextauth]/saml/provider.ts +++ b/app/api/auth/[...nextauth]/saml/provider.ts @@ -97,7 +97,8 @@ export function SAMLProvider(options: SAMLProviderOptions) { name: userData.name, companyId: undefined, techCompanyId: undefined, - domain: userData.domain + domain: 'pending', + deptName:userData.deptName }; debugLog('User create data:', userCreateData); diff --git a/app/api/menu-assignments/active-status/route.ts b/app/api/menu-assignments/active-status/route.ts new file mode 100644 index 00000000..bd31e6b8 --- /dev/null +++ b/app/api/menu-assignments/active-status/route.ts @@ -0,0 +1,32 @@ +// app/api/menu-assignments/active-status/route.ts + +import { NextResponse } from "next/server" +import db from "@/db/db" +import { menuAssignments } from "@/db/schema" +import { eq } from "drizzle-orm" + +export async function GET() { + try { + // 모든 메뉴의 활성 상태를 조회 + const menus = await db + .select({ + menuPath: menuAssignments.menuPath, + isActive: menuAssignments.isActive, + }) + .from(menuAssignments) + + // 객체 형태로 변환 { "/evcp/menu-path": true, "/evcp/other-path": false } + const activeMenusMap = menus.reduce((acc, menu) => { + acc[menu.menuPath] = menu.isActive + return acc + }, {} as Record) + + return NextResponse.json(activeMenusMap) + } catch (error) { + console.error("Error fetching menu active status:", error) + return NextResponse.json( + { error: "Failed to fetch menu status" }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/components/layout/GroupedMenuRender.tsx b/components/layout/GroupedMenuRender.tsx index e2a5a225..9006c85d 100644 --- a/components/layout/GroupedMenuRender.tsx +++ b/components/layout/GroupedMenuRender.tsx @@ -4,6 +4,7 @@ import { NavigationMenuLink } from "@/components/ui/navigation-menu"; import { cn } from "@/lib/utils"; import * as LucideIcons from "lucide-react"; import { MenuItem } from '@/config/menuConfig'; +import { filterActiveAdditionalMenus } from "@/hooks/use-active-menus"; type GroupedMenuItems = { [key: string]: MenuItem[]; @@ -12,9 +13,15 @@ type GroupedMenuItems = { interface GroupedMenuRendererProps { items: MenuItem[]; lng: string; + activeMenus?: Record; // 활성 메뉴 상태 추가 } -const GroupedMenuRenderer = ({ items, lng }: GroupedMenuRendererProps) => { +const GroupedMenuRenderer = ({ items, lng, activeMenus = {} }: GroupedMenuRendererProps) => { + // 활성 메뉴만 필터링 (activeMenus가 빈 객체면 모든 메뉴 표시) + const filteredItems = Object.keys(activeMenus).length > 0 + ? filterActiveAdditionalMenus(items, activeMenus) + : items; + // 그룹별로 아이템 분류 const groupItems = (items: MenuItem[]): GroupedMenuItems => { return items.reduce((groups, item) => { @@ -27,32 +34,44 @@ const GroupedMenuRenderer = ({ items, lng }: GroupedMenuRendererProps) => { }, {} as GroupedMenuItems); }; - const groupedItems = groupItems(items); + const groupedItems = groupItems(filteredItems); const groups = Object.keys(groupedItems); + // 활성 메뉴가 없으면 아무것도 렌더링하지 않음 + if (filteredItems.length === 0) { + return ( +
+

+ 사용 가능한 메뉴가 없습니다. +

+
+ ); + } + return (
- {groups.map((groupName, index) => ( -
- {groupName !== 'default' && ( -

{groupName}

- )} -
- {groupedItems[groupName].map((item) => ( - - ))} + {groups.map((groupName, index) => { + // 빈 그룹은 건너뛰기 + if (groupedItems[groupName].length === 0) return null; + + return ( +
+ {groupName !== 'default' && ( +

{groupName}

+ )} +
+ {groupedItems[groupName].map((item) => ( + + ))} +
-
- ))} + ); + })}
); }; const MenuListItem = ({ item, lng }: { item: MenuItem; lng: string }) => { - - - - return ( word[0]?.toUpperCase()) .join(""); - - const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); // 모바일 메뉴 상태 + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen); }; - const isPartnerRoute = pathname?.includes("/partners"); + // 도메인별 메뉴 및 브랜딩 정보 가져오기 + const getDomainConfig = (pathname: string) => { + if (pathname?.includes("/partners")) { + return { + main: mainNavVendor, + additional: additionalNavVendor, + logoHref: `/${lng}/partners`, + brandName: "eVCP Partners", + basePath: `/${lng}/partners` + }; + } + + if (pathname?.includes("/procurement")) { + return { + main: procurementNav, + additional: additional2Nav, + logoHref: `/${lng}/procurement`, + brandName: "eVCP 구매관리", + basePath: `/${lng}/procurement` + }; + } + + if (pathname?.includes("/sales")) { + return { + main: salesNav, + additional: additional2Nav, + logoHref: `/${lng}/sales`, + brandName: "eVCP 기술영업", + basePath: `/${lng}/sales` + }; + } + + if (pathname?.includes("/engineering")) { + return { + main: engineeringNav, + additional: additional2Nav, + logoHref: `/${lng}/engineering`, + brandName: "eVCP 설계관리", + basePath: `/${lng}/engineering` + }; + } + + // 기본값: /evcp (전체 메뉴) + return { + main: mainNav, + additional: additionalNav, + logoHref: `/${lng}/evcp`, + brandName: "eVCP 삼성중공업", + basePath: `/${lng}/evcp` + }; + }; - const main = isPartnerRoute ? mainNavVendor : mainNav; - const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + const { main: originalMain, additional: originalAdditional, logoHref, brandName, basePath } = getDomainConfig(pathname); - const basePath = `/${lng}${isPartnerRoute ? "/partners" : "/evcp"}`; + // 활성 메뉴만 필터링 (로딩 중이거나 에러 시에는 모든 메뉴 표시) + const main = isLoading ? originalMain : filterActiveMenus(originalMain, activeMenus); + const additional = isLoading ? originalAdditional : filterActiveAdditionalMenus(originalAdditional, activeMenus); return ( <> - {/*
*/}
@@ -88,9 +150,9 @@ export function Header() { Toggle Menu - {/* 로고 영역 - 항상 표시 */} + {/* 로고 영역 - 도메인별 브랜딩 */}
- + - {isPartnerRoute - ? "eVCP Partners" - : pathname?.includes("/evcp") - ? "eVCP 삼성중공업" - : "eVCP"} + {brandName}
- {/* 네비게이션 메뉴 - 내부 스크롤 적용, 드롭다운은 제약 없이 표시 */} + {/* 네비게이션 메뉴 - 도메인별 활성화된 메뉴만 표시 */}
- {/* NavigationMenu는 z-index를 높게 설정하여 드롭다운이 제대로 표시되도록 함 */} - {/* 스크롤 가능한 메뉴 리스트 컨테이너 */}
{main.map((section: MenuSection) => ( @@ -124,7 +180,11 @@ export function Header() { {/* 그룹핑이 필요한 메뉴의 경우 GroupedMenuRenderer 사용 */} {section.useGrouping ? ( - + ) : ( @@ -144,7 +204,7 @@ export function Header() { ))} - {/* 추가 네비게이션 항목 */} + {/* 추가 네비게이션 항목 - 도메인별 활성화된 것만 */} {additional.map((item) => ( @@ -164,7 +224,7 @@ export function Header() {
- {/* 우측 영역 - 고정 너비와 우선순위로 항상 표시되도록 함 */} + {/* 우측 영역 */}
{/* 데스크탑에서는 CommandMenu, 모바일에서는 검색 아이콘만 */}
@@ -177,11 +237,10 @@ export function Header() { {/* 알림 버튼 */} - {/* 사용자 메뉴 (DropdownMenu) */} + {/* 사용자 메뉴 */} @@ -207,8 +266,16 @@ export function Header() {
- {/* 모바일 메뉴 */} - {isMobileMenuOpen && } + {/* 모바일 메뉴 - 도메인별 활성화된 메뉴만 전달 */} + {isMobileMenuOpen && ( + + )}
); diff --git a/components/layout/MobileMenu.tsx b/components/layout/MobileMenu.tsx index 2e70aeba..dc02d2e3 100644 --- a/components/layout/MobileMenu.tsx +++ b/components/layout/MobileMenu.tsx @@ -5,29 +5,42 @@ import * as React from "react"; import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; -import { MenuSection, mainNav, additionalNav, MenuItem, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; +import { MenuSection, MenuItem } from "@/config/menuConfig"; import { cn } from "@/lib/utils"; import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; import { Button } from "@/components/ui/button"; +import { filterActiveMenus, filterActiveAdditionalMenus } from "@/hooks/use-active-menus"; interface MobileMenuProps { lng: string; onClose: () => void; + activeMenus?: Record; + domainMain?: MenuSection[]; // 헤더에서 계산된 도메인별 메인 메뉴 + domainAdditional?: MenuItem[]; // 헤더에서 계산된 도메인별 추가 메뉴 } -export function MobileMenu({ lng, onClose }: MobileMenuProps) { +export function MobileMenu({ + lng, + onClose, + activeMenus = {}, + domainMain = [], + domainAdditional = [] +}: MobileMenuProps) { const router = useRouter(); - - + const handleLinkClick = (href: string) => { router.push(href); onClose(); }; - const pathname = usePathname(); - const isPartnerRoute = pathname?.includes("/partners"); - const main = isPartnerRoute ? mainNavVendor : mainNav; - const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + // 활성 메뉴만 필터링 (activeMenus가 빈 객체면 모든 메뉴 표시) + const main = Object.keys(activeMenus).length > 0 + ? filterActiveMenus(domainMain, activeMenus) + : domainMain; + + const additional = Object.keys(activeMenus).length > 0 + ? filterActiveAdditionalMenus(domainAdditional, activeMenus) + : domainAdditional; return ( @@ -36,42 +49,44 @@ export function MobileMenu({ lng, onClose }: MobileMenuProps) {
-
+ ) : ( + {placeholder} + )} + + + + + + + + + 검색 결과가 없습니다. + + {/* 담당자 없음 옵션 */} + handleSelect(null)} + className="flex items-center gap-2" + > + +
+ 담당자 없음 + + 담당자를 지정하지 않습니다 + +
+
+ + {/* 사용자 목록 */} + {availableUsers.map((user) => ( + handleSelect(user.id)} + className="flex items-center gap-2" + disabled={user.id === otherManagerId && user.id !== currentManagerId} + > + +
+ {user.name} + + {user.email} + +
+ {user.id === otherManagerId && user.id !== currentManagerId && ( + + 다른 담당자로 선택됨 + + )} +
+ ))} +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/lib/menu-list/table/menu-list-table.tsx b/lib/menu-list/table/menu-list-table.tsx new file mode 100644 index 00000000..097be082 --- /dev/null +++ b/lib/menu-list/table/menu-list-table.tsx @@ -0,0 +1,280 @@ +// app/evcp/menu-list/components/menu-list-table.tsx + +"use client"; + +import { useState, useMemo } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Button } from "@/components/ui/button"; +import { Search, Filter, ExternalLink } from "lucide-react"; +import { toast } from "sonner"; +import { ManagerSelect } from "./manager-select"; +import { toggleMenuActive } from "../servcie"; + +interface MenuAssignment { + id: number; + menuPath: string; + menuTitle: string; + menuDescription?: string | null; + menuGroup?: string | null; + sectionTitle: string; + domain: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + manager1Id?: number | null; + manager2Id?: number | null; + manager1Name?: string | null; + manager1Email?: string | null; + manager2Name?: string | null; + manager2Email?: string | null; +} + +interface User { + id: number; + name: string; + email: string; + domain: string; +} + +interface MenuListTableProps { + initialMenus: MenuAssignment[]; + initialUsers: User[]; +} + +export function MenuListTable({ initialMenus, initialUsers }: MenuListTableProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [domainFilter, setDomainFilter] = useState("all"); + const [sectionFilter, setSectionFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + + // 필터링된 메뉴 데이터 + const filteredMenus = useMemo(() => { + return initialMenus.filter((menu) => { + const matchesSearch = + menu.menuTitle.toLowerCase().includes(searchQuery.toLowerCase()) || + menu.menuPath.toLowerCase().includes(searchQuery.toLowerCase()) || + menu.sectionTitle.toLowerCase().includes(searchQuery.toLowerCase()) || + (menu.menuDescription?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false); + + const matchesDomain = domainFilter === "all" || menu.domain === domainFilter; + const matchesSection = sectionFilter === "all" || menu.sectionTitle === sectionFilter; + const matchesStatus = statusFilter === "all" || + (statusFilter === "active" && menu.isActive) || + (statusFilter === "inactive" && !menu.isActive); + + return matchesSearch && matchesDomain && matchesSection && matchesStatus; + }); + }, [initialMenus, searchQuery, domainFilter, sectionFilter, statusFilter]); + + // 섹션 리스트 추출 + const sections = useMemo(() => { + const sectionSet = new Set(initialMenus.map(menu => menu.sectionTitle)); + return Array.from(sectionSet).sort(); + }, [initialMenus]); + + // 도메인별 사용자 필터링 + const getFilteredUsers = (domain: string) => { + return initialUsers.filter(user => user.domain === domain); + }; + + // 메뉴 활성화/비활성화 토글 + const handleToggleActive = async (menuPath: string, isActive: boolean) => { + try { + const result = await toggleMenuActive(menuPath, isActive); + if (result.success) { + toast.success(result.message); + } else { + toast.error(result.message); + } + } catch (error) { + toast.error("메뉴 상태 변경 중 오류가 발생했습니다."); + } + }; + + return ( +
+ {/* 필터 영역 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+
+ +
+ + + + + +
+
+ + {/* 결과 요약 */} +
+ + 총 {filteredMenus.length}개의 메뉴 + {searchQuery && ` (${initialMenus.length}개 중 검색 결과)`} + +
+ + {/* 테이블 */} +
+ + + + 상태 + 메뉴 정보 + 도메인 + 담당자 1 + 담당자 2 + {/* 동작 */} + + + + {filteredMenus.length === 0 ? ( + + + 조건에 맞는 메뉴가 없습니다. + + + ) : ( + filteredMenus.map((menu) => { + const domainUsers = getFilteredUsers(menu.domain); + + return ( + + + handleToggleActive(menu.menuPath, checked)} + /> + + + +
+
+ {menu.menuTitle} + + {menu.sectionTitle} + + {menu.menuGroup && ( + + {menu.menuGroup} + + )} +
+
+ {menu.menuPath} +
+ {menu.menuDescription && ( +
+ {menu.menuDescription} +
+ )} +
+
+ + + + {menu.domain.toUpperCase()} + + + + + + + + + + + + {/* + + */} +
+ ); + }) + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/lib/users/access-control/assign-domain-dialog.tsx b/lib/users/access-control/assign-domain-dialog.tsx new file mode 100644 index 00000000..fda06b28 --- /dev/null +++ b/lib/users/access-control/assign-domain-dialog.tsx @@ -0,0 +1,253 @@ +// components/assign-domain-dialog.tsx +"use client" + +import * as React from "react" +import { User } from "@/db/schema/users" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Users, Loader2, CheckCircle } from "lucide-react" +import { toast } from "sonner" +import { assignUsersDomain } from "../service" + +interface AssignDomainDialogProps { + users: User[] +} + +// 도메인 옵션 정의 +const domainOptions = [ + { + value: "pending", + label: "승인 대기", + description: "신규 사용자 (기본 메뉴만)", + color: "yellow", + icon: "🟡" + }, + { + value: "evcp", + label: "전체 시스템", + description: "모든 메뉴 접근 (관리자급)", + color: "blue", + icon: "🔵" + }, + { + value: "procurement", + label: "구매관리팀", + description: "구매, 협력업체, 계약 관리", + color: "green", + icon: "🟢" + }, + { + value: "sales", + label: "기술영업팀", + description: "기술영업, 견적, 프로젝트 관리", + color: "purple", + icon: "🟣" + }, + { + value: "engineering", + label: "설계관리팀", + description: "설계, 기술평가, 문서 관리", + color: "orange", + icon: "🟠" + }, + { + value: "partners", + label: "협력업체", + description: "외부 협력업체용 기능", + color: "indigo", + icon: "🟦" + } +] + +export function AssignDomainDialog({ users }: AssignDomainDialogProps) { + const [open, setOpen] = React.useState(false) + const [selectedDomain, setSelectedDomain] = React.useState("") + const [isLoading, setIsLoading] = React.useState(false) + + // 도메인별 사용자 그룹핑 + const usersByDomain = React.useMemo(() => { + const groups: Record = {} + users.forEach(user => { + const domain = user.domain || "pending" + if (!groups[domain]) { + groups[domain] = [] + } + groups[domain].push(user) + }) + return groups + }, [users]) + + const handleAssign = async () => { + if (!selectedDomain) { + toast.error("도메인을 선택해주세요.") + return + } + + setIsLoading(true) + try { + const userIds = users.map(user => user.id) + const result = await assignUsersDomain(userIds, selectedDomain as any) + + if (result.success) { + toast.success(`${users.length}명의 사용자에게 ${selectedDomain} 도메인이 할당되었습니다.`) + setOpen(false) + setSelectedDomain("") + // 테이블 새로고침을 위해 router.refresh() 또는 revalidation 필요 + window.location.reload() // 간단한 방법 + } else { + toast.error(result.message || "도메인 할당 중 오류가 발생했습니다.") + } + } catch (error) { + console.error("도메인 할당 오류:", error) + toast.error("도메인 할당 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const selectedDomainInfo = domainOptions.find(option => option.value === selectedDomain) + + return ( + + + + + + + + + + 사용자 도메인 할당 + + + 선택된 {users.length}명의 사용자에게 도메인을 할당합니다. + + + +
+ {/* 현재 사용자 도메인 분포 */} +
+

현재 도메인 분포

+
+ {Object.entries(usersByDomain).map(([domain, domainUsers]) => { + const domainInfo = domainOptions.find(opt => opt.value === domain) + return ( + + {domainInfo?.icon || "⚪"} + {domainInfo?.label || domain} ({domainUsers.length}명) + + ) + })} +
+
+ + + + {/* 사용자 목록 */} +
+

대상 사용자

+ +
+ {users.map((user, index) => ( +
+
+ {user.name} + ({user.email}) +
+ + {domainOptions.find(opt => opt.value === user.domain)?.label || user.domain} + +
+ ))} +
+
+
+ + + + {/* 도메인 선택 */} +
+

할당할 도메인

+ +
+ + {/* 선택된 도메인 미리보기 */} + {selectedDomainInfo && ( +
+
+ + 선택된 도메인 +
+
+ {selectedDomainInfo.icon} +
+
{selectedDomainInfo.label}
+
+ {selectedDomainInfo.description} +
+
+
+
+ )} +
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/users/access-control/domain-stats-cards.tsx b/lib/users/access-control/domain-stats-cards.tsx new file mode 100644 index 00000000..e6320e12 --- /dev/null +++ b/lib/users/access-control/domain-stats-cards.tsx @@ -0,0 +1,232 @@ +// components/domain-stats-cards.tsx +"use client" + +import * as React from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Users, + Clock, + Shield, + ShoppingCart, + TrendingUp, + Settings, + Building, + AlertCircle +} from "lucide-react" +import { toast } from "sonner" +import { getUserDomainStats } from "../service" + +interface DomainStatsCardsProps { + onDomainFilter: (domain: string | null) => void + currentFilter?: string | null +} + +// 도메인별 설정 +const domainConfig = { + pending: { + label: "승인 대기", + description: "신규 사용자", + icon: Clock, + color: "bg-yellow-500", + textColor: "text-yellow-700", + bgColor: "bg-yellow-50", + borderColor: "border-yellow-200" + }, + evcp: { + label: "전체 시스템", + description: "관리자급", + icon: Shield, + color: "bg-blue-500", + textColor: "text-blue-700", + bgColor: "bg-blue-50", + borderColor: "border-blue-200" + }, + procurement: { + label: "구매관리팀", + description: "구매/계약 관리", + icon: ShoppingCart, + color: "bg-green-500", + textColor: "text-green-700", + bgColor: "bg-green-50", + borderColor: "border-green-200" + }, + sales: { + label: "기술영업팀", + description: "영업/프로젝트", + icon: TrendingUp, + color: "bg-purple-500", + textColor: "text-purple-700", + bgColor: "bg-purple-50", + borderColor: "border-purple-200" + }, + engineering: { + label: "설계관리팀", + description: "설계/기술평가", + icon: Settings, + color: "bg-orange-500", + textColor: "text-orange-700", + bgColor: "bg-orange-50", + borderColor: "border-orange-200" + }, + // partners: { + // label: "협력업체", + // description: "외부 업체", + // icon: Building, + // color: "bg-indigo-500", + // textColor: "text-indigo-700", + // bgColor: "bg-indigo-50", + // borderColor: "border-indigo-200" + // } +} + +interface DomainStats { + domain: string + count: number +} + +export function DomainStatsCards({ onDomainFilter, currentFilter }: DomainStatsCardsProps) { + const [stats, setStats] = React.useState([]) + const [isLoading, setIsLoading] = React.useState(true) + const [totalUsers, setTotalUsers] = React.useState(0) + + // 통계 데이터 로드 + React.useEffect(() => { + const loadStats = async () => { + setIsLoading(true) + try { + const result = await getUserDomainStats() + if (result.success) { + setStats(result.data) + setTotalUsers(result.data.reduce((sum, item) => sum + item.count, 0)) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("통계 로드 오류:", error) + toast.error("통계를 불러오는 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + loadStats() + }, []) + + // 도메인별 카드 클릭 핸들러 + const handleDomainClick = (domain: string) => { + if (currentFilter === domain) { + // 이미 선택된 도메인이면 필터 해제 + onDomainFilter(null) + } else { + // 새로운 도메인 필터 적용 + onDomainFilter(domain) + } + } + + // pending 사용자 수 가져오기 + const pendingCount = stats.find(s => s.domain === "pending")?.count || 0 + + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + +
+
+
+ ))} +
+ ) + } + + return ( +
+ {/* 요약 정보 */} +
+
+

도메인별 사용자 현황

+

+ 총 {totalUsers}명의 사용자가 등록되어 있습니다. +

+
+ + {pendingCount > 0 && ( + + )} +
+ + {/* 도메인별 통계 카드 */} +
+ {Object.entries(domainConfig).map(([domain, config]) => { + const domainStat = stats.find(s => s.domain === domain) + const count = domainStat?.count || 0 + const isActive = currentFilter === domain + const IconComponent = config.icon + + return ( + handleDomainClick(domain)} + > + +
+
+ +
+
+
+

+ {config.label} +

+ + {count} + +
+

+ {config.description} +

+
+
+
+
+ ) + })} +
+ + {/* 필터 상태 표시 */} + {currentFilter && ( +
+ 필터 적용됨: + + {domainConfig[currentFilter as keyof typeof domainConfig]?.label || currentFilter} + + +
+ )} +
+ ) +} \ No newline at end of file diff --git a/lib/users/access-control/users-table-columns.tsx b/lib/users/access-control/users-table-columns.tsx new file mode 100644 index 00000000..7e510b96 --- /dev/null +++ b/lib/users/access-control/users-table-columns.tsx @@ -0,0 +1,149 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { type User } from "@/db/schema/users" + +import { formatDate } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { getErrorMessage } from "@/lib/handle-error" + +import { toast } from "sonner" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { MultiSelect } from "@/components/ui/multi-select" +import { userAccessColumnsConfig } from "@/config/userAccessColumnsConfig" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns(): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: 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, + } + + + + const groupMap: Record[]> = {} + + userAccessColumnsConfig.forEach((cfg) => { + // 만약 group가 없으면 "_noGroup" 처리 + const groupName = cfg.group || "_noGroup" + + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } + + // child column 정의 + const childCol: ColumnDef = { + accessorKey: cfg.id, + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + cell: ({ row, cell }) => { + + if (cfg.id === "createdAt") { + const dateVal = cell.getValue() as Date + return formatDate(dateVal) + } + + if (cfg.id === "domain") { + const domainValues = row.original.domain; + return ( +
+ + + {domainValues} + + +
+ ); + } + + return row.getValue(cfg.id) ?? "" + }, + } + + groupMap[groupName].push(childCol) + }) + + // ---------------------------------------------------------------- + // 3-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef[] = [] + + // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 + // 여기서는 그냥 Object.entries 순서 + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + if (groupName === "_noGroup") { + // 그룹 없음 → 그냥 최상위 레벨 컬럼 + nestedColumns.push(...colDefs) + } else { + // 상위 컬럼 + nestedColumns.push({ + id: groupName, + header: groupName, // "Basic Info", "Metadata" 등 + columns: colDefs, + }) + } + }) + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, nestedColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...nestedColumns, + ] +} \ No newline at end of file diff --git a/lib/users/access-control/users-table-toolbar-actions.tsx b/lib/users/access-control/users-table-toolbar-actions.tsx new file mode 100644 index 00000000..6a431016 --- /dev/null +++ b/lib/users/access-control/users-table-toolbar-actions.tsx @@ -0,0 +1,51 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, Upload } from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { User } from "@/db/schema/users" +import { AssignDomainDialog } from "./assign-domain-dialog" + +interface UsersTableToolbarActionsProps { + table: Table +} + +export function UsersTableToolbarActions({ table }: UsersTableToolbarActionsProps) { + // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 + const fileInputRef = React.useRef(null) + + return ( +
+ + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + row.original)} + /> + ) : null} + + + + {/** 4) Export 버튼 */} + +
+ ) +} \ No newline at end of file diff --git a/lib/users/access-control/users-table.tsx b/lib/users/access-control/users-table.tsx new file mode 100644 index 00000000..50ce4dee --- /dev/null +++ b/lib/users/access-control/users-table.tsx @@ -0,0 +1,166 @@ +"use client" + +import * as React from "react" +import { User} from "@/db/schema/users" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar" + +import { getUsersNotPartners } from "@/lib/users/service"; +import { getColumns } from "./users-table-columns" +import { UsersTableToolbarActions } from "./users-table-toolbar-actions" +import { DomainStatsCards } from "./domain-stats-cards" + +interface UsersTableProps { + promises: Promise< + [ + Awaited>, + ] + > +} + +export function UserAccessControlTable({ promises }: UsersTableProps) { + const [{ data, pageCount }] = React.use(promises) + const [currentDomainFilter, setCurrentDomainFilter] = React.useState(null) + + const columns = React.useMemo(() => getColumns(), []) + + // 도메인 필터에 따른 데이터 필터링 + const filteredData = React.useMemo(() => { + if (!currentDomainFilter) { + return data // 필터가 없으면 전체 데이터 + } + return data.filter(user => user.domain === currentDomainFilter) + }, [data, currentDomainFilter]) + + // 필터링된 데이터의 페이지 수 재계산 + const filteredPageCount = React.useMemo(() => { + if (!currentDomainFilter) { + return pageCount // 필터가 없으면 원본 페이지 수 + } + // 필터링된 데이터는 페이지 수를 1로 설정 (클라이언트 필터링이므로) + return 1 + }, [pageCount, currentDomainFilter]) + + /** + * Advanced filter fields for the data table. + */ + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { + id: "name", + label: "사용자명", + type: "text", + }, + { + id: "email", + label: "이메일", + type: "text", + }, + { + id: "deptName", + label: "부서", + type: "text", + }, + { + id: "domain", + label: "도메인", + type: "multi-select", + options: [ + { label: "🟡 승인 대기", value: "pending" }, + { label: "🔵 전체 시스템", value: "evcp" }, + { label: "🟢 구매관리팀", value: "procurement" }, + { label: "🟣 기술영업팀", value: "sales" }, + { label: "🟠 설계관리팀", value: "engineering" }, + { label: "🟦 협력업체", value: "partners" }, + ], + }, + { + id: "createdAt", + label: "생성일", + type: "date", + }, + ] + + const { table } = useDataTable({ + data: filteredData, // 필터링된 데이터 사용 + columns, + pageCount: filteredPageCount, // 필터링된 페이지 수 사용 + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => `${originalRow.id}`, + shallow: false, + clearOnDefault: true, + }) + + // 도메인 필터 핸들러 (단순화) + const handleDomainFilter = React.useCallback((domain: string | null) => { + setCurrentDomainFilter(domain) + }, []) + + return ( +
+ {/* 도메인 통계 카드 */} + + + {/* 현재 필터 상태 표시 */} + {/* {currentDomainFilter && ( +
+
+ + 필터 적용됨: + + + {(() => { + const domainLabels = { + pending: "🟡 승인 대기", + evcp: "🔵 전체 시스템", + procurement: "🟢 구매관리팀", + sales: "🟣 기술영업팀", + engineering: "🟠 설계관리팀", + partners: "🟦 협력업체" + } + return domainLabels[currentDomainFilter as keyof typeof domainLabels] || currentDomainFilter + })()} + +
+
+ + {filteredData.length}명 표시 + + +
+
+ )} */} + + {/* 데이터 테이블 */} + + + + + +
+ ) +} \ No newline at end of file diff --git a/lib/users/auth/verifyCredentails.ts b/lib/users/auth/verifyCredentails.ts index ec3159a8..1b67b874 100644 --- a/lib/users/auth/verifyCredentails.ts +++ b/lib/users/auth/verifyCredentails.ts @@ -458,8 +458,11 @@ export async function verifySGipsCredentials( error?: string; }> { try { + + const sgipsUrl = process.env.S_GIPS_URL || "http://qa.shi-api.com/evcp/Common/verifySgipsUser" + // 1. S-Gips API 호출로 인증 확인 - const response = await fetch(process.env.S_GIPS_URL, { + const response = await fetch(sgipsUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/lib/users/service.ts b/lib/users/service.ts index ad01c22a..9671abfb 100644 --- a/lib/users/service.ts +++ b/lib/users/service.ts @@ -6,15 +6,15 @@ import { getAllUsers, createUser, getUserById, updateUser, deleteUser, getUserBy import logger from '@/lib/logger'; import { Role, userRoles, users, userView, type User } from '@/db/schema/users'; import { saveDocument } from '../storage'; -import { GetUsersSchema } from '../admin-users/validations'; -import { revalidateTag, unstable_cache, unstable_noStore } from 'next/cache'; +import { GetSimpleUsersSchema, GetUsersSchema } from '../admin-users/validations'; +import { revalidatePath, revalidateTag, unstable_cache, unstable_noStore } from 'next/cache'; import { filterColumns } from '../filter-columns'; -import { countUsers, selectUsersWithCompanyAndRoles } from '../admin-users/repository'; +import { countUsers, countUsersSimple, selectUsers, selectUsersWithCompanyAndRoles } from '../admin-users/repository'; import db from "@/db/db"; import { getErrorMessage } from "@/lib/handle-error"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray } from "drizzle-orm"; +import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray, ne } from "drizzle-orm"; interface AssignUsersArgs { roleId: number @@ -340,6 +340,82 @@ export async function getUsersEVCP(input: GetUsersSchema) { )(); } + +export async function getUsersNotPartners(input: GetSimpleUsersSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // (1) advancedWhere + const advancedWhere = filterColumns({ + table: users, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + // (2) globalWhere + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(users.name, s), + ilike(users.email, s), + ilike(users.deptName, s), + ); + } + + // (3) 디폴트 domainWhere = eq(userView.domain, "partners") + // 다만, 사용자가 이미 domain 필터를 줬다면 적용 X + let domainWhere; + const hasDomainFilter = input.filters?.some((f) => f.id === "domain"); + if (!hasDomainFilter) { + domainWhere = ne(users.domain, "partners"); + } + + // (4) 최종 where + const finalWhere = and(advancedWhere, globalWhere, domainWhere); + + // (5) 정렬 + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(users[item.id]) : asc(users[item.id]) + ) + : [desc(users.createdAt)]; + + // ... + const { data, total } = await db.transaction(async (tx) => { + const data = await selectUsers(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }); + + + console.log(data) + + const total = await countUsersSimple(tx, finalWhere); + return { data, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + return { data, pageCount }; + } catch (err) { + + console.log(err) + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input)], + { + revalidate: 3600, + tags: ["users-access-control"], + } + )(); +} + export async function getAllRoles(): Promise { try { return await findAllRoles(); @@ -507,3 +583,169 @@ export async function assignUsersToRole(roleId: number, userIds: number[]) { } } + + +export type UserDomain = "pending" | "evcp" | "procurement" | "sales" | "engineering" | "partners" + +/** + * 여러 사용자에게 도메인을 일괄 할당하는 함수 + */ +export async function assignUsersDomain( + userIds: number[], + domain: UserDomain +) { + try { + if (!userIds.length) { + return { + success: false, + message: "할당할 사용자가 없습니다." + } + } + + if (!domain) { + return { + success: false, + message: "도메인을 선택해주세요." + } + } + + // 사용자들의 도메인 업데이트 + const result = await db + .update(users) + .set({ + domain, + updatedAt: new Date(), + }) + .where(inArray(users.id, userIds)) + .returning({ + id: users.id, + name: users.name, + email: users.email, + domain: users.domain, + }) + + // 관련 페이지들 revalidate + revalidatePath("/evcp/user-management") + revalidatePath("/") + + return { + success: true, + message: `${result.length}명의 사용자 도메인이 업데이트되었습니다.`, + data: result + } + } catch (error) { + console.error("사용자 도메인 할당 오류:", error) + return { + success: false, + message: "도메인 할당 중 오류가 발생했습니다." + } + } +} + +/** + * 단일 사용자의 도메인을 변경하는 함수 + */ +export async function assignUserDomain( + userId: number, + domain: UserDomain +) { + try { + const result = await db + .update(users) + .set({ + domain, + updatedAt: new Date(), + }) + .where(eq(users.id, userId)) + .returning({ + id: users.id, + name: users.name, + email: users.email, + domain: users.domain, + }) + + if (result.length === 0) { + return { + success: false, + message: "사용자를 찾을 수 없습니다." + } + } + + revalidatePath("/evcp/user-management") + revalidatePath("/evcp/users") + + return { + success: true, + message: `${result[0].name}님의 도메인이 ${domain}으로 변경되었습니다.`, + data: result[0] + } + } catch (error) { + console.error("사용자 도메인 할당 오류:", error) + return { + success: false, + message: "도메인 할당 중 오류가 발생했습니다." + } + } +} + +/** + * 도메인별 사용자 통계를 조회하는 함수 + */ +export async function getUserDomainStats() { + try { + const stats = await db + .select({ + domain: users.domain, + count: count(), + }) + .from(users) + .where(eq(users.isActive, true)) + .groupBy(users.domain) + + return { + success: true, + data: stats + } + } catch (error) { + console.error("도메인 통계 조회 오류:", error) + return { + success: false, + message: "통계 조회 중 오류가 발생했습니다.", + data: [] + } + } +} + +/** + * pending 도메인 사용자 목록을 조회하는 함수 (관리자용) + */ +export async function getPendingUsers() { + try { + const pendingUsers = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + createdAt: users.createdAt, + domain: users.domain, + }) + .from(users) + .where(and( + eq(users.domain, "pending"), + eq(users.isActive, true) + )) + .orderBy(desc(users.createdAt)) + + return { + success: true, + data: pendingUsers + } + } catch (error) { + console.error("pending 사용자 조회 오류:", error) + return { + success: false, + message: "사용자 조회 중 오류가 발생했습니다.", + data: [] + } + } +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index ed471109..242f439b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -13,6 +13,9 @@ acceptLanguage.languages(languages); // 로그인이 필요 없는 공개 경로 const publicPaths = [ '/evcp', + '/procurement', + '/sales', + '/engineering', '/partners', '/partners/repository', '/partners/signup', @@ -36,29 +39,87 @@ function isPublicPath(path: string, lng: string) { return false; } +// 도메인별 기본 대시보드 경로 정의 +function getDashboardPath(domain: string, lng: string): string { + switch (domain) { + case 'pending': + return `/${lng}/pending`; + case 'evcp': + return `/${lng}/evcp/report`; + case 'procurement': + return `/${lng}/procurement/dashboard`; + case 'sales': + return `/${lng}/sales/dashboard`; + case 'engineering': + return `/${lng}/engineering/dashboard`; + case 'partners': + return `/${lng}/partners/dashboard`; + default: + return `/${lng}/pending`; // 기본값 + } +} + +// 도메인별 로그인 페이지 경로 정의 +function getLoginPath(domain: string, lng: string): string { + switch (domain) { + case 'partners': + return `/${lng}/partners`; + case 'pending': + return `/${lng}/pending`; + case 'evcp': + case 'procurement': + case 'sales': + case 'engineering': + default: + return `/${lng}/evcp`; + } +} + // 도메인-URL 일치 여부 확인 및 올바른 리다이렉트 경로 반환 function getDomainRedirectPath(path: string, domain: string, lng: string) { // 도메인이 없는 경우 리다이렉트 없음 if (!domain) return null; - // URL에 partners가 있는지 확인 - const hasPartnersInPath = path.includes('/partners'); - // URL에 evcp가 있는지 확인 - const hasEvcpInPath = path.includes('/evcp'); + // 각 도메인 경로 패턴 확인 + const domainPatterns = { + pending: `/pending/`, + evcp: `/evcp/`, + procurement: `/procurement/`, + sales: `/sales/`, + engineering: `/engineering/`, + partners: `/partners/` + }; - // 1. 도메인이 'partners'인데 URL에 '/evcp/'가 있으면 - if (domain === 'partners' && hasEvcpInPath) { - // URL에서 '/evcp/'를 '/partners/'로 교체 - return path.replace('/evcp/', '/partners/'); + // 현재 경로가 어떤 도메인 패턴에 속하는지 확인 + let currentPathDomain = null; + for (const [domainName, pattern] of Object.entries(domainPatterns)) { + if (path.includes(pattern)) { + currentPathDomain = domainName; + break; + } } - - // 2. 도메인이 'evcp'인데 URL에 '/partners/'가 있으면 - if (domain === 'evcp' && hasPartnersInPath) { - // URL에서 '/partners/'를 '/evcp/'로 교체 - return path.replace('/partners/', '/evcp/'); + + // 도메인과 경로가 일치하지 않는 경우 + if (currentPathDomain && currentPathDomain !== domain) { + // pending 사용자는 오직 pending 경로만 접근 가능 + if (domain === 'pending') { + return getDashboardPath('pending', lng); + } + + // 다른 도메인 사용자가 pending에 접근하려는 경우 + if (currentPathDomain === 'pending') { + return getDashboardPath(domain, lng); + } + + // 일반적인 도메인 불일치 처리 + const targetPattern = domainPatterns[domain as keyof typeof domainPatterns]; + if (targetPattern && currentPathDomain) { + const sourcePattern = domainPatterns[currentPathDomain as keyof typeof domainPatterns]; + return path.replace(sourcePattern, targetPattern); + } } - // 불일치가 없으면 null 반환 (리다이렉트 필요 없음) + // 일치하거나 처리할 수 없는 경우 null 반환 return null; } @@ -86,7 +147,10 @@ function createLoginUrl(pathname: string, detectedLng: string, origin: string, r // 경로에 따라 적절한 로그인 페이지 선택 if (pathname.includes('/partners') || pathname.startsWith(`/${detectedLng}/vendor`)) { loginPath = `/${detectedLng}/partners`; + } else if (pathname.includes('/pending')) { + loginPath = `/${detectedLng}/pending`; } else { + // evcp, procurement, sales, engineering은 모두 evcp 로그인 사용 loginPath = `/${detectedLng}/evcp`; } @@ -165,8 +229,6 @@ export async function middleware(request: NextRequest) { const loginUrl = createLoginUrl(pathname, detectedLng, origin, request, 'expired'); return NextResponse.redirect(loginUrl); } - - // 세션 만료 경고를 위한 응답 헤더 설정은 나중에 적용 } /** @@ -192,18 +254,20 @@ export async function middleware(request: NextRequest) { const { isExpired } = checkSessionTimeout(token); if (!isExpired) { - // 로그인 페이지 경로 확인 (정확한 /ko/evcp 또는 /en/partners 등) - const isEvcpLoginPage = pathname === `/${detectedLng}/evcp`; - const isPartnersLoginPage = pathname === `/${detectedLng}/partners`; + // 모든 도메인의 로그인 페이지 확인 + const loginPages = [ + `/${detectedLng}/evcp`, + `/${detectedLng}/procurement`, + `/${detectedLng}/sales`, + `/${detectedLng}/engineering`, + `/${detectedLng}/partners`, + `/${detectedLng}/pending` + ]; - if (isEvcpLoginPage) { - // EVCP 로그인 페이지에 접근한 경우 report 페이지로 리다이렉트 - const redirectUrl = new URL(`/${detectedLng}/evcp/report`, origin); - redirectUrl.search = searchParams.toString(); - return NextResponse.redirect(redirectUrl); - } else if (isPartnersLoginPage) { - // Partners 로그인 페이지에 접근한 경우 dashboard 페이지로 리다이렉트 - const redirectUrl = new URL(`/${detectedLng}/partners/dashboard`, origin); + if (loginPages.includes(pathname)) { + // 사용자의 도메인에 맞는 대시보드로 리다이렉트 + const dashboardPath = getDashboardPath(token.domain as string, detectedLng); + const redirectUrl = new URL(dashboardPath, origin); redirectUrl.search = searchParams.toString(); return NextResponse.redirect(redirectUrl); } -- cgit v1.2.3