251126:1700 1.4.4
This commit is contained in:
17
.vscode/nap-dms.lcbp3.code-workspace
vendored
17
.vscode/nap-dms.lcbp3.code-workspace
vendored
@@ -21,10 +21,21 @@
|
|||||||
"username": "root"
|
"username": "root"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor.fontSize": 15,
|
"editor.fontSize": 16,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
|
"terminal": true
|
||||||
},
|
},
|
||||||
"editor.codeActions.triggerOnFocusChange": true
|
"editor.codeActions.triggerOnFocusChange": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.minimap.sectionHeaderFontSize": 12,
|
||||||
|
"terminal.integrated.fontSize": 15,
|
||||||
|
"workbench.colorTheme": "Default Dark Modern",
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"terminal.background": "#07003c",
|
||||||
|
"terminal.foreground": "#ffffff",
|
||||||
|
"terminalCursor.background": "#ffffff",
|
||||||
|
"terminalCursor.foreground": "#eeff00"
|
||||||
|
},
|
||||||
|
"geminicodeassist.agentYoloMode": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.sqltools": "explicit"
|
||||||
},
|
},
|
||||||
"eslint.validate": [
|
"eslint.validate": [
|
||||||
"javascript",
|
"javascript",
|
||||||
@@ -25,5 +26,9 @@
|
|||||||
"username": "root"
|
"username": "root"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor.fontSize": 16
|
"editor.fontSize": 16,
|
||||||
|
"editor.fontLigatures": false,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.minimap.sectionHeaderFontSize": 12,
|
||||||
|
"markdown.extension.print.theme": "dark"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// File: src/app.module.ts
|
// File: src/app.module.ts
|
||||||
// บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1)
|
// บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1)
|
||||||
// บันทึกการแก้ไข: เพิ่ม MonitoringModule และ WinstonModule (T6.3)
|
// บันทึกการแก้ไข: เพิ่ม MonitoringModule และ WinstonModule (T6.3)
|
||||||
|
// เพิ่ม MasterModule
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
@@ -9,26 +10,28 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
import { WinstonModule } from 'nest-winston'; // ✅ Import WinstonModule
|
import { WinstonModule } from 'nest-winston';
|
||||||
import { redisStore } from 'cache-manager-redis-yet';
|
import { redisStore } from 'cache-manager-redis-yet';
|
||||||
|
import { RedisModule } from '@nestjs-modules/ioredis';
|
||||||
|
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { envValidationSchema } from './common/config/env.validation.js';
|
import { envValidationSchema } from './common/config/env.validation.js';
|
||||||
import redisConfig from './common/config/redis.config';
|
import redisConfig from './common/config/redis.config';
|
||||||
import { winstonConfig } from './modules/monitoring/logger/winston.config'; // ✅ Import Config
|
import { winstonConfig } from './modules/monitoring/logger/winston.config';
|
||||||
|
|
||||||
// Entities & Interceptors
|
// Entities & Interceptors
|
||||||
import { AuditLog } from './common/entities/audit-log.entity';
|
import { AuditLog } from './common/entities/audit-log.entity';
|
||||||
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor';
|
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor';
|
||||||
import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
||||||
// import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor';
|
|
||||||
|
|
||||||
// Modules
|
// Modules
|
||||||
|
import { AuthModule } from './common/auth/auth.module.js';
|
||||||
import { UserModule } from './modules/user/user.module';
|
import { UserModule } from './modules/user/user.module';
|
||||||
import { ProjectModule } from './modules/project/project.module';
|
import { ProjectModule } from './modules/project/project.module';
|
||||||
|
import { MasterModule } from './modules/master/master.module'; // [NEW] ✅ เพิ่ม MasterModule
|
||||||
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
||||||
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
||||||
import { AuthModule } from './common/auth/auth.module.js';
|
|
||||||
import { JsonSchemaModule } from './modules/json-schema/json-schema.module.js';
|
import { JsonSchemaModule } from './modules/json-schema/json-schema.module.js';
|
||||||
import { WorkflowEngineModule } from './modules/workflow-engine/workflow-engine.module';
|
import { WorkflowEngineModule } from './modules/workflow-engine/workflow-engine.module';
|
||||||
import { CorrespondenceModule } from './modules/correspondence/correspondence.module';
|
import { CorrespondenceModule } from './modules/correspondence/correspondence.module';
|
||||||
@@ -37,12 +40,10 @@ import { DrawingModule } from './modules/drawing/drawing.module';
|
|||||||
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
||||||
import { CirculationModule } from './modules/circulation/circulation.module';
|
import { CirculationModule } from './modules/circulation/circulation.module';
|
||||||
import { NotificationModule } from './modules/notification/notification.module';
|
import { NotificationModule } from './modules/notification/notification.module';
|
||||||
// ✅ Import Monitoring Module
|
|
||||||
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
||||||
import { ResilienceModule } from './common/resilience/resilience.module'; // ✅ Import
|
import { ResilienceModule } from './common/resilience/resilience.module';
|
||||||
// ... imports
|
import { SearchModule } from './modules/search/search.module';
|
||||||
import { SearchModule } from './modules/search/search.module'; // ✅ Import
|
|
||||||
import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
// 1. Setup Config Module พร้อม Validation
|
// 1. Setup Config Module พร้อม Validation
|
||||||
@@ -80,7 +81,7 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 📝 Setup Winston Logger (Structured Logging) [Req 6.10]
|
// 📝 Setup Winston Logger
|
||||||
WinstonModule.forRoot(winstonConfig),
|
WinstonModule.forRoot(winstonConfig),
|
||||||
|
|
||||||
// 2. Setup TypeORM (MariaDB)
|
// 2. Setup TypeORM (MariaDB)
|
||||||
@@ -114,7 +115,8 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
// [NEW] Setup Redis Module (สำหรับ InjectRedis)
|
|
||||||
|
// Setup Redis Module (for InjectRedis)
|
||||||
RedisModule.forRootAsync({
|
RedisModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
@@ -126,25 +128,27 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
|
|||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
// 📊 Register Monitoring Module (Health & Metrics) [Req 6.10]
|
|
||||||
MonitoringModule,
|
|
||||||
|
|
||||||
// Feature Modules
|
// 📊 Monitoring & Resilience
|
||||||
|
MonitoringModule,
|
||||||
|
ResilienceModule,
|
||||||
|
|
||||||
|
// 📦 Feature Modules
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
ProjectModule,
|
ProjectModule,
|
||||||
|
MasterModule, // ✅ [NEW] Register MasterModule here
|
||||||
FileStorageModule,
|
FileStorageModule,
|
||||||
DocumentNumberingModule,
|
DocumentNumberingModule,
|
||||||
JsonSchemaModule,
|
JsonSchemaModule,
|
||||||
WorkflowEngineModule,
|
WorkflowEngineModule,
|
||||||
CorrespondenceModule,
|
CorrespondenceModule,
|
||||||
RfaModule, // 👈 ต้องมี
|
RfaModule,
|
||||||
DrawingModule, // 👈 ต้องมี
|
DrawingModule,
|
||||||
TransmittalModule, // 👈 ต้องมี
|
TransmittalModule,
|
||||||
CirculationModule, // 👈 ต้องมี
|
CirculationModule,
|
||||||
SearchModule, // ✅ Register Module
|
SearchModule,
|
||||||
NotificationModule, // 👈 ต้องมี
|
NotificationModule,
|
||||||
ResilienceModule, // ✅ Register Module
|
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -164,17 +168,6 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
|
|||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: AuditLogInterceptor,
|
useClass: AuditLogInterceptor,
|
||||||
},
|
},
|
||||||
// 🔄 4. Register Idempotency (ถ้าต้องการ Global)
|
|
||||||
// {
|
|
||||||
// provide: APP_INTERCEPTOR,
|
|
||||||
// useClass: IdempotencyInterceptor,
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
||||||
/*วิธีใช้งาน
|
|
||||||
เมื่อต้องการเปิด Maintenance Mode ให้ Admin (หรือคุณ) ยิงคำสั่งเข้า Redis หรือสร้าง API เพื่อ Set ค่า: SET system:maintenance_mode true (หรือ "true")
|
|
||||||
|
|
||||||
ระบบจะตอบกลับด้วย 503 Service Unavailable ทันที ยกเว้น Controller ที่คุณใส่ @BypassMaintenance() ไว้ครับ
|
|
||||||
*/
|
|
||||||
|
|||||||
29
backend/src/modules/master/dto/create-discipline.dto.ts
Normal file
29
backend/src/modules/master/dto/create-discipline.dto.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateDisciplineDto {
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
contractId!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
disciplineCode!: string; // เช่น 'STR', 'ARC'
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
codeNameTh?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
codeNameEn?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
23
backend/src/modules/master/dto/create-sub-type.dto.ts
Normal file
23
backend/src/modules/master/dto/create-sub-type.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { IsInt, IsString, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateSubTypeDto {
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
contractId!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
correspondenceTypeId!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
subTypeCode!: string; // เช่น 'MAT'
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
subTypeName?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
subTypeNumber?: string; // เช่น '11'
|
||||||
|
}
|
||||||
15
backend/src/modules/master/dto/save-number-format.dto.ts
Normal file
15
backend/src/modules/master/dto/save-number-format.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { IsInt, IsString, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class SaveNumberFormatDto {
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
projectId!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
correspondenceTypeId!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
formatTemplate!: string; // เช่น '{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}'
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
Put,
|
||||||
Body,
|
Body,
|
||||||
Patch,
|
Patch,
|
||||||
Param,
|
Param,
|
||||||
@@ -12,22 +13,28 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||||
import { MasterService } from './master.service';
|
import { MasterService } from './master.service';
|
||||||
|
|
||||||
|
// DTOs (สมมติว่ามีการสร้างไฟล์เหล่านี้แล้วตามแผนงาน)
|
||||||
import { CreateTagDto } from './dto/create-tag.dto';
|
import { CreateTagDto } from './dto/create-tag.dto';
|
||||||
import { UpdateTagDto } from './dto/update-tag.dto';
|
import { UpdateTagDto } from './dto/update-tag.dto';
|
||||||
import { SearchTagDto } from './dto/search-tag.dto';
|
import { SearchTagDto } from './dto/search-tag.dto';
|
||||||
|
import { CreateDisciplineDto } from './dto/create-discipline.dto'; // [New]
|
||||||
|
import { CreateSubTypeDto } from './dto/create-sub-type.dto'; // [New]
|
||||||
|
import { SaveNumberFormatDto } from './dto/save-number-format.dto'; // [New]
|
||||||
|
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
|
||||||
@ApiTags('Master Data')
|
@ApiTags('Master Data')
|
||||||
@Controller('master')
|
@Controller('master')
|
||||||
@UseGuards(JwtAuthGuard) // บังคับ Login ทุก Endpoint
|
@UseGuards(JwtAuthGuard)
|
||||||
export class MasterController {
|
export class MasterController {
|
||||||
constructor(private readonly masterService: MasterService) {}
|
constructor(private readonly masterService: MasterService) {}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// 📦 Dropdowns Endpoints (Read-Only for Frontend)
|
// 📦 Common Dropdowns (Read-Only)
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
@Get('correspondence-types')
|
@Get('correspondence-types')
|
||||||
@@ -67,7 +74,82 @@ export class MasterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// 🏷️ Tag Management Endpoints
|
// 🏗️ Disciplines Management (Req 6B)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
@Get('disciplines')
|
||||||
|
@ApiOperation({ summary: 'Get disciplines (filter by contract optional)' })
|
||||||
|
@ApiQuery({ name: 'contractId', required: false, type: Number })
|
||||||
|
getDisciplines(@Query('contractId') contractId?: number) {
|
||||||
|
return this.masterService.findAllDisciplines(contractId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('disciplines')
|
||||||
|
@RequirePermission('master_data.manage') // สิทธิ์ Admin
|
||||||
|
@ApiOperation({ summary: 'Create a new discipline' })
|
||||||
|
createDiscipline(@Body() dto: CreateDisciplineDto) {
|
||||||
|
return this.masterService.createDiscipline(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('disciplines/:id')
|
||||||
|
@RequirePermission('master_data.manage')
|
||||||
|
@ApiOperation({ summary: 'Delete a discipline' })
|
||||||
|
deleteDiscipline(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
return this.masterService.deleteDiscipline(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 📑 Correspondence Sub-Types (Req 6B)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
@Get('sub-types')
|
||||||
|
@ApiOperation({ summary: 'Get sub-types (filter by contract/type optional)' })
|
||||||
|
@ApiQuery({ name: 'contractId', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'typeId', required: false, type: Number })
|
||||||
|
getSubTypes(
|
||||||
|
@Query('contractId') contractId?: number,
|
||||||
|
@Query('typeId') typeId?: number,
|
||||||
|
) {
|
||||||
|
return this.masterService.findAllSubTypes(contractId, typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sub-types')
|
||||||
|
@RequirePermission('master_data.manage')
|
||||||
|
@ApiOperation({ summary: 'Create/Map a new sub-type' })
|
||||||
|
createSubType(@Body() dto: CreateSubTypeDto) {
|
||||||
|
return this.masterService.createSubType(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('sub-types/:id')
|
||||||
|
@RequirePermission('master_data.manage')
|
||||||
|
@ApiOperation({ summary: 'Delete a sub-type' })
|
||||||
|
deleteSubType(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
return this.masterService.deleteSubType(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 🔢 Numbering Formats (Admin Config)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
@Get('numbering-formats')
|
||||||
|
@RequirePermission('master_data.manage') // ข้อมูล config ควรสงวนสิทธิ์
|
||||||
|
@ApiOperation({ summary: 'Get numbering format for specific project/type' })
|
||||||
|
getNumberFormat(
|
||||||
|
@Query('projectId', ParseIntPipe) projectId: number,
|
||||||
|
@Query('typeId', ParseIntPipe) typeId: number,
|
||||||
|
) {
|
||||||
|
return this.masterService.findNumberFormat(projectId, typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('numbering-formats')
|
||||||
|
@RequirePermission('system.manage_all') // เฉพาะ Superadmin/System Admin
|
||||||
|
@ApiOperation({ summary: 'Save or Update numbering format template' })
|
||||||
|
saveNumberFormat(@Body() dto: SaveNumberFormatDto) {
|
||||||
|
return this.masterService.saveNumberFormat(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 🏷️ Tag Management
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
@Get('tags')
|
@Get('tags')
|
||||||
@@ -83,21 +165,21 @@ export class MasterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('tags')
|
@Post('tags')
|
||||||
@RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์ (Admin/Doc Control)
|
@RequirePermission('master_data.tag.manage')
|
||||||
@ApiOperation({ summary: 'Create a new tag' })
|
@ApiOperation({ summary: 'Create a new tag' })
|
||||||
createTag(@Body() dto: CreateTagDto) {
|
createTag(@Body() dto: CreateTagDto) {
|
||||||
return this.masterService.createTag(dto);
|
return this.masterService.createTag(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('tags/:id')
|
@Patch('tags/:id')
|
||||||
@RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์
|
@RequirePermission('master_data.tag.manage')
|
||||||
@ApiOperation({ summary: 'Update a tag' })
|
@ApiOperation({ summary: 'Update a tag' })
|
||||||
updateTag(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateTagDto) {
|
updateTag(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateTagDto) {
|
||||||
return this.masterService.updateTag(id, dto);
|
return this.masterService.updateTag(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('tags/:id')
|
@Delete('tags/:id')
|
||||||
@RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์
|
@RequirePermission('master_data.tag.manage')
|
||||||
@ApiOperation({ summary: 'Delete a tag' })
|
@ApiOperation({ summary: 'Delete a tag' })
|
||||||
deleteTag(@Param('id', ParseIntPipe) id: number) {
|
deleteTag(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.masterService.deleteTag(id);
|
return this.masterService.deleteTag(id);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { MasterService } from './master.service';
|
import { MasterService } from './master.service';
|
||||||
import { MasterController } from './master.controller';
|
import { MasterController } from './master.controller';
|
||||||
|
|
||||||
// Import Entities
|
// Import Entities เดิม
|
||||||
import { Tag } from './entities/tag.entity';
|
import { Tag } from './entities/tag.entity';
|
||||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||||
@@ -14,6 +14,12 @@ import { RfaStatusCode } from '../rfa/entities/rfa-status-code.entity';
|
|||||||
import { RfaApproveCode } from '../rfa/entities/rfa-approve-code.entity';
|
import { RfaApproveCode } from '../rfa/entities/rfa-approve-code.entity';
|
||||||
import { CirculationStatusCode } from '../circulation/entities/circulation-status-code.entity';
|
import { CirculationStatusCode } from '../circulation/entities/circulation-status-code.entity';
|
||||||
|
|
||||||
|
// [New v1.4.4] Import Entities ใหม่ตาม Req 6B และ T2.6
|
||||||
|
import { Discipline } from './entities/discipline.entity';
|
||||||
|
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||||
|
// Entity นี้อาจจะอยู่ใน module document-numbering แต่นำมาใช้ที่นี่เพื่อการจัดการ Master Data
|
||||||
|
import { DocumentNumberFormat } from '../document-numbering/entities/document-number-format.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
@@ -24,10 +30,14 @@ import { CirculationStatusCode } from '../circulation/entities/circulation-statu
|
|||||||
RfaStatusCode,
|
RfaStatusCode,
|
||||||
RfaApproveCode,
|
RfaApproveCode,
|
||||||
CirculationStatusCode,
|
CirculationStatusCode,
|
||||||
|
// [New] Register Repositories
|
||||||
|
Discipline,
|
||||||
|
CorrespondenceSubType,
|
||||||
|
DocumentNumberFormat,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [MasterController],
|
controllers: [MasterController],
|
||||||
providers: [MasterService],
|
providers: [MasterService],
|
||||||
exports: [MasterService], // Export เผื่อ Module อื่นต้องใช้
|
exports: [MasterService],
|
||||||
})
|
})
|
||||||
export class MasterModule {}
|
export class MasterModule {}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// File: src/modules/master/master.service.ts
|
// File: src/modules/master/master.service.ts
|
||||||
|
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
ConflictException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
@@ -13,132 +17,220 @@ import { RfaApproveCode } from '../rfa/entities/rfa-approve-code.entity';
|
|||||||
import { CirculationStatusCode } from '../circulation/entities/circulation-status-code.entity';
|
import { CirculationStatusCode } from '../circulation/entities/circulation-status-code.entity';
|
||||||
import { Tag } from './entities/tag.entity';
|
import { Tag } from './entities/tag.entity';
|
||||||
|
|
||||||
|
// [New] Entities
|
||||||
|
import { Discipline } from './entities/discipline.entity';
|
||||||
|
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||||
|
import { DocumentNumberFormat } from '../document-numbering/entities/document-number-format.entity';
|
||||||
|
|
||||||
// Import DTOs
|
// Import DTOs
|
||||||
import { CreateTagDto } from './dto/create-tag.dto';
|
import { CreateTagDto } from './dto/create-tag.dto';
|
||||||
import { UpdateTagDto } from './dto/update-tag.dto';
|
import { UpdateTagDto } from './dto/update-tag.dto';
|
||||||
import { SearchTagDto } from './dto/search-tag.dto';
|
import { SearchTagDto } from './dto/search-tag.dto';
|
||||||
|
import { CreateDisciplineDto } from './dto/create-discipline.dto';
|
||||||
|
import { CreateSubTypeDto } from './dto/create-sub-type.dto';
|
||||||
|
import { SaveNumberFormatDto } from './dto/save-number-format.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MasterService {
|
export class MasterService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(CorrespondenceType)
|
@InjectRepository(CorrespondenceType)
|
||||||
private readonly corrTypeRepo: Repository<CorrespondenceType>,
|
private readonly corrTypeRepo: Repository<CorrespondenceType>,
|
||||||
|
|
||||||
@InjectRepository(CorrespondenceStatus)
|
@InjectRepository(CorrespondenceStatus)
|
||||||
private readonly corrStatusRepo: Repository<CorrespondenceStatus>,
|
private readonly corrStatusRepo: Repository<CorrespondenceStatus>,
|
||||||
|
|
||||||
@InjectRepository(RfaType)
|
@InjectRepository(RfaType)
|
||||||
private readonly rfaTypeRepo: Repository<RfaType>,
|
private readonly rfaTypeRepo: Repository<RfaType>,
|
||||||
|
|
||||||
@InjectRepository(RfaStatusCode)
|
@InjectRepository(RfaStatusCode)
|
||||||
private readonly rfaStatusRepo: Repository<RfaStatusCode>,
|
private readonly rfaStatusRepo: Repository<RfaStatusCode>,
|
||||||
|
|
||||||
@InjectRepository(RfaApproveCode)
|
@InjectRepository(RfaApproveCode)
|
||||||
private readonly rfaApproveRepo: Repository<RfaApproveCode>,
|
private readonly rfaApproveRepo: Repository<RfaApproveCode>,
|
||||||
|
|
||||||
@InjectRepository(CirculationStatusCode)
|
@InjectRepository(CirculationStatusCode)
|
||||||
private readonly circulationStatusRepo: Repository<CirculationStatusCode>,
|
private readonly circulationStatusRepo: Repository<CirculationStatusCode>,
|
||||||
|
|
||||||
@InjectRepository(Tag)
|
@InjectRepository(Tag)
|
||||||
private readonly tagRepo: Repository<Tag>,
|
private readonly tagRepo: Repository<Tag>,
|
||||||
|
|
||||||
|
// [New] Repositories
|
||||||
|
@InjectRepository(Discipline)
|
||||||
|
private readonly disciplineRepo: Repository<Discipline>,
|
||||||
|
@InjectRepository(CorrespondenceSubType)
|
||||||
|
private readonly subTypeRepo: Repository<CorrespondenceSubType>,
|
||||||
|
@InjectRepository(DocumentNumberFormat)
|
||||||
|
private readonly formatRepo: Repository<DocumentNumberFormat>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// =================================================================
|
// ... (Method เดิม: findAllCorrespondenceTypes, findAllCorrespondenceStatuses, ฯลฯ เก็บไว้เหมือนเดิม) ...
|
||||||
// ✉️ Correspondence Master Data
|
// หมายเหตุ: ตรวจสอบว่า Entity ใช้ชื่อ property ว่า isActive หรือ is_active (ใน SQL เป็น is_active แต่ใน Entity มักเป็น isActive)
|
||||||
// =================================================================
|
// โค้ดเดิมใช้ `where: { isActive: true }` ซึ่งถูกต้องถ้า Entity map column name แล้ว
|
||||||
|
|
||||||
async findAllCorrespondenceTypes() {
|
async findAllCorrespondenceTypes() {
|
||||||
return this.corrTypeRepo.find({
|
return this.corrTypeRepo.find({
|
||||||
where: { isActive: true }, // ✅ แก้เป็น camelCase
|
where: { isActive: true },
|
||||||
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllCorrespondenceStatuses() {
|
async findAllCorrespondenceStatuses() {
|
||||||
return this.corrStatusRepo.find({
|
return this.corrStatusRepo.find({
|
||||||
where: { isActive: true }, // ✅ แก้เป็น camelCase
|
where: { isActive: true },
|
||||||
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 📐 RFA Master Data
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
async findAllRfaTypes() {
|
async findAllRfaTypes() {
|
||||||
return this.rfaTypeRepo.find({
|
return this.rfaTypeRepo.find({
|
||||||
where: { isActive: true }, // ✅ แก้เป็น camelCase
|
where: { isActive: true },
|
||||||
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllRfaStatuses() {
|
async findAllRfaStatuses() {
|
||||||
return this.rfaStatusRepo.find({
|
return this.rfaStatusRepo.find({
|
||||||
where: { isActive: true }, // ✅ แก้เป็น camelCase
|
where: { isActive: true },
|
||||||
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllRfaApproveCodes() {
|
async findAllRfaApproveCodes() {
|
||||||
return this.rfaApproveRepo.find({
|
return this.rfaApproveRepo.find({
|
||||||
where: { isActive: true }, // ✅ แก้เป็น camelCase
|
where: { isActive: true },
|
||||||
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🔄 Circulation Master Data
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
async findAllCirculationStatuses() {
|
async findAllCirculationStatuses() {
|
||||||
return this.circulationStatusRepo.find({
|
return this.circulationStatusRepo.find({
|
||||||
where: { isActive: true }, // ✅ แก้เป็น camelCase
|
where: { isActive: true },
|
||||||
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// 🏷️ Tag Management (CRUD)
|
// 🏗️ Disciplines Logic
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
|
async findAllDisciplines(contractId?: number) {
|
||||||
|
const query = this.disciplineRepo
|
||||||
|
.createQueryBuilder('d')
|
||||||
|
.leftJoinAndSelect('d.contract', 'c')
|
||||||
|
.orderBy('d.disciplineCode', 'ASC');
|
||||||
|
|
||||||
|
if (contractId) {
|
||||||
|
query.where('d.contractId = :contractId', { contractId });
|
||||||
|
}
|
||||||
|
// เพิ่มเงื่อนไข Active หากต้องการ
|
||||||
|
query.andWhere('d.isActive = :isActive', { isActive: true });
|
||||||
|
|
||||||
|
return query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDiscipline(dto: CreateDisciplineDto) {
|
||||||
|
const exists = await this.disciplineRepo.findOne({
|
||||||
|
where: { contractId: dto.contractId, disciplineCode: dto.disciplineCode },
|
||||||
|
});
|
||||||
|
if (exists)
|
||||||
|
throw new ConflictException(
|
||||||
|
'Discipline code already exists in this contract',
|
||||||
|
);
|
||||||
|
|
||||||
|
const discipline = this.disciplineRepo.create(dto);
|
||||||
|
return this.disciplineRepo.save(discipline);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDiscipline(id: number) {
|
||||||
|
const result = await this.disciplineRepo.delete(id);
|
||||||
|
if (result.affected === 0)
|
||||||
|
throw new NotFoundException(`Discipline ID ${id} not found`);
|
||||||
|
return { deleted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 📑 Sub-Types Logic
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
async findAllSubTypes(contractId?: number, typeId?: number) {
|
||||||
|
const query = this.subTypeRepo
|
||||||
|
.createQueryBuilder('st')
|
||||||
|
.leftJoinAndSelect('st.contract', 'c')
|
||||||
|
.leftJoinAndSelect('st.correspondenceType', 'ct')
|
||||||
|
.orderBy('st.subTypeCode', 'ASC');
|
||||||
|
|
||||||
|
if (contractId)
|
||||||
|
query.andWhere('st.contractId = :contractId', { contractId });
|
||||||
|
if (typeId) query.andWhere('st.correspondenceTypeId = :typeId', { typeId });
|
||||||
|
|
||||||
|
return query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSubType(dto: CreateSubTypeDto) {
|
||||||
|
// อาจจะเช็ค Duplicate code ด้วย logic คล้าย discipline
|
||||||
|
const subType = this.subTypeRepo.create(dto);
|
||||||
|
return this.subTypeRepo.save(subType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSubType(id: number) {
|
||||||
|
const result = await this.subTypeRepo.delete(id);
|
||||||
|
if (result.affected === 0)
|
||||||
|
throw new NotFoundException(`Sub-type ID ${id} not found`);
|
||||||
|
return { deleted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 🔢 Numbering Formats Logic
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
async findNumberFormat(projectId: number, typeId: number) {
|
||||||
|
const format = await this.formatRepo.findOne({
|
||||||
|
where: { projectId, correspondenceTypeId: typeId },
|
||||||
|
});
|
||||||
|
if (!format) {
|
||||||
|
// Optional: Return default format structure or null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveNumberFormat(dto: SaveNumberFormatDto) {
|
||||||
|
// Check if exists (Upsert)
|
||||||
|
let format = await this.formatRepo.findOne({
|
||||||
|
where: {
|
||||||
|
projectId: dto.projectId,
|
||||||
|
correspondenceTypeId: dto.correspondenceTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (format) {
|
||||||
|
format.formatTemplate = dto.formatTemplate;
|
||||||
|
// format.updatedBy = ... (ถ้ามี)
|
||||||
|
} else {
|
||||||
|
format = this.formatRepo.create({
|
||||||
|
projectId: dto.projectId,
|
||||||
|
correspondenceTypeId: dto.correspondenceTypeId,
|
||||||
|
formatTemplate: dto.formatTemplate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatRepo.save(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (Tag Logic เดิม คงไว้ตามปกติ) ...
|
||||||
async findAllTags(query?: SearchTagDto) {
|
async findAllTags(query?: SearchTagDto) {
|
||||||
const qb = this.tagRepo.createQueryBuilder('tag');
|
const qb = this.tagRepo.createQueryBuilder('tag');
|
||||||
|
|
||||||
if (query?.search) {
|
if (query?.search) {
|
||||||
qb.where('tag.tag_name LIKE :search OR tag.description LIKE :search', {
|
qb.where('tag.tag_name LIKE :search OR tag.description LIKE :search', {
|
||||||
search: `%${query.search}%`,
|
search: `%${query.search}%`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
qb.orderBy('tag.tag_name', 'ASC');
|
qb.orderBy('tag.tag_name', 'ASC');
|
||||||
|
|
||||||
if (query?.page && query?.limit) {
|
if (query?.page && query?.limit) {
|
||||||
const page = query.page;
|
const page = query.page;
|
||||||
const limit = query.limit;
|
const limit = query.limit;
|
||||||
qb.skip((page - 1) * limit).take(limit);
|
qb.skip((page - 1) * limit).take(limit);
|
||||||
}
|
|
||||||
|
|
||||||
if (query?.page && query?.limit) {
|
|
||||||
const [items, total] = await qb.getManyAndCount();
|
const [items, total] = await qb.getManyAndCount();
|
||||||
return {
|
return {
|
||||||
data: items,
|
data: items,
|
||||||
meta: {
|
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||||
total,
|
|
||||||
page: query.page,
|
|
||||||
limit: query.limit,
|
|
||||||
totalPages: Math.ceil(total / query.limit),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return qb.getMany();
|
return qb.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneTag(id: number) {
|
async findOneTag(id: number) {
|
||||||
const tag = await this.tagRepo.findOne({ where: { id } });
|
const tag = await this.tagRepo.findOne({ where: { id } });
|
||||||
if (!tag) {
|
if (!tag) throw new NotFoundException(`Tag with ID "${id}" not found`);
|
||||||
throw new NotFoundException(`Tag with ID "${id}" not found`);
|
|
||||||
}
|
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user