diff --git a/.vscode/nap-dms.lcbp3.code-workspace b/.vscode/nap-dms.lcbp3.code-workspace index 2bcef52..c7ca363 100644 --- a/.vscode/nap-dms.lcbp3.code-workspace +++ b/.vscode/nap-dms.lcbp3.code-workspace @@ -21,10 +21,21 @@ "username": "root" } ], - "editor.fontSize": 15, + "editor.fontSize": 16, "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, } } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3bf0f0a..c5d5f52 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,8 @@ { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll.eslint": "explicit", + "source.fixAll.sqltools": "explicit" }, "eslint.validate": [ "javascript", @@ -25,5 +26,9 @@ "username": "root" } ], - "editor.fontSize": 16 + "editor.fontSize": 16, + "editor.fontLigatures": false, + "editor.tabSize": 2, + "editor.minimap.sectionHeaderFontSize": 12, + "markdown.extension.print.theme": "dark" } \ No newline at end of file diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index cd676a9..fe23321 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,6 +1,7 @@ // File: src/app.module.ts // บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1) // บันทึกการแก้ไข: เพิ่ม MonitoringModule และ WinstonModule (T6.3) +// เพิ่ม MasterModule import { Module } from '@nestjs/common'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; @@ -9,26 +10,28 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BullModule } from '@nestjs/bullmq'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; 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 { RedisModule } from '@nestjs-modules/ioredis'; + import { AppController } from './app.controller'; import { AppService } from './app.service'; import { envValidationSchema } from './common/config/env.validation.js'; 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 import { AuditLog } from './common/entities/audit-log.entity'; import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor'; import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard'; -// import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor'; // Modules +import { AuthModule } from './common/auth/auth.module.js'; import { UserModule } from './modules/user/user.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 { 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 { WorkflowEngineModule } from './modules/workflow-engine/workflow-engine.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 { CirculationModule } from './modules/circulation/circulation.module'; import { NotificationModule } from './modules/notification/notification.module'; -// ✅ Import Monitoring Module import { MonitoringModule } from './modules/monitoring/monitoring.module'; -import { ResilienceModule } from './common/resilience/resilience.module'; // ✅ Import -// ... imports -import { SearchModule } from './modules/search/search.module'; // ✅ Import -import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW] +import { ResilienceModule } from './common/resilience/resilience.module'; +import { SearchModule } from './modules/search/search.module'; + @Module({ imports: [ // 1. Setup Config Module พร้อม Validation @@ -80,7 +81,7 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW] inject: [ConfigService], }), - // 📝 Setup Winston Logger (Structured Logging) [Req 6.10] + // 📝 Setup Winston Logger WinstonModule.forRoot(winstonConfig), // 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({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ @@ -126,25 +128,27 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW] }), inject: [ConfigService], }), - // 📊 Register Monitoring Module (Health & Metrics) [Req 6.10] - MonitoringModule, - // Feature Modules + // 📊 Monitoring & Resilience + MonitoringModule, + ResilienceModule, + + // 📦 Feature Modules AuthModule, UserModule, ProjectModule, + MasterModule, // ✅ [NEW] Register MasterModule here FileStorageModule, DocumentNumberingModule, JsonSchemaModule, WorkflowEngineModule, CorrespondenceModule, - RfaModule, // 👈 ต้องมี - DrawingModule, // 👈 ต้องมี - TransmittalModule, // 👈 ต้องมี - CirculationModule, // 👈 ต้องมี - SearchModule, // ✅ Register Module - NotificationModule, // 👈 ต้องมี - ResilienceModule, // ✅ Register Module + RfaModule, + DrawingModule, + TransmittalModule, + CirculationModule, + SearchModule, + NotificationModule, ], controllers: [AppController], providers: [ @@ -164,17 +168,6 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW] provide: APP_INTERCEPTOR, useClass: AuditLogInterceptor, }, - // 🔄 4. Register Idempotency (ถ้าต้องการ Global) - // { - // provide: APP_INTERCEPTOR, - // useClass: IdempotencyInterceptor, - // }, ], }) export class AppModule {} - -/*วิธีใช้งาน -เมื่อต้องการเปิด Maintenance Mode ให้ Admin (หรือคุณ) ยิงคำสั่งเข้า Redis หรือสร้าง API เพื่อ Set ค่า: SET system:maintenance_mode true (หรือ "true") - -ระบบจะตอบกลับด้วย 503 Service Unavailable ทันที ยกเว้น Controller ที่คุณใส่ @BypassMaintenance() ไว้ครับ -*/ diff --git a/backend/src/modules/master/dto/create-discipline.dto.ts b/backend/src/modules/master/dto/create-discipline.dto.ts new file mode 100644 index 0000000..6f064eb --- /dev/null +++ b/backend/src/modules/master/dto/create-discipline.dto.ts @@ -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; +} diff --git a/backend/src/modules/master/dto/create-sub-type.dto.ts b/backend/src/modules/master/dto/create-sub-type.dto.ts new file mode 100644 index 0000000..befb1b1 --- /dev/null +++ b/backend/src/modules/master/dto/create-sub-type.dto.ts @@ -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' +} diff --git a/backend/src/modules/master/dto/save-number-format.dto.ts b/backend/src/modules/master/dto/save-number-format.dto.ts new file mode 100644 index 0000000..6322184 --- /dev/null +++ b/backend/src/modules/master/dto/save-number-format.dto.ts @@ -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}' +} diff --git a/backend/src/modules/master/master.controller.ts b/backend/src/modules/master/master.controller.ts index f52db0f..1e8ac71 100644 --- a/backend/src/modules/master/master.controller.ts +++ b/backend/src/modules/master/master.controller.ts @@ -4,6 +4,7 @@ import { Controller, Get, Post, + Put, Body, Patch, Param, @@ -12,22 +13,28 @@ import { UseGuards, ParseIntPipe, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { MasterService } from './master.service'; + +// DTOs (สมมติว่ามีการสร้างไฟล์เหล่านี้แล้วตามแผนงาน) import { CreateTagDto } from './dto/create-tag.dto'; import { UpdateTagDto } from './dto/update-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 { RequirePermission } from '../../common/decorators/require-permission.decorator'; @ApiTags('Master Data') @Controller('master') -@UseGuards(JwtAuthGuard) // บังคับ Login ทุก Endpoint +@UseGuards(JwtAuthGuard) export class MasterController { constructor(private readonly masterService: MasterService) {} // ================================================================= - // 📦 Dropdowns Endpoints (Read-Only for Frontend) + // 📦 Common Dropdowns (Read-Only) // ================================================================= @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') @@ -83,21 +165,21 @@ export class MasterController { } @Post('tags') - @RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์ (Admin/Doc Control) + @RequirePermission('master_data.tag.manage') @ApiOperation({ summary: 'Create a new tag' }) createTag(@Body() dto: CreateTagDto) { return this.masterService.createTag(dto); } @Patch('tags/:id') - @RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์ + @RequirePermission('master_data.tag.manage') @ApiOperation({ summary: 'Update a tag' }) updateTag(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateTagDto) { return this.masterService.updateTag(id, dto); } @Delete('tags/:id') - @RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์ + @RequirePermission('master_data.tag.manage') @ApiOperation({ summary: 'Delete a tag' }) deleteTag(@Param('id', ParseIntPipe) id: number) { return this.masterService.deleteTag(id); diff --git a/backend/src/modules/master/master.module.ts b/backend/src/modules/master/master.module.ts index 050e235..691447b 100644 --- a/backend/src/modules/master/master.module.ts +++ b/backend/src/modules/master/master.module.ts @@ -5,7 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { MasterService } from './master.service'; import { MasterController } from './master.controller'; -// Import Entities +// Import Entities เดิม import { Tag } from './entities/tag.entity'; import { CorrespondenceType } from '../correspondence/entities/correspondence-type.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 { 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({ imports: [ TypeOrmModule.forFeature([ @@ -24,10 +30,14 @@ import { CirculationStatusCode } from '../circulation/entities/circulation-statu RfaStatusCode, RfaApproveCode, CirculationStatusCode, + // [New] Register Repositories + Discipline, + CorrespondenceSubType, + DocumentNumberFormat, ]), ], controllers: [MasterController], providers: [MasterService], - exports: [MasterService], // Export เผื่อ Module อื่นต้องใช้ + exports: [MasterService], }) export class MasterModule {} diff --git a/backend/src/modules/master/master.service.ts b/backend/src/modules/master/master.service.ts index 92c6a4d..1177744 100644 --- a/backend/src/modules/master/master.service.ts +++ b/backend/src/modules/master/master.service.ts @@ -1,6 +1,10 @@ // 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 { 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 { 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 { CreateTagDto } from './dto/create-tag.dto'; import { UpdateTagDto } from './dto/update-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() export class MasterService { constructor( @InjectRepository(CorrespondenceType) private readonly corrTypeRepo: Repository, - @InjectRepository(CorrespondenceStatus) private readonly corrStatusRepo: Repository, - @InjectRepository(RfaType) private readonly rfaTypeRepo: Repository, - @InjectRepository(RfaStatusCode) private readonly rfaStatusRepo: Repository, - @InjectRepository(RfaApproveCode) private readonly rfaApproveRepo: Repository, - @InjectRepository(CirculationStatusCode) private readonly circulationStatusRepo: Repository, - @InjectRepository(Tag) private readonly tagRepo: Repository, + + // [New] Repositories + @InjectRepository(Discipline) + private readonly disciplineRepo: Repository, + @InjectRepository(CorrespondenceSubType) + private readonly subTypeRepo: Repository, + @InjectRepository(DocumentNumberFormat) + private readonly formatRepo: Repository, ) {} - // ================================================================= - // ✉️ Correspondence Master Data - // ================================================================= + // ... (Method เดิม: findAllCorrespondenceTypes, findAllCorrespondenceStatuses, ฯลฯ เก็บไว้เหมือนเดิม) ... + // หมายเหตุ: ตรวจสอบว่า Entity ใช้ชื่อ property ว่า isActive หรือ is_active (ใน SQL เป็น is_active แต่ใน Entity มักเป็น isActive) + // โค้ดเดิมใช้ `where: { isActive: true }` ซึ่งถูกต้องถ้า Entity map column name แล้ว async findAllCorrespondenceTypes() { return this.corrTypeRepo.find({ - where: { isActive: true }, // ✅ แก้เป็น camelCase - order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase + where: { isActive: true }, + order: { sortOrder: 'ASC' }, }); } - async findAllCorrespondenceStatuses() { return this.corrStatusRepo.find({ - where: { isActive: true }, // ✅ แก้เป็น camelCase - order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase + where: { isActive: true }, + order: { sortOrder: 'ASC' }, }); } - - // ================================================================= - // 📐 RFA Master Data - // ================================================================= - async findAllRfaTypes() { return this.rfaTypeRepo.find({ - where: { isActive: true }, // ✅ แก้เป็น camelCase - order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase + where: { isActive: true }, + order: { sortOrder: 'ASC' }, }); } - async findAllRfaStatuses() { return this.rfaStatusRepo.find({ - where: { isActive: true }, // ✅ แก้เป็น camelCase - order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase + where: { isActive: true }, + order: { sortOrder: 'ASC' }, }); } - async findAllRfaApproveCodes() { return this.rfaApproveRepo.find({ - where: { isActive: true }, // ✅ แก้เป็น camelCase - order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase + where: { isActive: true }, + order: { sortOrder: 'ASC' }, }); } - - // ================================================================= - // 🔄 Circulation Master Data - // ================================================================= - async findAllCirculationStatuses() { return this.circulationStatusRepo.find({ - where: { isActive: true }, // ✅ แก้เป็น camelCase - order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase + where: { isActive: true }, + 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) { const qb = this.tagRepo.createQueryBuilder('tag'); - if (query?.search) { qb.where('tag.tag_name LIKE :search OR tag.description LIKE :search', { search: `%${query.search}%`, }); } - qb.orderBy('tag.tag_name', 'ASC'); - if (query?.page && query?.limit) { const page = query.page; const limit = query.limit; qb.skip((page - 1) * limit).take(limit); - } - - if (query?.page && query?.limit) { const [items, total] = await qb.getManyAndCount(); return { data: items, - meta: { - total, - page: query.page, - limit: query.limit, - totalPages: Math.ceil(total / query.limit), - }, + meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, }; } - return qb.getMany(); } async findOneTag(id: number) { const tag = await this.tagRepo.findOne({ where: { id } }); - if (!tag) { - throw new NotFoundException(`Tag with ID "${id}" not found`); - } + if (!tag) throw new NotFoundException(`Tag with ID "${id}" not found`); return tag; }