From 2865bebdb17ecb89e1a1dca3024611c43a0a1dfe Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 5 Dec 2025 00:32:02 +0700 Subject: [PATCH] 251205:0000 Just start debug backend/frontend --- backend/src/config/database.config.ts | 18 + .../src/database/migrations/InitialSchema.ts | 136 +++++++ .../src/database/seeds/organization.seed.ts | 72 ++++ backend/src/database/seeds/run-seed.ts | 71 +--- backend/src/database/seeds/user.seed.ts | 106 +++++ .../src/modules/auth/entities/role.entity.ts | 45 +++ .../entities/correspondence.entity.ts | 36 ++ .../drawings/entities/drawing.entity.ts | 44 +++ .../entities/organization.entity.ts | 34 ++ .../src/modules/rfas/entities/rfa.entity.ts | 41 ++ .../src/modules/users/entities/user.entity.ts | 52 +++ .../app/(admin)/admin/audit-logs/page.tsx | 122 ++++++ .../admin/numbering/[id]/edit/page.tsx | 82 ++++ .../app/(admin)/admin/numbering/new/page.tsx | 27 ++ frontend/app/(admin)/admin/numbering/page.tsx | 136 +++++++ .../app/(admin)/admin/organizations/page.tsx | 156 ++++++++ frontend/app/(admin)/admin/users/page.tsx | 143 +++++++ .../admin/workflows/[id]/edit/page.tsx | 164 ++++++++ .../app/(admin)/admin/workflows/new/page.tsx | 134 +++++++ frontend/app/(admin)/admin/workflows/page.tsx | 104 +++++ frontend/app/(admin)/layout.tsx | 30 ++ .../(dashboard)/correspondences/[id]/page.tsx | 22 ++ .../(dashboard)/correspondences/new/page.tsx | 361 +----------------- .../app/(dashboard)/correspondences/page.tsx | 308 ++------------- frontend/app/(dashboard)/dashboard/page.tsx | 88 ++--- .../app/(dashboard)/drawings/[id]/page.tsx | 112 ++++++ frontend/app/(dashboard)/drawings/page.tsx | 43 +++ .../app/(dashboard)/drawings/upload/page.tsx | 16 + frontend/app/(dashboard)/layout.tsx | 19 +- frontend/app/(dashboard)/rfas/[id]/page.tsx | 22 ++ frontend/app/(dashboard)/rfas/new/page.tsx | 16 + frontend/app/(dashboard)/rfas/page.tsx | 48 +++ frontend/app/(dashboard)/search/page.tsx | 56 +++ frontend/app/demo/page.tsx | 192 ++++++---- frontend/components/admin/sidebar.tsx | 48 +++ frontend/components/admin/user-dialog.tsx | 240 ++++++++++++ frontend/components/common/can.tsx | 0 frontend/components/common/confirm-dialog.tsx | 49 +++ frontend/components/common/data-table.tsx | 87 +++++ frontend/components/common/file-upload.tsx | 102 +++++ frontend/components/common/pagination.tsx | 74 ++++ frontend/components/common/status-badge.tsx | 63 +++ .../components/correspondences/detail.tsx | 133 +++++++ frontend/components/correspondences/form.tsx | 187 +++++++++ frontend/components/correspondences/list.tsx | 87 +++++ .../components/dashboard/pending-tasks.tsx | 66 ++++ .../components/dashboard/quick-actions.tsx | 30 ++ .../components/dashboard/recent-activity.tsx | 136 +++---- frontend/components/dashboard/stats-cards.tsx | 64 ++++ frontend/components/drawings/card.tsx | 72 ++++ frontend/components/drawings/list.tsx | 56 +++ .../components/drawings/revision-history.tsx | 50 +++ frontend/components/drawings/upload-form.tsx | 167 ++++++++ frontend/components/layout/global-search.tsx | 114 ++++++ frontend/components/layout/header.tsx | 24 ++ .../layout/notifications-dropdown.tsx | 139 +++++++ frontend/components/layout/sidebar.tsx | 230 +++++------ frontend/components/layout/user-menu.tsx | 81 ++++ .../components/numbering/sequence-viewer.tsx | 92 +++++ .../components/numbering/template-editor.tsx | 203 ++++++++++ .../components/numbering/template-tester.tsx | 110 ++++++ frontend/components/rfas/detail.tsx | 201 ++++++++++ frontend/components/rfas/form.tsx | 237 ++++++++++++ frontend/components/rfas/list.tsx | 80 ++++ frontend/components/search/filters.tsx | 95 +++++ frontend/components/search/results.tsx | 97 +++++ frontend/components/workflows/dsl-editor.tsx | 115 ++++++ .../components/workflows/visual-builder.tsx | 109 ++++++ frontend/lib/api/admin.ts | 103 +++++ frontend/lib/api/correspondences.ts | 85 +++++ frontend/lib/api/dashboard.ts | 65 ++++ frontend/lib/api/drawings.ts | 119 ++++++ frontend/lib/api/notifications.ts | 50 +++ frontend/lib/api/numbering.ts | 111 ++++++ frontend/lib/api/rfas.ts | 98 +++++ frontend/lib/api/search.ts | 79 ++++ frontend/lib/api/workflows.ts | 84 ++++ frontend/package.json | 4 + frontend/tsconfig.json | 2 +- frontend/types/admin.ts | 43 +++ frontend/types/correspondence.ts | 32 ++ frontend/types/dashboard.ts | 28 ++ frontend/types/drawing.ts | 34 ++ frontend/types/notification.ts | 14 + frontend/types/numbering.ts | 37 ++ frontend/types/rfa.ts | 32 ++ frontend/types/search.ts | 18 + frontend/types/workflow.ts | 35 ++ 88 files changed, 6751 insertions(+), 1016 deletions(-) create mode 100644 backend/src/config/database.config.ts create mode 100644 backend/src/database/migrations/InitialSchema.ts create mode 100644 backend/src/database/seeds/organization.seed.ts create mode 100644 backend/src/database/seeds/user.seed.ts create mode 100644 backend/src/modules/auth/entities/role.entity.ts create mode 100644 backend/src/modules/correspondences/entities/correspondence.entity.ts create mode 100644 backend/src/modules/drawings/entities/drawing.entity.ts create mode 100644 backend/src/modules/organizations/entities/organization.entity.ts create mode 100644 backend/src/modules/rfas/entities/rfa.entity.ts create mode 100644 backend/src/modules/users/entities/user.entity.ts create mode 100644 frontend/app/(admin)/admin/audit-logs/page.tsx create mode 100644 frontend/app/(admin)/admin/numbering/[id]/edit/page.tsx create mode 100644 frontend/app/(admin)/admin/numbering/new/page.tsx create mode 100644 frontend/app/(admin)/admin/numbering/page.tsx create mode 100644 frontend/app/(admin)/admin/organizations/page.tsx create mode 100644 frontend/app/(admin)/admin/users/page.tsx create mode 100644 frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx create mode 100644 frontend/app/(admin)/admin/workflows/new/page.tsx create mode 100644 frontend/app/(admin)/admin/workflows/page.tsx create mode 100644 frontend/app/(admin)/layout.tsx create mode 100644 frontend/app/(dashboard)/correspondences/[id]/page.tsx create mode 100644 frontend/app/(dashboard)/drawings/[id]/page.tsx create mode 100644 frontend/app/(dashboard)/drawings/page.tsx create mode 100644 frontend/app/(dashboard)/drawings/upload/page.tsx create mode 100644 frontend/app/(dashboard)/rfas/[id]/page.tsx create mode 100644 frontend/app/(dashboard)/rfas/new/page.tsx create mode 100644 frontend/app/(dashboard)/rfas/page.tsx create mode 100644 frontend/app/(dashboard)/search/page.tsx create mode 100644 frontend/components/admin/sidebar.tsx create mode 100644 frontend/components/admin/user-dialog.tsx create mode 100644 frontend/components/common/can.tsx create mode 100644 frontend/components/common/confirm-dialog.tsx create mode 100644 frontend/components/common/data-table.tsx create mode 100644 frontend/components/common/file-upload.tsx create mode 100644 frontend/components/common/pagination.tsx create mode 100644 frontend/components/common/status-badge.tsx create mode 100644 frontend/components/correspondences/detail.tsx create mode 100644 frontend/components/correspondences/form.tsx create mode 100644 frontend/components/correspondences/list.tsx create mode 100644 frontend/components/dashboard/pending-tasks.tsx create mode 100644 frontend/components/dashboard/quick-actions.tsx create mode 100644 frontend/components/dashboard/stats-cards.tsx create mode 100644 frontend/components/drawings/card.tsx create mode 100644 frontend/components/drawings/list.tsx create mode 100644 frontend/components/drawings/revision-history.tsx create mode 100644 frontend/components/drawings/upload-form.tsx create mode 100644 frontend/components/layout/global-search.tsx create mode 100644 frontend/components/layout/header.tsx create mode 100644 frontend/components/layout/notifications-dropdown.tsx create mode 100644 frontend/components/layout/user-menu.tsx create mode 100644 frontend/components/numbering/sequence-viewer.tsx create mode 100644 frontend/components/numbering/template-editor.tsx create mode 100644 frontend/components/numbering/template-tester.tsx create mode 100644 frontend/components/rfas/detail.tsx create mode 100644 frontend/components/rfas/form.tsx create mode 100644 frontend/components/rfas/list.tsx create mode 100644 frontend/components/search/filters.tsx create mode 100644 frontend/components/search/results.tsx create mode 100644 frontend/components/workflows/dsl-editor.tsx create mode 100644 frontend/components/workflows/visual-builder.tsx create mode 100644 frontend/lib/api/admin.ts create mode 100644 frontend/lib/api/correspondences.ts create mode 100644 frontend/lib/api/dashboard.ts create mode 100644 frontend/lib/api/drawings.ts create mode 100644 frontend/lib/api/notifications.ts create mode 100644 frontend/lib/api/numbering.ts create mode 100644 frontend/lib/api/rfas.ts create mode 100644 frontend/lib/api/search.ts create mode 100644 frontend/lib/api/workflows.ts create mode 100644 frontend/types/admin.ts create mode 100644 frontend/types/correspondence.ts create mode 100644 frontend/types/dashboard.ts create mode 100644 frontend/types/drawing.ts create mode 100644 frontend/types/notification.ts create mode 100644 frontend/types/numbering.ts create mode 100644 frontend/types/rfa.ts create mode 100644 frontend/types/search.ts create mode 100644 frontend/types/workflow.ts diff --git a/backend/src/config/database.config.ts b/backend/src/config/database.config.ts new file mode 100644 index 0000000..0959342 --- /dev/null +++ b/backend/src/config/database.config.ts @@ -0,0 +1,18 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { config } from 'dotenv'; + +config(); + +export const databaseConfig: TypeOrmModuleOptions = { + type: 'mysql', + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '3306'), + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || 'Center#2025', + database: process.env.DB_DATABASE || 'lcbp3_dev', + entities: [__dirname + '/../**/*.entity{.ts,.js}'], + migrations: [__dirname + '/../database/migrations/*{.ts,.js}'], + synchronize: false, + logging: process.env.NODE_ENV === 'development', + autoLoadEntities: true, +}; diff --git a/backend/src/database/migrations/InitialSchema.ts b/backend/src/database/migrations/InitialSchema.ts new file mode 100644 index 0000000..a2e66e6 --- /dev/null +++ b/backend/src/database/migrations/InitialSchema.ts @@ -0,0 +1,136 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialSchema1701234567890 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Organizations + await queryRunner.query(` + CREATE TABLE organizations ( + id INT PRIMARY KEY AUTO_INCREMENT, + organization_code VARCHAR(20) NOT NULL UNIQUE, + organization_name VARCHAR(200) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + INDEX idx_org_code (organization_code) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Permissions + await queryRunner.query(` + CREATE TABLE permissions ( + id INT PRIMARY KEY AUTO_INCREMENT, + permission_code VARCHAR(50) NOT NULL UNIQUE, + description TEXT NULL, + resource VARCHAR(50) NOT NULL, + action VARCHAR(50) NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Roles + await queryRunner.query(` + CREATE TABLE roles ( + id INT PRIMARY KEY AUTO_INCREMENT, + role_name VARCHAR(50) NOT NULL UNIQUE, + description TEXT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Role Permissions + await queryRunner.query(` + CREATE TABLE role_permissions ( + role_id INT NOT NULL, + permission_id INT NOT NULL, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Users + await queryRunner.query(` + CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // User Roles + await queryRunner.query(` + CREATE TABLE user_roles ( + user_id INT NOT NULL, + role_id INT NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Correspondences + await queryRunner.query(` + CREATE TABLE correspondences ( + id INT PRIMARY KEY AUTO_INCREMENT, + document_number VARCHAR(50) NOT NULL UNIQUE, + subject VARCHAR(255) NOT NULL, + body TEXT NULL, + type VARCHAR(50) NOT NULL, + status VARCHAR(50) DEFAULT 'Draft', + created_by_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (created_by_id) REFERENCES users(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // RFAs + await queryRunner.query(` + CREATE TABLE rfas ( + id INT PRIMARY KEY AUTO_INCREMENT, + rfa_number VARCHAR(50) NOT NULL UNIQUE, + title VARCHAR(255) NOT NULL, + discipline_code VARCHAR(20) NULL, + status VARCHAR(50) DEFAULT 'Draft', + created_by_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (created_by_id) REFERENCES users(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Drawings + await queryRunner.query(` + CREATE TABLE drawings ( + id INT PRIMARY KEY AUTO_INCREMENT, + drawing_number VARCHAR(50) NOT NULL UNIQUE, + title VARCHAR(255) NOT NULL, + drawing_type VARCHAR(50) NOT NULL, + revision VARCHAR(10) NOT NULL, + status VARCHAR(50) DEFAULT 'Draft', + uploaded_by_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (uploaded_by_id) REFERENCES users(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS drawings`); + await queryRunner.query(`DROP TABLE IF EXISTS rfas`); + await queryRunner.query(`DROP TABLE IF EXISTS correspondences`); + await queryRunner.query(`DROP TABLE IF EXISTS user_roles`); + await queryRunner.query(`DROP TABLE IF EXISTS users`); + await queryRunner.query(`DROP TABLE IF EXISTS role_permissions`); + await queryRunner.query(`DROP TABLE IF EXISTS roles`); + await queryRunner.query(`DROP TABLE IF EXISTS permissions`); + await queryRunner.query(`DROP TABLE IF EXISTS organizations`); + } +} diff --git a/backend/src/database/seeds/organization.seed.ts b/backend/src/database/seeds/organization.seed.ts new file mode 100644 index 0000000..599a6ee --- /dev/null +++ b/backend/src/database/seeds/organization.seed.ts @@ -0,0 +1,72 @@ +import { DataSource } from 'typeorm'; +import { Organization } from '../../modules/organizations/entities/organization.entity'; + +export async function seedOrganizations(dataSource: DataSource) { + const repo = dataSource.getRepository(Organization); + + const orgs = [ + { organizationCode: 'กทท.', organizationName: 'การท่าเรือแห่งประเทศไทย' }, + { + organizationCode: 'สคฉ.3', + organizationName: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3', + }, + { + organizationCode: 'สคฉ.3-01', + organizationName: 'ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน', + }, + { + organizationCode: 'สคฉ.3-02', + organizationName: 'ตรวจรับพัสดุ งานทางทะเล', + }, + { + organizationCode: 'สคฉ.3-03', + organizationName: 'ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค', + }, + { + organizationCode: 'สคฉ.3-04', + organizationName: 'ตรวจรับพัสดุ ตรวจสอบผลกระทบสิ่งแวดล้อม', + }, + { + organizationCode: 'สคฉ.3-05', + organizationName: 'ตรวจรับพัสดุ เยียวยาการประมง', + }, + { + organizationCode: 'สคฉ.3-06', + organizationName: 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 3', + }, + { + organizationCode: 'สคฉ.3-07', + organizationName: 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 4', + }, + { + organizationCode: 'สคฉ.3-xx', + organizationName: 'ตรวจรับพัสดุ ที่ปรึกษาออกแบบ ส่วนที่ 4', + }, + { organizationCode: 'TEAM', organizationName: 'Designer Consulting Ltd.' }, + { + organizationCode: 'คคง.', + organizationName: 'Construction Supervision Ltd.', + }, + { organizationCode: 'ผรม.1', organizationName: 'Contractor งานทางทะเล' }, + { organizationCode: 'ผรม.2', organizationName: 'Contractor งานก่อสร้าง' }, + { + organizationCode: 'ผรม.3', + organizationName: 'Contractor งานก่อสร้าง ส่วนที่ 3', + }, + { + organizationCode: 'ผรม.4', + organizationName: 'Contractor งานก่อสร้าง ส่วนที่ 4', + }, + { organizationCode: 'EN', organizationName: 'Third Party Environment' }, + { organizationCode: 'CAR', organizationName: 'Third Party Fishery Care' }, + ]; + + for (const org of orgs) { + const exists = await repo.findOneBy({ + organizationCode: org.organizationCode, + }); + if (!exists) { + await repo.save(repo.create(org)); + } + } +} diff --git a/backend/src/database/seeds/run-seed.ts b/backend/src/database/seeds/run-seed.ts index 81b9941..eb5cf05 100644 --- a/backend/src/database/seeds/run-seed.ts +++ b/backend/src/database/seeds/run-seed.ts @@ -1,67 +1,24 @@ -import { config } from 'dotenv'; import { DataSource } from 'typeorm'; -import { seedWorkflowDefinitions } from '../seeds/workflow-definitions.seed'; // Import ฟังก์ชัน Seed ที่คุณมี -// Import Entities ที่เกี่ยวข้อง -import { WorkflowDefinition } from '../../modules/workflow-engine/entities/workflow-definition.entity'; -import { WorkflowHistory } from '../../modules/workflow-engine/entities/workflow-history.entity'; -import { WorkflowInstance } from '../../modules/workflow-engine/entities/workflow-instance.entity'; +import { databaseConfig } from '../../config/database.config'; +import { seedOrganizations } from './organization.seed'; +import { seedUsers } from './user.seed'; -// โหลด Environment Variables (.env) -config(); - -const runSeed = async () => { - // ตั้งค่าการเชื่อมต่อฐานข้อมูล (ควรตรงกับ docker-compose หรือ .env ของคุณ) - const dataSource = new DataSource({ - type: 'mariadb', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '3306'), - username: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || 'Center#2025', - database: process.env.DB_DATABASE || 'lcbp3_dev', - // สำคัญ: ต้องใส่ Entities ที่เกี่ยวข้องทั้งหมดเพื่อให้ TypeORM รู้จัก - entities: [ - WorkflowDefinition, - WorkflowInstance, - WorkflowHistory, - // ใส่ Entity อื่นๆ ถ้าจำเป็น หรือใช้ path pattern: __dirname + '/../../modules/**/*.entity{.ts,.js}' - ], - synchronize: false, // ห้ามใช้ true บน Production - }); +async function runSeeds() { + const dataSource = new DataSource(databaseConfig as any); + await dataSource.initialize(); try { - console.log('🔌 Connecting to database...'); - await dataSource.initialize(); - console.log('✅ Database connected.'); + console.log('🌱 Seeding database...'); - console.log('🌱 Running Seeds...'); - await seedWorkflowDefinitions(dataSource); - console.log('✅ Seeding completed successfully.'); + await seedOrganizations(dataSource); + await seedUsers(dataSource); + + console.log('✅ Seeding completed!'); } catch (error) { - console.error('❌ Error during seeding:', error); + console.error('❌ Seeding failed:', error); } finally { - if (dataSource.isInitialized) { - await dataSource.destroy(); - console.log('🔌 Database connection closed.'); - } + await dataSource.destroy(); } -}; - -runSeed(); - -/* -npx ts-node -r tsconfig-paths/register src/database/run-seed.ts - -**หรือเพิ่มใน `package.json` (แนะนำ):** -คุณสามารถเพิ่ม script ใน `package.json` เพื่อให้เรียกใช้ได้ง่ายขึ้นในอนาคต: - -"scripts": { - "seed": "ts-node -r tsconfig-paths/register src/database/seeds/run-seed.ts" } -http://googleusercontent.com/immersive_entry_chip/1 - -### 💡 ข้อควรระวัง -1. **Environment Variables:** ตรวจสอบให้แน่ใจว่าค่า Config (Host, User, Password) ในไฟล์ `run-seed.ts` หรือ `.env` นั้นถูกต้องและตรงกับ Docker Container ที่กำลังรันอยู่ -2. **Entities:** หากฟังก์ชัน Seed มีการเรียกใช้ Entity อื่นนอกเหนือจาก `WorkflowDefinition` ต้องนำมาใส่ใน `entities: [...]` ของ `DataSource` ให้ครบ ไม่อย่างนั้นจะเจอ Error `RepositoryNotFoundError` - -*/ +runSeeds(); diff --git a/backend/src/database/seeds/user.seed.ts b/backend/src/database/seeds/user.seed.ts new file mode 100644 index 0000000..f9a3e6d --- /dev/null +++ b/backend/src/database/seeds/user.seed.ts @@ -0,0 +1,106 @@ +import { DataSource } from 'typeorm'; +import { User } from '../../modules/users/entities/user.entity'; +import { Role } from '../../modules/auth/entities/role.entity'; +import * as bcrypt from 'bcrypt'; + +export async function seedUsers(dataSource: DataSource) { + const userRepo = dataSource.getRepository(User); + const roleRepo = dataSource.getRepository(Role); + + // Create Roles + const rolesData = [ + { + roleName: 'Superadmin', + description: + 'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global', + }, + { + roleName: 'Org Admin', + description: + 'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร', + }, + { + roleName: 'Document Control', + description: + 'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร', + }, + { + roleName: 'Editor', + description: + 'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย', + }, + { + roleName: 'Viewer', + description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น', + }, + { + roleName: 'Project Manager', + description: + 'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ', + }, + { + roleName: 'Contract Admin', + description: + 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา', + }, + ]; + + const roleMap = new Map(); + for (const r of rolesData) { + let role = await roleRepo.findOneBy({ roleName: r.roleName }); + if (!role) { + role = await roleRepo.save(roleRepo.create(r)); + } + roleMap.set(r.roleName, role); + } + + // Create Users + const usersData = [ + { + username: 'superadmin', + email: 'superadmin@example.com', + firstName: 'Super', + lastName: 'Admin', + roleName: 'Superadmin', + }, + { + username: 'admin', + email: 'admin@example.com', + firstName: 'Admin', + lastName: 'คคง.', + roleName: 'Org Admin', + }, + { + username: 'editor01', + email: 'editor01@example.com', + firstName: 'DC', + lastName: 'C1', + roleName: 'Editor', + }, + { + username: 'viewer01', + email: 'viewer01@example.com', + firstName: 'Viewer', + lastName: 'สคฉ.03', + roleName: 'Viewer', + }, + ]; + + const salt = await bcrypt.genSalt(); + const passwordHash = await bcrypt.hash('password123', salt); // Default password + + for (const u of usersData) { + const exists = await userRepo.findOneBy({ username: u.username }); + if (!exists) { + const user = userRepo.create({ + username: u.username, + email: u.email, + firstName: u.firstName, + lastName: u.lastName, + passwordHash, + roles: [roleMap.get(u.roleName)], + }); + await userRepo.save(user); + } + } +} diff --git a/backend/src/modules/auth/entities/role.entity.ts b/backend/src/modules/auth/entities/role.entity.ts new file mode 100644 index 0000000..af7e891 --- /dev/null +++ b/backend/src/modules/auth/entities/role.entity.ts @@ -0,0 +1,45 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; + +@Entity('permissions') +export class Permission { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'permission_code', length: 50, unique: true }) + permissionCode!: string; + + @Column({ name: 'description', type: 'text', nullable: true }) + description!: string; + + @Column({ name: 'resource', length: 50 }) + resource!: string; + + @Column({ name: 'action', length: 50 }) + action!: string; +} + +@Entity('roles') +export class Role { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'role_name', length: 50, unique: true }) + roleName!: string; + + @Column({ name: 'description', type: 'text', nullable: true }) + description!: string; + + @ManyToMany(() => Permission) + @JoinTable({ + name: 'role_permissions', + joinColumn: { name: 'role_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' }, + }) + permissions!: Permission[]; +} diff --git a/backend/src/modules/correspondences/entities/correspondence.entity.ts b/backend/src/modules/correspondences/entities/correspondence.entity.ts new file mode 100644 index 0000000..bf5b0fc --- /dev/null +++ b/backend/src/modules/correspondences/entities/correspondence.entity.ts @@ -0,0 +1,36 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from '../../users/entities/user.entity'; + +@Entity('correspondences') +export class Correspondence { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'document_number', length: 50, unique: true }) + documentNumber!: string; + + @Column({ length: 255 }) + subject!: string; + + @Column({ type: 'text', nullable: true }) + body!: string; + + @Column({ length: 50 }) + type!: string; + + @Column({ length: 50, default: 'Draft' }) + status!: string; + + @Column({ name: 'created_by_id' }) + createdById!: number; + + @ManyToOne(() => User) + @JoinColumn({ name: 'created_by_id' }) + createdBy!: User; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; +} diff --git a/backend/src/modules/drawings/entities/drawing.entity.ts b/backend/src/modules/drawings/entities/drawing.entity.ts new file mode 100644 index 0000000..0366034 --- /dev/null +++ b/backend/src/modules/drawings/entities/drawing.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; + +@Entity('drawings') +export class Drawing { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'drawing_number', length: 50, unique: true }) + drawingNumber!: string; + + @Column({ length: 255 }) + title!: string; + + @Column({ name: 'drawing_type', length: 50 }) + drawingType!: string; + + @Column({ length: 10 }) + revision!: string; + + @Column({ length: 50, default: 'Draft' }) + status!: string; + + @Column({ name: 'uploaded_by_id' }) + uploadedById!: number; + + @ManyToOne(() => User) + @JoinColumn({ name: 'uploaded_by_id' }) + uploadedBy!: User; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; +} diff --git a/backend/src/modules/organizations/entities/organization.entity.ts b/backend/src/modules/organizations/entities/organization.entity.ts new file mode 100644 index 0000000..4dcce7f --- /dev/null +++ b/backend/src/modules/organizations/entities/organization.entity.ts @@ -0,0 +1,34 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, +} from 'typeorm'; + +@Entity('organizations') +export class Organization { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'organization_code', length: 20, unique: true }) + @Index('idx_org_code') + organizationCode!: string; + + @Column({ name: 'organization_name', length: 200 }) + organizationName!: string; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; + + @DeleteDateColumn({ name: 'deleted_at' }) + deletedAt!: Date; +} diff --git a/backend/src/modules/rfas/entities/rfa.entity.ts b/backend/src/modules/rfas/entities/rfa.entity.ts new file mode 100644 index 0000000..671b191 --- /dev/null +++ b/backend/src/modules/rfas/entities/rfa.entity.ts @@ -0,0 +1,41 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; + +@Entity('rfas') +export class Rfa { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'rfa_number', length: 50, unique: true }) + rfaNumber!: string; + + @Column({ length: 255 }) + title!: string; + + @Column({ name: 'discipline_code', length: 20, nullable: true }) + disciplineCode!: string; + + @Column({ length: 50, default: 'Draft' }) + status!: string; + + @Column({ name: 'created_by_id' }) + createdById!: number; + + @ManyToOne(() => User) + @JoinColumn({ name: 'created_by_id' }) + createdBy!: User; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; +} diff --git a/backend/src/modules/users/entities/user.entity.ts b/backend/src/modules/users/entities/user.entity.ts new file mode 100644 index 0000000..1d3544f --- /dev/null +++ b/backend/src/modules/users/entities/user.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; +import { Role } from '../../auth/entities/role.entity'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 50, unique: true }) + username!: string; + + @Column({ length: 100, unique: true }) + email!: string; + + @Column({ name: 'password_hash', length: 255 }) + passwordHash!: string; + + @Column({ name: 'first_name', length: 100 }) + firstName!: string; + + @Column({ name: 'last_name', length: 100 }) + lastName!: string; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; + + @ManyToMany(() => Role) + @JoinTable({ + name: 'user_roles', + joinColumn: { name: 'user_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' }, + }) + roles!: Role[]; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; + + @DeleteDateColumn({ name: 'deleted_at' }) + deletedAt!: Date; +} diff --git a/frontend/app/(admin)/admin/audit-logs/page.tsx b/frontend/app/(admin)/admin/audit-logs/page.tsx new file mode 100644 index 0000000..e3a4ff6 --- /dev/null +++ b/frontend/app/(admin)/admin/audit-logs/page.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { formatDistanceToNow } from "date-fns"; +import { AuditLog } from "@/types/admin"; +import { adminApi } from "@/lib/api/admin"; +import { Loader2 } from "lucide-react"; + +export default function AuditLogsPage() { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [filters, setFilters] = useState({ + user: "", + action: "", + entity: "", + }); + + useEffect(() => { + const fetchLogs = async () => { + setLoading(true); + try { + const data = await adminApi.getAuditLogs(); + setLogs(data); + } catch (error) { + console.error("Failed to fetch audit logs", error); + } finally { + setLoading(false); + } + }; + + fetchLogs(); + }, []); + + return ( +
+
+

Audit Logs

+

View system activity and changes

+
+ + {/* Filters */} + +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Logs List */} + {loading ? ( +
+ +
+ ) : ( +
+ {logs.map((log) => ( + +
+
+
+ {log.user_name} + + {log.action} + + {log.entity_type} +
+

{log.description}

+

+ {formatDistanceToNow(new Date(log.created_at), { + addSuffix: true, + })} +

+
+ {log.ip_address && ( + + IP: {log.ip_address} + + )} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/frontend/app/(admin)/admin/numbering/[id]/edit/page.tsx b/frontend/app/(admin)/admin/numbering/[id]/edit/page.tsx new file mode 100644 index 0000000..db81512 --- /dev/null +++ b/frontend/app/(admin)/admin/numbering/[id]/edit/page.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { TemplateEditor } from "@/components/numbering/template-editor"; +import { SequenceViewer } from "@/components/numbering/sequence-viewer"; +import { numberingApi } from "@/lib/api/numbering"; +import { CreateTemplateDto } from "@/types/numbering"; +import { useRouter } from "next/navigation"; +import { Loader2 } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +export default function EditTemplatePage({ params }: { params: { id: string } }) { + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [initialData, setInitialData] = useState | null>(null); + + useEffect(() => { + const fetchTemplate = async () => { + setLoading(true); + try { + const data = await numberingApi.getTemplate(parseInt(params.id)); + if (data) { + setInitialData({ + document_type_id: data.document_type_id, + discipline_code: data.discipline_code, + template_format: data.template_format, + reset_annually: data.reset_annually, + padding_length: data.padding_length, + starting_number: 1, // Default for edit view as we don't usually reset this + }); + } + } catch (error) { + console.error("Failed to fetch template", error); + } finally { + setLoading(false); + } + }; + + fetchTemplate(); + }, [params.id]); + + const handleSave = async (data: CreateTemplateDto) => { + try { + await numberingApi.updateTemplate(parseInt(params.id), data); + router.push("/admin/numbering"); + } catch (error) { + console.error("Failed to update template", error); + alert("Failed to update template"); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+

Edit Numbering Template

+ + + + Configuration + Sequences + + + + {initialData && ( + + )} + + + + + + +
+ ); +} diff --git a/frontend/app/(admin)/admin/numbering/new/page.tsx b/frontend/app/(admin)/admin/numbering/new/page.tsx new file mode 100644 index 0000000..51a3c32 --- /dev/null +++ b/frontend/app/(admin)/admin/numbering/new/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { TemplateEditor } from "@/components/numbering/template-editor"; +import { numberingApi } from "@/lib/api/numbering"; +import { CreateTemplateDto } from "@/types/numbering"; +import { useRouter } from "next/navigation"; + +export default function NewTemplatePage() { + const router = useRouter(); + + const handleSave = async (data: CreateTemplateDto) => { + try { + await numberingApi.createTemplate(data); + router.push("/admin/numbering"); + } catch (error) { + console.error("Failed to create template", error); + alert("Failed to create template"); + } + }; + + return ( +
+

New Numbering Template

+ +
+ ); +} diff --git a/frontend/app/(admin)/admin/numbering/page.tsx b/frontend/app/(admin)/admin/numbering/page.tsx new file mode 100644 index 0000000..3e51ad9 --- /dev/null +++ b/frontend/app/(admin)/admin/numbering/page.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Edit, Eye, Loader2 } from "lucide-react"; +import Link from "next/link"; +import { NumberingTemplate } from "@/types/numbering"; +import { numberingApi } from "@/lib/api/numbering"; +import { TemplateTester } from "@/components/numbering/template-tester"; + +export default function NumberingPage() { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(true); + const [testerOpen, setTesterOpen] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState(null); + + useEffect(() => { + const fetchTemplates = async () => { + setLoading(true); + try { + const data = await numberingApi.getTemplates(); + setTemplates(data); + } catch (error) { + console.error("Failed to fetch templates", error); + } finally { + setLoading(false); + } + }; + + fetchTemplates(); + }, []); + + const handleTest = (template: NumberingTemplate) => { + setSelectedTemplate(template); + setTesterOpen(true); + }; + + return ( +
+
+
+

+ Document Numbering Configuration +

+

+ Manage document numbering templates and sequences +

+
+ + + +
+ + {loading ? ( +
+ +
+ ) : ( +
+ {templates.map((template) => ( + +
+
+
+

+ {template.document_type_name} +

+ {template.discipline_code || "All"} + + {template.is_active ? "Active" : "Inactive"} + +
+ +
+ {template.template_format} +
+ +
+
+ Example: + + {template.example_number} + +
+
+ Current Sequence: + + {template.current_number} + +
+
+ Annual Reset: + + {template.reset_annually ? "Yes" : "No"} + +
+
+ Padding: + + {template.padding_length} digits + +
+
+
+ +
+ + + + +
+
+
+ ))} +
+ )} + + +
+ ); +} diff --git a/frontend/app/(admin)/admin/organizations/page.tsx b/frontend/app/(admin)/admin/organizations/page.tsx new file mode 100644 index 0000000..df65983 --- /dev/null +++ b/frontend/app/(admin)/admin/organizations/page.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { DataTable } from "@/components/common/data-table"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Organization } from "@/types/admin"; +import { adminApi } from "@/lib/api/admin"; +import { ColumnDef } from "@tanstack/react-table"; +import { Loader2, Plus } from "lucide-react"; + +export default function OrganizationsPage() { + const [organizations, setOrganizations] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + org_code: "", + org_name: "", + org_name_th: "", + description: "", + }); + + const fetchOrgs = async () => { + setLoading(true); + try { + const data = await adminApi.getOrganizations(); + setOrganizations(data); + } catch (error) { + console.error("Failed to fetch organizations", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchOrgs(); + }, []); + + const columns: ColumnDef[] = [ + { accessorKey: "org_code", header: "Code" }, + { accessorKey: "org_name", header: "Name (EN)" }, + { accessorKey: "org_name_th", header: "Name (TH)" }, + { accessorKey: "description", header: "Description" }, + ]; + + const handleSubmit = async () => { + setIsSubmitting(true); + try { + await adminApi.createOrganization(formData); + setDialogOpen(false); + setFormData({ + org_code: "", + org_name: "", + org_name_th: "", + description: "", + }); + fetchOrgs(); + } catch (error) { + console.error(error); + alert("Failed to create organization"); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+

Organizations

+

Manage project organizations

+
+ +
+ + {loading ? ( +
+ +
+ ) : ( + + )} + + + + + Add Organization + +
+
+ + + setFormData({ ...formData, org_code: e.target.value }) + } + placeholder="e.g., PAT" + /> +
+
+ + + setFormData({ ...formData, org_name: e.target.value }) + } + /> +
+
+ + + setFormData({ ...formData, org_name_th: e.target.value }) + } + /> +
+
+ + + setFormData({ ...formData, description: e.target.value }) + } + /> +
+
+ + +
+
+
+
+
+ ); +} diff --git a/frontend/app/(admin)/admin/users/page.tsx b/frontend/app/(admin)/admin/users/page.tsx new file mode 100644 index 0000000..71d3f77 --- /dev/null +++ b/frontend/app/(admin)/admin/users/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { DataTable } from "@/components/common/data-table"; +import { UserDialog } from "@/components/admin/user-dialog"; +import { ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, Plus, Loader2 } from "lucide-react"; +import { User } from "@/types/admin"; +import { adminApi } from "@/lib/api/admin"; + +export default function UsersPage() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + + const fetchUsers = async () => { + setLoading(true); + try { + const data = await adminApi.getUsers(); + setUsers(data); + } catch (error) { + console.error("Failed to fetch users", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + const columns: ColumnDef[] = [ + { + accessorKey: "username", + header: "Username", + }, + { + accessorKey: "email", + header: "Email", + }, + { + accessorKey: "first_name", + header: "Name", + cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`, + }, + { + accessorKey: "is_active", + header: "Status", + cell: ({ row }) => ( + + {row.original.is_active ? "Active" : "Inactive"} + + ), + }, + { + id: "roles", + header: "Roles", + cell: ({ row }) => ( +
+ {row.original.roles?.map((role) => ( + + {role.role_name} + + ))} +
+ ), + }, + { + id: "actions", + cell: ({ row }) => ( + + + + + + { + setSelectedUser(row.original); + setDialogOpen(true); + }} + > + Edit + + alert("Deactivate not implemented in mock")} + > + {row.original.is_active ? "Deactivate" : "Activate"} + + + + ), + }, + ]; + + return ( +
+
+
+

User Management

+

+ Manage system users and their roles +

+
+ +
+ + {loading ? ( +
+ +
+ ) : ( + + )} + + +
+ ); +} diff --git a/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx b/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx new file mode 100644 index 0000000..8667d77 --- /dev/null +++ b/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { DSLEditor } from "@/components/workflows/dsl-editor"; +import { VisualWorkflowBuilder } from "@/components/workflows/visual-builder"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Card } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { workflowApi } from "@/lib/api/workflows"; +import { WorkflowType } from "@/types/workflow"; +import { useRouter } from "next/navigation"; +import { Loader2 } from "lucide-react"; + +export default function WorkflowEditPage({ params }: { params: { id: string } }) { + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [workflowData, setWorkflowData] = useState({ + workflow_name: "", + description: "", + workflow_type: "CORRESPONDENCE" as WorkflowType, + dsl_definition: "", + }); + + useEffect(() => { + const fetchWorkflow = async () => { + setLoading(true); + try { + const data = await workflowApi.getWorkflow(parseInt(params.id)); + if (data) { + setWorkflowData({ + workflow_name: data.workflow_name, + description: data.description, + workflow_type: data.workflow_type, + dsl_definition: data.dsl_definition, + }); + } + } catch (error) { + console.error("Failed to fetch workflow", error); + } finally { + setLoading(false); + } + }; + + fetchWorkflow(); + }, [params.id]); + + const handleSave = async () => { + setSaving(true); + try { + await workflowApi.updateWorkflow(parseInt(params.id), workflowData); + router.push("/admin/workflows"); + } catch (error) { + console.error("Failed to save workflow", error); + alert("Failed to save workflow"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Edit Workflow

+
+ + +
+
+ + +
+
+ + + setWorkflowData({ + ...workflowData, + workflow_name: e.target.value, + }) + } + /> +
+ +
+ +