251205:0000 Just start debug backend/frontend
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
2025-12-05 00:32:02 +07:00
parent 474982af87
commit 18f78f8a5e
88 changed files with 6751 additions and 1016 deletions

View File

@@ -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,
};

View File

@@ -0,0 +1,136 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class InitialSchema1701234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// 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<void> {
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`);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}