251123:2300 Update T1

This commit is contained in:
2025-11-24 08:15:15 +07:00
parent 23006898d9
commit 9360d78ea6
81 changed files with 4232 additions and 347 deletions

View File

@@ -0,0 +1,65 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
ParseIntPipe,
UseGuards,
Patch,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { CirculationService } from './circulation.service';
import { CreateCirculationDto } from './dto/create-circulation.dto';
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
import { SearchCirculationDto } from './dto/search-circulation.dto';
import { User } from '../user/entities/user.entity';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Audit } from '../../common/decorators/audit.decorator'; // Import
@ApiTags('Circulations')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('circulations')
export class CirculationController {
constructor(private readonly circulationService: CirculationService) {}
@Post()
@ApiOperation({ summary: 'Create internal circulation' })
@RequirePermission('circulation.create') // สิทธิ์ ID 41
@Audit('circulation.create', 'circulation') // ✅ แปะตรงนี้
create(@Body() createDto: CreateCirculationDto, @CurrentUser() user: User) {
return this.circulationService.create(createDto, user);
}
@Get()
@ApiOperation({ summary: 'List circulations in my organization' })
@RequirePermission('document.view')
findAll(@Query() searchDto: SearchCirculationDto, @CurrentUser() user: User) {
return this.circulationService.findAll(searchDto, user);
}
@Get(':id')
@ApiOperation({ summary: 'Get circulation details' })
@RequirePermission('document.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.circulationService.findOne(id);
}
@Patch('routings/:id')
@ApiOperation({ summary: 'Update my routing task (Complete/Reject)' })
@RequirePermission('circulation.respond') // สิทธิ์ ID 42
updateRouting(
@Param('id', ParseIntPipe) id: number,
@Body() updateDto: UpdateCirculationRoutingDto,
@CurrentUser() user: User,
) {
return this.circulationService.updateRoutingStatus(id, updateDto, user);
}
}

View File

@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Circulation } from './entities/circulation.entity';
import { CirculationRouting } from './entities/circulation-routing.entity';
import { CirculationStatusCode } from './entities/circulation-status-code.entity';
import { CirculationService } from './circulation.service';
import { CirculationController } from './circulation.controller';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Circulation,
CirculationRouting,
CirculationStatusCode,
]),
UserModule,
],
controllers: [CirculationController],
providers: [CirculationService],
exports: [CirculationService],
})
export class CirculationModule {}

View File

@@ -1,11 +1,18 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
Injectable,
NotFoundException,
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { Repository, DataSource, Not } from 'typeorm'; // เพิ่ม Not
import { Circulation } from './entities/circulation.entity';
import { CirculationRouting } from './entities/circulation-routing.entity';
import { User } from '../user/entities/user.entity';
import { CreateCirculationDto } from './dto/create-circulation.dto'; // ต้องสร้าง DTO นี้
import { CreateCirculationDto } from './dto/create-circulation.dto';
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; // Import ใหม่
import { SearchCirculationDto } from './dto/search-circulation.dto'; // Import ใหม่
@Injectable()
export class CirculationService {
@@ -18,13 +25,16 @@ export class CirculationService {
) {}
async create(createDto: CreateCirculationDto, user: User) {
if (!user.primaryOrganizationId) {
throw new BadRequestException('User must belong to an organization');
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Create Master Circulation
// TODO: Generate Circulation No. logic here (Simple format)
// Generate No. (Mock Logic) -> ควรใช้ NumberingService จริงในอนาคต
const circulationNo = `CIR-${Date.now()}`;
const circulation = queryRunner.manager.create(Circulation, {
@@ -37,13 +47,12 @@ export class CirculationService {
});
const savedCirculation = await queryRunner.manager.save(circulation);
// 2. Create Routings (Assignees)
if (createDto.assigneeIds && createDto.assigneeIds.length > 0) {
const routings = createDto.assigneeIds.map((userId, index) =>
queryRunner.manager.create(CirculationRouting, {
circulationId: savedCirculation.id,
stepNumber: index + 1,
organizationId: user.primaryOrganizationId, // Internal routing
organizationId: user.primaryOrganizationId,
assignedTo: userId,
status: 'PENDING',
}),
@@ -61,23 +70,84 @@ export class CirculationService {
}
}
async findAll(searchDto: SearchCirculationDto, user: User) {
const { search, status, page = 1, limit = 20 } = searchDto;
const query = this.circulationRepo
.createQueryBuilder('c')
.leftJoinAndSelect('c.creator', 'creator')
.where('c.organizationId = :orgId', {
orgId: user.primaryOrganizationId,
});
if (status) {
query.andWhere('c.statusCode = :status', { status });
}
if (search) {
query.andWhere(
'(c.circulationNo LIKE :search OR c.subject LIKE :search)',
{ search: `%${search}%` },
);
}
query
.orderBy('c.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit);
const [data, total] = await query.getManyAndCount();
return { data, meta: { total, page, limit } };
}
async findOne(id: number) {
const circulation = await this.circulationRepo.findOne({
where: { id },
relations: ['routings', 'routings.assignee', 'correspondence'],
relations: ['routings', 'routings.assignee', 'correspondence', 'creator'],
order: { routings: { stepNumber: 'ASC' } },
});
if (!circulation) throw new NotFoundException('Circulation not found');
return circulation;
}
// Method update status (Complete task)
// ✅ Logic อัปเดตสถานะและปิดงาน
async updateRoutingStatus(
routingId: number,
status: string,
comments: string,
dto: UpdateCirculationRoutingDto,
user: User,
) {
// Logic to update routing status
// and Check if all routings are completed -> Close Circulation
const routing = await this.routingRepo.findOne({
where: { id: routingId },
relations: ['circulation'],
});
if (!routing) throw new NotFoundException('Routing task not found');
// Check Permission: คนทำต้องเป็นเจ้าของ Task
if (routing.assignedTo !== user.user_id) {
throw new ForbiddenException('You are not assigned to this task');
}
// Update Routing
routing.status = dto.status;
routing.comments = dto.comments;
routing.completedAt = new Date();
await this.routingRepo.save(routing);
// Check: ถ้าทุกคนทำเสร็จแล้ว ให้ปิดใบเวียน (Master)
const pendingCount = await this.routingRepo.count({
where: {
circulationId: routing.circulationId,
status: 'PENDING', // หรือ status ที่ยังไม่เสร็จ
},
});
if (pendingCount === 0) {
await this.circulationRepo.update(routing.circulationId, {
statusCode: 'COMPLETED',
closedAt: new Date(),
});
}
return routing;
}
}

View File

@@ -4,6 +4,7 @@ import {
IsNotEmpty,
IsArray,
IsOptional,
ArrayMinSize, // ✅ เพิ่ม
} from 'class-validator';
export class CreateCirculationDto {
@@ -17,7 +18,7 @@ export class CreateCirculationDto {
@IsArray()
@IsInt({ each: true })
@IsNotEmpty()
@ArrayMinSize(1) // ✅ ต้องมีผู้รับอย่างน้อย 1 คน
assigneeIds!: number[]; // รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ)
@IsString()

View File

@@ -1,34 +0,0 @@
import {
IsInt,
IsString,
IsOptional,
IsArray,
IsNotEmpty,
IsEnum,
} from 'class-validator';
export enum TransmittalPurpose {
FOR_APPROVAL = 'FOR_APPROVAL',
FOR_INFORMATION = 'FOR_INFORMATION',
FOR_REVIEW = 'FOR_REVIEW',
OTHER = 'OTHER',
}
export class CreateTransmittalDto {
@IsInt()
@IsNotEmpty()
projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร
@IsEnum(TransmittalPurpose)
@IsOptional()
purpose?: TransmittalPurpose; // วัตถุประสงค์
@IsString()
@IsOptional()
remarks?: string; // หมายเหตุ
@IsArray()
@IsInt({ each: true })
@IsNotEmpty()
itemIds!: number[]; // ID ของเอกสาร (Correspondence IDs) ที่จะแนบไปใน Transmittal
}

View File

@@ -0,0 +1,22 @@
import { IsInt, IsOptional, IsString } from 'class-validator';
import { Type } from 'class-transformer';
export class SearchCirculationDto {
@IsOptional()
@IsString()
search?: string; // ค้นหาจาก Subject หรือ No.
@IsOptional()
@IsString()
status?: string; // OPEN, COMPLETED
@IsOptional()
@IsInt()
@Type(() => Number)
page: number = 1;
@IsOptional()
@IsInt()
@Type(() => Number)
limit: number = 20;
}

View File

@@ -0,0 +1,16 @@
import { IsString, IsOptional, IsEnum } from 'class-validator';
export enum CirculationAction {
COMPLETED = 'COMPLETED',
REJECTED = 'REJECTED',
// IN_PROGRESS อาจจะไม่ต้องส่งมา เพราะเป็น auto state ตอนเริ่มดู
}
export class UpdateCirculationRoutingDto {
@IsEnum(CirculationAction)
status!: string; // สถานะที่ต้องการอัปเดต
@IsString()
@IsOptional()
comments?: string; // ความคิดเห็นเพิ่มเติม
}

View File

@@ -21,6 +21,8 @@ import { WorkflowActionDto } from './dto/workflow-action.dto.js';
import { AddReferenceDto } from './dto/add-reference.dto.js';
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js';
import { Query, Delete } from '@nestjs/common'; // เพิ่ม Query, Delete
import { Audit } from '../../common/decorators/audit.decorator'; // Import
@Controller('correspondences')
@UseGuards(JwtAuthGuard, RbacGuard)
export class CorrespondenceController {
@@ -38,6 +40,7 @@ export class CorrespondenceController {
@Post()
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
return this.correspondenceService.create(createDto, req.user);
}
@@ -52,6 +55,7 @@ export class CorrespondenceController {
// ✅ เพิ่ม Endpoint นี้ครับ
@Post(':id/submit')
@RequirePermission('correspondence.create') // หรือจะสร้าง Permission ใหม่ 'workflow.submit' ก็ได้
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
submit(
@Param('id', ParseIntPipe) id: number,
@Body() submitDto: SubmitCorrespondenceDto,

View File

@@ -16,6 +16,7 @@ import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule
@Module({
imports: [
@@ -33,6 +34,7 @@ import { CorrespondenceReference } from './entities/correspondence-reference.ent
JsonSchemaModule, // Import เพื่อ Validate JSON
UserModule, // <--- 2. ใส่ UserModule ใน imports เพื่อให้ RbacGuard ทำงานได้
WorkflowEngineModule, // <--- Import WorkflowEngine
SearchModule, // ✅ 2. ใส่ SearchModule ที่นี่
],
controllers: [CorrespondenceController],
providers: [CorrespondenceService],

View File

@@ -35,7 +35,7 @@ import { DocumentNumberingService } from '../document-numbering/document-numberi
import { JsonSchemaService } from '../json-schema/json-schema.service.js';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service.js';
import { UserService } from '../user/user.service.js';
import { SearchService } from '../search/search.service'; // Import SearchService
@Injectable()
export class CorrespondenceService {
private readonly logger = new Logger(CorrespondenceService.name);
@@ -61,6 +61,7 @@ export class CorrespondenceService {
private workflowEngine: WorkflowEngineService,
private userService: UserService,
private dataSource: DataSource,
private searchService: SearchService, // Inject
) {}
/**
@@ -182,7 +183,18 @@ export class CorrespondenceService {
});
await queryRunner.manager.save(revision);
await queryRunner.commitTransaction();
await queryRunner.commitTransaction(); // Transaction จบแล้ว ข้อมูลชัวร์แล้ว
// 🔥 Fire & Forget: ไม่ต้อง await ผลลัพธ์เพื่อความเร็ว (หรือใช้ Queue ก็ได้)
this.searchService.indexDocument({
id: savedCorr.id,
type: 'correspondence',
docNumber: docNumber,
title: createDto.title,
description: createDto.description,
status: 'DRAFT',
projectId: createDto.projectId,
createdAt: new Date(),
});
return {
...savedCorr,

View File

@@ -28,7 +28,7 @@ import { ContractDrawingController } from './contract-drawing.controller';
import { DrawingMasterDataController } from './drawing-master-data.controller';
// Modules
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -47,6 +47,7 @@ import { FileStorageModule } from '../../common/file-storage/file-storage.module
Attachment,
]),
FileStorageModule,
UserModule,
],
providers: [
ShopDrawingService,

View File

@@ -20,6 +20,7 @@ import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from '../user/entities/user.entity';
import { Audit } from '../../common/decorators/audit.decorator'; // Import
@ApiTags('Shop Drawings')
@ApiBearerAuth()
@@ -31,6 +32,7 @@ export class ShopDrawingController {
@Post()
@ApiOperation({ summary: 'Create new Shop Drawing with initial revision' })
@RequirePermission('drawing.create') // อ้างอิง Permission จาก Seed
@Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้
create(@Body() createDto: CreateShopDrawingDto, @CurrentUser() user: User) {
return this.shopDrawingService.create(createDto, user);
}
@@ -52,6 +54,7 @@ export class ShopDrawingController {
@Post(':id/revisions')
@ApiOperation({ summary: 'Add new revision to existing Shop Drawing' })
@RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร
@Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้
createRevision(
@Param('id', ParseIntPipe) id: number,
@Body() createRevisionDto: CreateShopDrawingRevisionDto,

View File

@@ -43,10 +43,9 @@ export class ShopDrawingService {
/**
* สร้าง Shop Drawing ใหม่ พร้อม Revision แรก (Rev 0)
* ทำงานภายใต้ Database Transaction เดียวกัน
*/
async create(createDto: CreateShopDrawingDto, user: User) {
// 1. ตรวจสอบเลขที่แบบซ้ำ (Unique Check)
// 1. Check Duplicate
const exists = await this.shopDrawingRepo.findOne({
where: { drawingNumber: createDto.drawingNumber },
});
@@ -61,7 +60,7 @@ export class ShopDrawingService {
await queryRunner.startTransaction();
try {
// 2. เตรียมข้อมูล Relations (Contract Drawings & Attachments)
// 2. Prepare Relations
let contractDrawings: ContractDrawing[] = [];
if (createDto.contractDrawingIds?.length) {
contractDrawings = await this.contractDrawingRepo.findBy({
@@ -76,7 +75,7 @@ export class ShopDrawingService {
});
}
// 3. สร้าง Master Shop Drawing
// 3. Create Master Shop Drawing
const shopDrawing = queryRunner.manager.create(ShopDrawing, {
projectId: createDto.projectId,
drawingNumber: createDto.drawingNumber,
@@ -87,23 +86,22 @@ export class ShopDrawingService {
});
const savedShopDrawing = await queryRunner.manager.save(shopDrawing);
// 4. สร้าง First Revision (Rev 0)
// 4. Create First Revision (Rev 0)
const revision = queryRunner.manager.create(ShopDrawingRevision, {
shopDrawingId: savedShopDrawing.id,
revisionNumber: 0, // เริ่มต้นที่ 0 เสมอ
revisionNumber: 0,
revisionLabel: createDto.revisionLabel || '0',
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
contractDrawings: contractDrawings, // ผูก M:N Relation
attachments: attachments, // ผูก M:N Relation
contractDrawings: contractDrawings,
attachments: attachments,
});
await queryRunner.manager.save(revision);
// 5. Commit Files (ย้ายไฟล์จาก Temp -> Permanent)
// 5. Commit Files
if (createDto.attachmentIds?.length) {
// ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[]
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
);
@@ -111,13 +109,13 @@ export class ShopDrawingService {
await queryRunner.commitTransaction();
// ✅ FIX: Return ข้อมูลของ ShopDrawing และ Revision (ไม่ใช่ savedCorr หรือ docNumber)
return {
...savedShopDrawing,
currentRevision: revision,
};
} catch (err) {
await queryRunner.rollbackTransaction();
// ✅ FIX: Cast err เป็น Error
this.logger.error(
`Failed to create shop drawing: ${(err as Error).message}`,
);
@@ -128,14 +126,12 @@ export class ShopDrawingService {
}
/**
* เพิ่ม Revision ใหม่ให้กับ Shop Drawing เดิม (Add Revision)
* เช่น Rev 0 -> Rev A
* เพิ่ม Revision ใหม่ (Add Revision)
*/
async createRevision(
shopDrawingId: number,
createDto: CreateShopDrawingRevisionDto,
) {
// 1. ตรวจสอบว่ามี Master Drawing อยู่จริง
const shopDrawing = await this.shopDrawingRepo.findOneBy({
id: shopDrawingId,
});
@@ -143,7 +139,6 @@ export class ShopDrawingService {
throw new NotFoundException('Shop Drawing not found');
}
// 2. ตรวจสอบ Label ซ้ำใน Drawing เดียวกัน
const exists = await this.revisionRepo.findOne({
where: { shopDrawingId, revisionLabel: createDto.revisionLabel },
});
@@ -158,7 +153,6 @@ export class ShopDrawingService {
await queryRunner.startTransaction();
try {
// 3. เตรียม Relations
let contractDrawings: ContractDrawing[] = [];
if (createDto.contractDrawingIds?.length) {
contractDrawings = await this.contractDrawingRepo.findBy({
@@ -173,14 +167,12 @@ export class ShopDrawingService {
});
}
// 4. หา Revision Number ล่าสุดเพื่อ +1 (Running Number ภายใน)
const latestRev = await this.revisionRepo.findOne({
where: { shopDrawingId },
order: { revisionNumber: 'DESC' },
});
const nextRevNum = (latestRev?.revisionNumber ?? -1) + 1;
// 5. บันทึก Revision ใหม่
const revision = queryRunner.manager.create(ShopDrawingRevision, {
shopDrawingId,
revisionNumber: nextRevNum,
@@ -194,9 +186,7 @@ export class ShopDrawingService {
});
await queryRunner.manager.save(revision);
// 6. Commit Files
if (createDto.attachmentIds?.length) {
// ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[]
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
);
@@ -206,7 +196,6 @@ export class ShopDrawingService {
return revision;
} catch (err) {
await queryRunner.rollbackTransaction();
// ✅ FIX: Cast err เป็น Error
this.logger.error(`Failed to create revision: ${(err as Error).message}`);
throw err;
} finally {
@@ -215,8 +204,7 @@ export class ShopDrawingService {
}
/**
* ค้นหา Shop Drawing (Search & Filter)
* รองรับการค้นหาด้วย Text และกรองตาม Category
* ค้นหา Shop Drawing
*/
async findAll(searchDto: SearchShopDrawingDto) {
const {
@@ -260,7 +248,7 @@ export class ShopDrawingService {
const [items, total] = await query.getManyAndCount();
// Transform Data: เลือก Revision ล่าสุดมาแสดงเป็น currentRevision
// Transform Data
const transformedItems = items.map((item) => {
item.revisions.sort((a, b) => b.revisionNumber - a.revisionNumber);
const currentRevision = item.revisions[0];
@@ -283,7 +271,7 @@ export class ShopDrawingService {
}
/**
* ดูรายละเอียด Shop Drawing (Get One)
* ดูรายละเอียด Shop Drawing
*/
async findOne(id: number) {
const shopDrawing = await this.shopDrawingRepo.findOne({
@@ -308,7 +296,7 @@ export class ShopDrawingService {
}
/**
* ลบ Shop Drawing (Soft Delete)
* ลบ Shop Drawing
*/
async remove(id: number, user: User) {
const shopDrawing = await this.findOne(id);

View File

@@ -0,0 +1,26 @@
import {
IsString,
IsNotEmpty,
IsInt,
IsOptional,
IsBoolean,
IsObject,
} from 'class-validator';
export class CreateJsonSchemaDto {
@IsString()
@IsNotEmpty()
schemaCode!: string; // รหัส Schema (ต้องไม่ซ้ำ เช่น 'RFA_DWG_V1')
@IsInt()
@IsOptional()
version?: number; // เวอร์ชัน (Default: 1)
@IsObject()
@IsNotEmpty()
schemaDefinition!: Record<string, any>; // โครงสร้าง JSON Schema (Standard Format)
@IsBoolean()
@IsOptional()
isActive?: boolean; // สถานะการใช้งาน
}

View File

@@ -0,0 +1,27 @@
import { IsString, IsOptional, IsBoolean, IsInt } from 'class-validator';
import { Type, Transform } from 'class-transformer';
export class SearchJsonSchemaDto {
@IsString()
@IsOptional()
search?: string; // ค้นหาจาก schemaCode
@IsBoolean()
@IsOptional()
@Transform(({ value }) => {
if (value === 'true') return true;
if (value === 'false') return false;
return value;
})
isActive?: boolean;
@IsOptional()
@IsInt()
@Type(() => Number)
page: number = 1;
@IsOptional()
@IsInt()
@Type(() => Number)
limit: number = 20;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateJsonSchemaDto } from './create-json-schema.dto';
export class UpdateJsonSchemaDto extends PartialType(CreateJsonSchemaDto) {}

View File

@@ -1,25 +1,36 @@
import { Controller, Post, Body, Param, UseGuards } from '@nestjs/common';
import { JsonSchemaService } from './json-schema.service.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
@Controller('json-schemas')
import { JsonSchemaService } from './json-schema.service';
// ✅ FIX: Import DTO
import { CreateJsonSchemaDto } from './dto/create-json-schema.dto';
// ✅ FIX: แก้ไข Path ของ Guards
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
@ApiTags('JSON Schemas') // ✅ Add Swagger Tag
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('json-schemas')
export class JsonSchemaController {
constructor(private readonly schemaService: JsonSchemaService) {}
@Post(':code')
@RequirePermission('system.manage_all') // เฉพาะ Superadmin หรือผู้มีสิทธิ์จัดการ System
create(@Param('code') code: string, @Body() definition: any) {
return this.schemaService.createOrUpdate(code, definition);
@Post()
@ApiOperation({ summary: 'Create or Update JSON Schema' })
@RequirePermission('system.manage_all') // Admin Only
create(@Body() createDto: CreateJsonSchemaDto) {
return this.schemaService.createOrUpdate(
createDto.schemaCode,
createDto.schemaDefinition,
);
}
// Endpoint สำหรับ Test Validate (Optional)
@Post(':code/validate')
@ApiOperation({ summary: 'Test validation against a schema' })
@RequirePermission('document.view')
async validate(@Param('code') code: string, @Body() data: any) {
const isValid = await this.schemaService.validate(code, data);
return { valid: isValid };
return { valid: isValid, message: 'Validation passed' };
}
}

View File

@@ -1,17 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JsonSchemaService } from './json-schema.service.js';
import { JsonSchemaController } from './json-schema.controller.js';
import { JsonSchema } from './entities/json-schema.entity.js';
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
import { JsonSchemaService } from './json-schema.service';
import { JsonSchemaController } from './json-schema.controller';
import { JsonSchema } from './entities/json-schema.entity';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([JsonSchema]),
UserModule, // <--- 2. ใส่ UserModule ใน imports
],
imports: [TypeOrmModule.forFeature([JsonSchema]), UserModule],
controllers: [JsonSchemaController],
providers: [JsonSchemaService],
exports: [JsonSchemaService], // Export ให้ Module อื่นเรียกใช้ .validate()
exports: [JsonSchemaService],
})
export class JsonSchemaModule {}

View File

@@ -3,31 +3,32 @@ import {
OnModuleInit,
BadRequestException,
NotFoundException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { JsonSchema } from './entities/json-schema.entity.js';
import { JsonSchema } from './entities/json-schema.entity'; // ลบ .js
@Injectable()
export class JsonSchemaService implements OnModuleInit {
private ajv: Ajv;
// Cache ตัว Validator ที่ Compile แล้ว เพื่อประสิทธิภาพ
private validators = new Map<string, any>();
private readonly logger = new Logger(JsonSchemaService.name);
constructor(
@InjectRepository(JsonSchema)
private schemaRepo: Repository<JsonSchema>,
) {
// ตั้งค่า AJV
this.ajv = new Ajv({ allErrors: true, strict: false }); // strict: false เพื่อยืดหยุ่นกับ custom keywords
addFormats(this.ajv); // รองรับ format เช่น email, date-time
this.ajv = new Ajv({ allErrors: true, strict: false });
addFormats(this.ajv);
}
onModuleInit() {
// (Optional) โหลด Schema ทั้งหมดมา Cache ตอนเริ่ม App ก็ได้
// แต่ตอนนี้ใช้วิธี Lazy Load (โหลดเมื่อใช้) ไปก่อน
async onModuleInit() {
// Pre-load schemas (Optional for performance)
// const schemas = await this.schemaRepo.find({ where: { isActive: true } });
// schemas.forEach(s => this.createValidator(s.schemaCode, s.schemaDefinition));
}
/**
@@ -36,7 +37,6 @@ export class JsonSchemaService implements OnModuleInit {
async validate(schemaCode: string, data: any): Promise<boolean> {
let validate = this.validators.get(schemaCode);
// ถ้ายังไม่มีใน Cache หรือต้องการตัวล่าสุด ให้ดึงจาก DB
if (!validate) {
const schema = await this.schemaRepo.findOne({
where: { schemaCode, isActive: true },
@@ -59,19 +59,21 @@ export class JsonSchemaService implements OnModuleInit {
const valid = validate(data);
if (!valid) {
// รวบรวม Error ทั้งหมดส่งกลับไป
const errors = validate.errors
?.map((e: any) => `${e.instancePath} ${e.message}`)
.join(', ');
// โยน Error กลับไปเพื่อให้ Controller/Service ปลายทางจัดการ
throw new BadRequestException(`JSON Validation Failed: ${errors}`);
}
return true;
}
// ฟังก์ชันสำหรับสร้าง/อัปเดต Schema (สำหรับ Admin)
async createOrUpdate(schemaCode: string, definition: any) {
// ตรวจสอบก่อนว่า Definition เป็น JSON Schema ที่ถูกต้องไหม
/**
* สร้างหรืออัปเดต Schema
*/
async createOrUpdate(schemaCode: string, definition: Record<string, any>) {
// 1. ตรวจสอบว่า Definition เป็น JSON Schema ที่ถูกต้องไหม
try {
this.ajv.compile(definition);
} catch (error: any) {
@@ -80,6 +82,7 @@ export class JsonSchemaService implements OnModuleInit {
);
}
// 2. บันทึกลง DB
let schema = await this.schemaRepo.findOne({ where: { schemaCode } });
if (schema) {
@@ -93,9 +96,12 @@ export class JsonSchemaService implements OnModuleInit {
});
}
// Clear Cache เก่า
this.validators.delete(schemaCode);
const savedSchema = await this.schemaRepo.save(schema);
return this.schemaRepo.save(schema);
// 3. Clear Cache เพื่อให้ครั้งหน้าโหลดตัวใหม่
this.validators.delete(schemaCode);
this.logger.log(`Schema '${schemaCode}' updated (v${savedSchema.version})`);
return savedSchema;
}
}

View File

@@ -0,0 +1,15 @@
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
export class CreateProjectDto {
@IsString()
@IsNotEmpty()
projectCode!: string; // รหัสโครงการ (เช่น LCBP3)
@IsString()
@IsNotEmpty()
projectName!: string; // ชื่อโครงการ
@IsBoolean()
@IsOptional()
isActive?: boolean; // สถานะการใช้งาน (Default: true)
}

View File

@@ -0,0 +1,27 @@
import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';
import { Type, Transform } from 'class-transformer';
export class SearchProjectDto {
@IsString()
@IsOptional()
search?: string; // ค้นหาจาก Project Code หรือ Name
@IsOptional()
@IsBoolean()
@Transform(({ value }) => {
if (value === 'true') return true;
if (value === 'false') return false;
return value;
})
isActive?: boolean; // กรองตามสถานะ Active
@IsOptional()
@IsInt()
@Type(() => Number)
page: number = 1;
@IsOptional()
@IsInt()
@Type(() => Number)
limit: number = 20;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateProjectDto } from './create-project.dto';
export class UpdateProjectDto extends PartialType(CreateProjectDto) {}

View File

@@ -1,4 +1,75 @@
import { Controller } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
@Controller('project')
export class ProjectController {}
import { ProjectService } from './project.service.js';
import { CreateProjectDto } from './dto/create-project.dto.js';
import { UpdateProjectDto } from './dto/update-project.dto.js';
import { SearchProjectDto } from './dto/search-project.dto.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@ApiTags('Projects')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('projects') // แนะนำให้ใช้ plural noun (projects)
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Post()
@ApiOperation({ summary: 'Create new Project' })
@RequirePermission('project.create')
create(@Body() createDto: CreateProjectDto) {
return this.projectService.create(createDto);
}
@Get()
@ApiOperation({ summary: 'Search Projects' })
@RequirePermission('project.view')
findAll(@Query() searchDto: SearchProjectDto) {
return this.projectService.findAll(searchDto);
}
@Get('organizations')
@ApiOperation({ summary: 'List All Organizations (Master Data)' })
// @RequirePermission('organization.view') // หรือเปิดให้ดูได้ทั่วไปถ้าจำเป็น
findAllOrgs() {
return this.projectService.findAllOrganizations();
}
@Get(':id')
@ApiOperation({ summary: 'Get Project Details' })
@RequirePermission('project.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.projectService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update Project' })
@RequirePermission('project.edit')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateDto: UpdateProjectDto,
) {
return this.projectService.update(id, updateDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete Project (Soft Delete)' })
@RequirePermission('project.delete')
remove(@Param('id', ParseIntPipe) id: number) {
return this.projectService.remove(id);
}
}

View File

@@ -7,7 +7,8 @@ import { Organization } from './entities/organization.entity.js';
import { Contract } from './entities/contract.entity.js';
import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม
import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม
// Modules
import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -17,6 +18,7 @@ import { ContractOrganization } from './entities/contract-organization.entity.js
ProjectOrganization, // ลงทะเบียน
ContractOrganization, // ลงทะเบียน
]),
UserModule, // ✅ 2. เพิ่ม UserModule เข้าไปใน imports
],
controllers: [ProjectController],
providers: [ProjectService],

View File

@@ -1,11 +1,25 @@
import { Injectable } from '@nestjs/common';
import {
Injectable,
NotFoundException,
ConflictException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, Like } from 'typeorm';
// Entities
import { Project } from './entities/project.entity.js';
import { Organization } from './entities/organization.entity.js';
// DTOs
import { CreateProjectDto } from './dto/create-project.dto.js';
import { UpdateProjectDto } from './dto/update-project.dto.js';
import { SearchProjectDto } from './dto/search-project.dto.js';
@Injectable()
export class ProjectService {
private readonly logger = new Logger(ProjectService.name);
constructor(
@InjectRepository(Project)
private projectRepository: Repository<Project>,
@@ -13,13 +27,92 @@ export class ProjectService {
private organizationRepository: Repository<Organization>,
) {}
// ดึงรายการ Project ทั้งหมด
async findAllProjects() {
return this.projectRepository.find();
// --- CRUD Operations ---
async create(createDto: CreateProjectDto) {
// 1. เช็คชื่อ/รหัสซ้ำ (ถ้าจำเป็น)
const existing = await this.projectRepository.findOne({
where: { projectCode: createDto.projectCode },
});
if (existing) {
throw new ConflictException(
`Project Code "${createDto.projectCode}" already exists`,
);
}
// 2. สร้าง Project
const project = this.projectRepository.create(createDto);
return this.projectRepository.save(project);
}
// ดึงรายการ Organization ทั้งหมด
async findAll(searchDto: SearchProjectDto) {
const { search, isActive, page = 1, limit = 20 } = searchDto;
const skip = (page - 1) * limit;
// สร้าง Query Builder
const query = this.projectRepository.createQueryBuilder('project');
if (isActive !== undefined) {
query.andWhere('project.isActive = :isActive', { isActive });
}
if (search) {
query.andWhere(
'(project.projectCode LIKE :search OR project.projectName LIKE :search)',
{ search: `%${search}%` },
);
}
query.orderBy('project.created_at', 'DESC');
query.skip(skip).take(limit);
const [items, total] = await query.getManyAndCount();
return {
data: items,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findOne(id: number) {
const project = await this.projectRepository.findOne({
where: { id },
relations: ['contracts'], // ดึงสัญญาที่เกี่ยวข้องมาด้วย
});
if (!project) {
throw new NotFoundException(`Project ID ${id} not found`);
}
return project;
}
async update(id: number, updateDto: UpdateProjectDto) {
const project = await this.findOne(id);
// Merge ข้อมูลใหม่ใส่ข้อมูลเดิม
this.projectRepository.merge(project, updateDto);
return this.projectRepository.save(project);
}
async remove(id: number) {
const project = await this.findOne(id);
// ใช้ Soft Delete
return this.projectRepository.softRemove(project);
}
// --- Organization Helper ---
async findAllOrganizations() {
return this.organizationRepository.find();
return this.organizationRepository.find({
where: { isActive: true },
order: { organizationCode: 'ASC' },
});
}
}

View File

@@ -15,6 +15,7 @@ import { RfaStatusCode } from './rfa-status-code.entity';
import { RfaApproveCode } from './rfa-approve-code.entity';
import { User } from '../../user/entities/user.entity';
import { RfaItem } from './rfa-item.entity';
import { RfaWorkflow } from './rfa-workflow.entity'; // Import เพิ่ม
@Entity('rfa_revisions')
@Unique(['rfaId', 'revisionNumber'])
@@ -96,4 +97,10 @@ export class RfaRevision {
// Items (Shop Drawings inside this RFA)
@OneToMany(() => RfaItem, (item) => item.rfaRevision, { cascade: true })
items!: RfaItem[];
// Workflows
@OneToMany(() => RfaWorkflow, (workflow) => workflow.rfaRevision, {
cascade: true,
})
workflows!: RfaWorkflow[];
}

View File

@@ -0,0 +1,64 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { RfaWorkflowTemplate } from './rfa-workflow-template.entity';
import { Organization } from '../../project/entities/organization.entity';
import { Role } from '../../user/entities/role.entity';
// ✅ 1. สร้าง Enum เพื่อให้ Type Safe
export enum RfaActionType {
REVIEW = 'REVIEW',
APPROVE = 'APPROVE',
ACKNOWLEDGE = 'ACKNOWLEDGE',
}
@Entity('rfa_workflow_template_steps')
export class RfaWorkflowTemplateStep {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'template_id' })
templateId!: number;
@Column({ name: 'step_number' })
stepNumber!: number;
@Column({ name: 'organization_id' })
organizationId!: number;
@Column({ name: 'role_id', nullable: true })
roleId?: number;
@Column({
name: 'action_type',
type: 'enum',
enum: RfaActionType, // ✅ 2. ใช้ Enum ตรงนี้
nullable: true,
})
actionType?: RfaActionType; // ✅ 3. เปลี่ยน type จาก string เป็น Enum
@Column({ name: 'duration_days', nullable: true })
durationDays?: number;
@Column({ name: 'is_optional', default: false })
isOptional!: boolean;
// Relations
@ManyToOne(() => RfaWorkflowTemplate, (template) => template.steps, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'template_id' })
template!: RfaWorkflowTemplate;
@ManyToOne(() => Organization)
@JoinColumn({ name: 'organization_id' })
organization!: Organization;
@ManyToOne(() => Role)
@JoinColumn({ name: 'role_id' })
role?: Role;
}

View File

@@ -0,0 +1,39 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { RfaWorkflowTemplateStep } from './rfa-workflow-template-step.entity';
@Entity('rfa_workflow_templates')
export class RfaWorkflowTemplate {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'template_name', length: 100 })
templateName!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
@Column({ type: 'json', nullable: true })
workflowConfig?: Record<string, any>; // Configuration เพิ่มเติม
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
// Relations
@OneToMany(() => RfaWorkflowTemplateStep, (step) => step.template, {
cascade: true,
})
steps!: RfaWorkflowTemplateStep[];
}

View File

@@ -0,0 +1,73 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { RfaRevision } from './rfa-revision.entity';
import { Organization } from '../../project/entities/organization.entity';
import { User } from '../../user/entities/user.entity';
@Entity('rfa_workflows')
export class RfaWorkflow {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'rfa_revision_id' })
rfaRevisionId!: number;
@Column({ name: 'step_number' })
stepNumber!: number;
@Column({ name: 'organization_id' })
organizationId!: number;
@Column({ name: 'assigned_to', nullable: true })
assignedTo?: number;
@Column({
name: 'action_type',
type: 'enum',
enum: ['REVIEW', 'APPROVE', 'ACKNOWLEDGE'],
nullable: true,
})
actionType?: string;
@Column({
type: 'enum',
enum: ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED'],
nullable: true,
})
status?: string;
@Column({ type: 'text', nullable: true })
comments?: string;
@Column({ name: 'completed_at', type: 'datetime', nullable: true })
completedAt?: Date;
@Column({ type: 'json', nullable: true })
stateContext?: Record<string, any>; // เก็บ Snapshot ข้อมูล ณ ขณะนั้น
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
// Relations
@ManyToOne(() => RfaRevision, (rev) => rev.workflows, { onDelete: 'CASCADE' }) // ต้องไปเพิ่ม Property workflows ใน RfaRevision ด้วย
@JoinColumn({ name: 'rfa_revision_id' })
rfaRevision!: RfaRevision;
@ManyToOne(() => Organization)
@JoinColumn({ name: 'organization_id' })
organization!: Organization;
@ManyToOne(() => User)
@JoinColumn({ name: 'assigned_to' })
assignee?: User;
}

View File

@@ -18,6 +18,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Audit } from '../../common/decorators/audit.decorator'; // Import
@ApiTags('RFA (Request for Approval)')
@ApiBearerAuth()
@@ -29,6 +30,7 @@ export class RfaController {
@Post()
@ApiOperation({ summary: 'Create new RFA (Draft)' })
@RequirePermission('rfa.create') // สิทธิ์ ID 37
@Audit('rfa.create', 'rfa') // ✅ แปะตรงนี้
create(@Body() createDto: CreateRfaDto, @CurrentUser() user: User) {
return this.rfaService.create(createDto, user);
}

View File

@@ -21,6 +21,12 @@ import { RfaController } from './rfa.controller';
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
import { UserModule } from '../user/user.module';
// ... imports
import { RfaWorkflow } from './entities/rfa-workflow.entity';
import { RfaWorkflowTemplate } from './entities/rfa-workflow-template.entity';
import { RfaWorkflowTemplateStep } from './entities/rfa-workflow-template-step.entity';
import { SearchModule } from '../search/search.module'; // ✅ เพิ่ม
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -32,9 +38,14 @@ import { UserModule } from '../user/user.module';
RfaApproveCode,
Correspondence,
ShopDrawingRevision,
// ... (ตัวเดิม)
RfaWorkflow,
RfaWorkflowTemplate,
RfaWorkflowTemplateStep,
]),
DocumentNumberingModule,
UserModule,
SearchModule,
],
providers: [RfaService],
controllers: [RfaController],

View File

@@ -34,6 +34,7 @@ import { DocumentNumberingService } from '../document-numbering/document-numberi
import { UserService } from '../user/user.service';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
import { NotificationService } from '../notification/notification.service';
import { SearchService } from '../search/search.service'; // Import SearchService
@Injectable()
export class RfaService {
@@ -66,6 +67,7 @@ export class RfaService {
private workflowEngine: WorkflowEngineService,
private notificationService: NotificationService,
private dataSource: DataSource,
private searchService: SearchService, // Inject
) {}
/**
@@ -166,6 +168,17 @@ export class RfaService {
}
await queryRunner.commitTransaction();
// 🔥 Fire & Forget: ไม่ต้อง await ผลลัพธ์เพื่อความเร็ว (หรือใช้ Queue ก็ได้)
this.searchService.indexDocument({
id: savedCorr.id,
type: 'correspondence',
docNumber: docNumber,
title: createDto.title,
description: createDto.description,
status: 'DRAFT',
projectId: createDto.projectId,
createdAt: new Date(),
});
return {
...savedRfa,

View File

@@ -0,0 +1,27 @@
import { IsString, IsOptional, IsInt, IsNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
export class SearchQueryDto {
@IsString()
@IsOptional()
q?: string; // คำค้นหา (Query)
@IsString()
@IsOptional()
type?: string; // กรองประเภท: 'rfa', 'correspondence', 'drawing'
@IsInt()
@Type(() => Number)
@IsOptional()
projectId?: number;
@IsInt()
@Type(() => Number)
@IsOptional()
page: number = 1;
@IsInt()
@Type(() => Number)
@IsOptional()
limit: number = 20;
}

View File

@@ -0,0 +1,22 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { SearchService } from './search.service';
import { SearchQueryDto } from './dto/search-query.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
@ApiTags('Search')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('search')
export class SearchController {
constructor(private readonly searchService: SearchService) {}
@Get()
@ApiOperation({ summary: 'Advanced Search across all documents' })
@RequirePermission('search.advanced') // สิทธิ์ ID 48
search(@Query() queryDto: SearchQueryDto) {
return this.searchService.search(queryDto);
}
}

View File

@@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { ElasticsearchModule } from '@nestjs/elasticsearch';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SearchService } from './search.service';
import { SearchController } from './search.controller';
import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule
@Module({
imports: [
ConfigModule,
// ✅ 2. เพิ่ม UserModule เข้าไปใน imports
UserModule,
ElasticsearchModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
node:
configService.get<string>('ELASTICSEARCH_NODE') ||
'http://localhost:9200',
auth: {
username: configService.get<string>('ELASTICSEARCH_USERNAME') || '',
password: configService.get<string>('ELASTICSEARCH_PASSWORD') || '',
},
}),
inject: [ConfigService],
}),
],
controllers: [SearchController],
providers: [SearchService],
exports: [SearchService],
})
export class SearchModule {}

View File

@@ -0,0 +1,152 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { ConfigService } from '@nestjs/config';
import { SearchQueryDto } from './dto/search-query.dto';
@Injectable()
export class SearchService implements OnModuleInit {
private readonly logger = new Logger(SearchService.name);
private readonly indexName = 'dms_documents';
constructor(
private readonly esService: ElasticsearchService,
private readonly configService: ConfigService,
) {}
async onModuleInit() {
await this.createIndexIfNotExists();
}
/**
* สร้าง Index และกำหนด Mapping (Schema)
*/
private async createIndexIfNotExists() {
try {
const indexExists = await this.esService.indices.exists({
index: this.indexName,
});
if (!indexExists) {
// ✅ FIX: Cast 'body' เป็น any เพื่อแก้ปัญหา Type Mismatch ของ Library
await this.esService.indices.create({
index: this.indexName,
body: {
mappings: {
properties: {
id: { type: 'integer' },
type: { type: 'keyword' }, // correspondence, rfa, drawing
docNumber: { type: 'text' },
title: { type: 'text', analyzer: 'standard' },
description: { type: 'text', analyzer: 'standard' },
status: { type: 'keyword' },
projectId: { type: 'integer' },
createdAt: { type: 'date' },
tags: { type: 'text' },
},
},
} as any,
});
this.logger.log(`Elasticsearch index '${this.indexName}' created.`);
}
} catch (error) {
this.logger.error(`Failed to create index: ${(error as Error).message}`);
}
}
/**
* Index เอกสาร (Create/Update)
*/
async indexDocument(doc: any) {
try {
return await this.esService.index({
index: this.indexName,
id: `${doc.type}_${doc.id}`, // Unique ID: rfa_101
document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน
});
} catch (error) {
this.logger.error(
`Failed to index document: ${(error as Error).message}`,
);
}
}
/**
* ลบเอกสารออกจาก Index
*/
async removeDocument(type: string, id: number) {
try {
await this.esService.delete({
index: this.indexName,
id: `${type}_${id}`,
});
} catch (error) {
this.logger.error(
`Failed to remove document: ${(error as Error).message}`,
);
}
}
/**
* ค้นหาเอกสาร (Full-text Search)
*/
async search(queryDto: SearchQueryDto) {
const { q, type, projectId, page = 1, limit = 20 } = queryDto;
const from = (page - 1) * limit;
const mustQueries: any[] = [];
// 1. Full-text search logic
if (q) {
mustQueries.push({
multi_match: {
query: q,
fields: ['title^3', 'docNumber^2', 'description', 'tags'], // Boost ความสำคัญ
fuzziness: 'AUTO',
},
});
} else {
mustQueries.push({ match_all: {} });
}
// 2. Filter logic
const filterQueries: any[] = [];
if (type) filterQueries.push({ term: { type } });
if (projectId) filterQueries.push({ term: { projectId } });
try {
const result = await this.esService.search({
index: this.indexName,
from,
size: limit,
// ✅ ส่ง Query Structure โดยตรง
query: {
bool: {
must: mustQueries,
filter: filterQueries,
},
},
sort: [{ createdAt: { order: 'desc' } }],
});
// 3. Format Result
const hits = result.hits.hits;
const total =
typeof result.hits.total === 'number'
? result.hits.total
: result.hits.total?.value || 0;
return {
data: hits.map((hit) => hit._source),
meta: {
total,
page,
limit,
took: result.took,
},
};
} catch (error) {
this.logger.error(`Search failed: ${(error as Error).message}`);
return { data: [], meta: { total: 0, page, limit, took: 0 } };
}
}
}

View File

@@ -19,6 +19,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Audit } from '../../common/decorators/audit.decorator'; // Import
@ApiTags('Transmittals')
@ApiBearerAuth()
@@ -30,24 +31,23 @@ export class TransmittalController {
@Post()
@ApiOperation({ summary: 'Create new Transmittal' })
@RequirePermission('transmittal.create') // สิทธิ์ ID 40
@Audit('transmittal.create', 'transmittal') // ✅ แปะตรงนี้
create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) {
return this.transmittalService.create(createDto, user);
}
// เพิ่ม Endpoint พื้นฐานสำหรับการค้นหา (Optional)
/*
@Get()
@ApiOperation({ summary: 'Search Transmittals' })
@RequirePermission('document.view')
findAll(@Query() searchDto: SearchTransmittalDto) {
// return this.transmittalService.findAll(searchDto);
// return this.transmittalService.findAll(searchDto);
}
@Get(':id')
@ApiOperation({ summary: 'Get Transmittal details' })
@RequirePermission('document.view')
findOne(@Param('id', ParseIntPipe) id: number) {
// return this.transmittalService.findOne(id);
// return this.transmittalService.findOne(id);
}
*/
}

View File

@@ -6,11 +6,12 @@ import { Correspondence } from '../correspondence/entities/correspondence.entity
import { TransmittalService } from './transmittal.service';
import { TransmittalController } from './transmittal.controller';
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
DocumentNumberingModule,
UserModule,
],
controllers: [TransmittalController],
providers: [TransmittalService],

View File

@@ -12,6 +12,7 @@ import { Correspondence } from '../correspondence/entities/correspondence.entity
import { CreateTransmittalDto } from './dto/create-transmittal.dto'; // ต้องสร้าง DTO
import { User } from '../user/entities/user.entity';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
import { SearchService } from '../search/search.service'; // Import SearchService
@Injectable()
export class TransmittalService {
@@ -24,6 +25,7 @@ export class TransmittalService {
private correspondenceRepo: Repository<Correspondence>,
private numberingService: DocumentNumberingService,
private dataSource: DataSource,
private searchService: SearchService, // Inject
) {}
async create(createDto: CreateTransmittalDto, user: User) {

View File

@@ -0,0 +1,42 @@
// File: src/modules/user/dto/update-preference.dto.ts
// บันทึกการแก้ไข: DTO สำหรับตรวจสอบข้อมูลการอัปเดต User Preferences (T1.3)
import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger'; // ใช้สำหรับสร้าง API Documentation (Swagger)
export class UpdatePreferenceDto {
@ApiPropertyOptional({
description: 'รับการแจ้งเตือนทางอีเมลหรือไม่',
default: true,
})
@IsOptional()
@IsBoolean()
notifyEmail?: boolean;
@ApiPropertyOptional({
description: 'รับการแจ้งเตือนทาง LINE หรือไม่',
default: true,
})
@IsOptional()
@IsBoolean()
notifyLine?: boolean;
@ApiPropertyOptional({
description:
'รับการแจ้งเตือนแบบรวม (Digest) แทน Real-time เพื่อลดจำนวนข้อความ',
default: false,
})
@IsOptional()
@IsBoolean()
digestMode?: boolean;
@ApiPropertyOptional({
description: 'ธีมของหน้าจอ (light, dark, หรือ system)',
default: 'light',
enum: ['light', 'dark', 'system'],
})
@IsOptional()
@IsString()
@IsIn(['light', 'dark', 'system']) // บังคับว่าต้องเป็นค่าใดค่าหนึ่งในนี้เท่านั้น
uiTheme?: string;
}

View File

@@ -1,4 +1,21 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto.js';
// File: src/modules/user/dto/update-preference.dto.ts
import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
export class UpdatePreferenceDto {
@IsOptional()
@IsBoolean()
notifyEmail?: boolean;
@IsOptional()
@IsBoolean()
notifyLine?: boolean;
@IsOptional()
@IsBoolean()
digestMode?: boolean;
@IsOptional()
@IsString()
@IsIn(['light', 'dark', 'system'])
uiTheme?: string;
}

View File

@@ -0,0 +1,27 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('permissions')
export class Permission {
@PrimaryGeneratedColumn({ name: 'permission_id' })
permissionId!: number;
@Column({ name: 'permission_name', length: 100, unique: true })
permissionName!: string; // e.g., 'rfa.create'
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ length: 50, nullable: true })
module?: string; // e.g., 'rfa', 'user'
@Column({
name: 'scope_level',
type: 'enum',
enum: ['GLOBAL', 'ORG', 'PROJECT'],
nullable: true,
})
scopeLevel?: string;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
}

View File

@@ -0,0 +1,29 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
export enum RoleScope {
GLOBAL = 'Global',
ORGANIZATION = 'Organization',
PROJECT = 'Project',
CONTRACT = 'Contract',
}
@Entity('roles')
export class Role {
@PrimaryGeneratedColumn({ name: 'role_id' })
roleId!: number;
@Column({ name: 'role_name', length: 100 })
roleName!: string;
@Column({
type: 'enum',
enum: RoleScope,
})
scope!: RoleScope;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'is_system', default: false })
isSystem!: boolean;
}

View File

@@ -1,3 +1,6 @@
// File: src/modules/user/entities/user-assignment.entity.ts
// บันทึกการแก้ไข: Entity สำหรับการมอบหมาย Role ให้กับ User ตาม Scope (T1.3, RBAC 4-Level)
import {
Entity,
Column,
@@ -6,8 +9,11 @@ import {
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { User } from './user.entity.js';
// Import Role, Org, Project, Contract entities...
import { User } from './user.entity';
import { Role } from './role.entity';
import { Organization } from '../../project/entities/organization.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
import { Project } from '../../project/entities/project.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
import { Contract } from '../../project/entities/contract.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
@Entity('user_assignments')
export class UserAssignment {
@@ -20,6 +26,7 @@ export class UserAssignment {
@Column({ name: 'role_id' })
roleId!: number;
// --- Scopes (เลือกได้เพียง 1 หรือเป็น NULL ทั้งหมดสำหรับ Global) ---
@Column({ name: 'organization_id', nullable: true })
organizationId?: number;
@@ -35,8 +42,29 @@ export class UserAssignment {
@CreateDateColumn({ name: 'assigned_at' })
assignedAt!: Date;
// Relation กลับไปหา User (เจ้าของสิทธิ์)
@ManyToOne(() => User)
// --- Relations ---
@ManyToOne(() => User, (user) => user.assignments, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user?: User;
user!: User;
@ManyToOne(() => Role, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'role_id' })
role!: Role;
@ManyToOne(() => Organization, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'organization_id' })
organization?: Organization;
@ManyToOne(() => Project, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'project_id' })
project?: Project;
@ManyToOne(() => Contract, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'contract_id' })
contract?: Contract;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'assigned_by_user_id' })
assignedBy?: User;
}

View File

@@ -1,7 +1,10 @@
// File: src/modules/user/entities/user-preference.entity.ts
// บันทึกการแก้ไข: Entity สำหรับเก็บการตั้งค่าส่วนตัวของผู้ใช้ แยกจากตาราง Users (T1.3)
import {
Entity,
PrimaryColumn,
Column,
PrimaryColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
@@ -10,7 +13,6 @@ import { User } from './user.entity';
@Entity('user_preferences')
export class UserPreference {
// ใช้ user_id เป็น Primary Key และ Foreign Key ในตัวเดียวกัน (1:1 Relation)
@PrimaryColumn({ name: 'user_id' })
userId!: number;
@@ -20,18 +22,17 @@ export class UserPreference {
@Column({ name: 'notify_line', default: true })
notifyLine!: boolean;
@Column({ name: 'digest_mode', default: true })
@Column({ name: 'digest_mode', default: false })
digestMode!: boolean; // รับแจ้งเตือนแบบรวม (Digest) แทน Real-time
@Column({ name: 'ui_theme', length: 20, default: 'light' })
@Column({ name: 'ui_theme', default: 'light', length: 20 })
uiTheme!: string;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
// --- Relations ---
@OneToOne(() => User)
// --- Relation ---
@OneToOne(() => User, (user) => user.preference, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
}

View File

@@ -1,3 +1,6 @@
// File: src/modules/user/entities/user.entity.ts
// บันทึกการแก้ไข: เพิ่ม Relations กับ UserAssignment และ UserPreference (T1.3)
import {
Entity,
Column,
@@ -5,10 +8,14 @@ import {
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
ManyToOne, // <--- เพิ่มตรงนี้
JoinColumn, // <--- เพิ่มตรงนี้
ManyToOne,
OneToMany,
OneToOne,
JoinColumn,
} from 'typeorm';
import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization
import { Organization } from '../../project/entities/organization.entity'; // Adjust path as needed
import { UserAssignment } from './user-assignment.entity';
import { UserPreference } from './user-preference.entity';
@Entity('users')
export class User {
@@ -18,7 +25,7 @@ export class User {
@Column({ unique: true, length: 50 })
username!: string;
@Column({ name: 'password_hash' })
@Column({ name: 'password_hash', select: false }) // ไม่ Select Password โดย Default
password!: string;
@Column({ unique: true, length: 100 })
@@ -33,15 +40,26 @@ export class User {
@Column({ name: 'is_active', default: true })
isActive!: boolean;
// Relation กับ Organization
@Column({ name: 'line_id', nullable: true, length: 100 })
lineId?: string;
// Relation กับ Organization (สังกัดหลัก)
@Column({ name: 'primary_organization_id', nullable: true })
primaryOrganizationId?: number;
@ManyToOne(() => Organization)
@ManyToOne(() => Organization, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'primary_organization_id' })
organization?: Organization;
// Base Entity Fields (ที่เราแยกมาเขียนเองเพราะเรื่อง deleted_at)
// Relation กับ Assignments (RBAC)
@OneToMany(() => UserAssignment, (assignment) => assignment.user)
assignments?: UserAssignment[];
// Relation กับ Preferences (1:1)
@OneToOne(() => UserPreference, (pref) => pref.user, { cascade: true })
preference?: UserPreference;
// Base Entity Fields
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;

View File

@@ -0,0 +1,47 @@
// File: src/modules/user/user-preference.service.ts
// บันทึกการแก้ไข: Service จัดการการตั้งค่าส่วนตัว (T1.3)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserPreference } from './entities/user-preference.entity';
import { UpdatePreferenceDto } from './dto/update-preference.dto';
@Injectable()
export class UserPreferenceService {
constructor(
@InjectRepository(UserPreference)
private prefRepo: Repository<UserPreference>,
) {}
// ดึง Preference ของ User (ถ้าไม่มีให้สร้าง Default)
async findByUser(userId: number): Promise<UserPreference> {
let pref = await this.prefRepo.findOne({ where: { userId } });
if (!pref) {
pref = this.prefRepo.create({
userId,
notifyEmail: true,
notifyLine: true,
digestMode: false,
uiTheme: 'light',
});
await this.prefRepo.save(pref);
}
return pref;
}
// อัปเดต Preference
async update(
userId: number,
dto: UpdatePreferenceDto,
): Promise<UserPreference> {
const pref = await this.findByUser(userId);
// Merge ข้อมูลใหม่
this.prefRepo.merge(pref, dto);
return this.prefRepo.save(pref);
}
}

View File

@@ -1,3 +1,6 @@
// File: src/modules/user/user.controller.ts
// บันทึกการแก้ไข: เพิ่ม Endpoints สำหรับ User Preferences (T1.3)
import {
Controller,
Get,
@@ -8,47 +11,86 @@ import {
Delete,
UseGuards,
ParseIntPipe,
Request, // <--- อย่าลืม Import Request
} from '@nestjs/common';
import { UserService } from './user.service.js';
import { CreateUserDto } from './dto/create-user.dto.js';
import { UpdateUserDto } from './dto/update-user.dto.js';
import { AssignRoleDto } from './dto/assign-role.dto.js'; // <--- Import DTO
import { UserAssignmentService } from './user-assignment.service.js'; // <--- Import Service ใหม่
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
import { UserService } from './user.service';
import { UserAssignmentService } from './user-assignment.service';
import { UserPreferenceService } from './user-preference.service'; // ✅ เพิ่ม
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AssignRoleDto } from './dto/assign-role.dto';
import { UpdatePreferenceDto } from './dto/update-preference.dto'; // ✅ เพิ่ม DTO
import { JwtAuthGuard } from '../../common/auth/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard'; // สมมติว่ามีแล้ว ถ้ายังไม่มีให้คอมเมนต์ไว้ก่อน
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from './entities/user.entity';
@ApiTags('Users')
@ApiBearerAuth()
@Controller('users')
@UseGuards(JwtAuthGuard, RbacGuard)
@UseGuards(JwtAuthGuard, RbacGuard) // RbacGuard จะเช็ค permission
export class UserController {
constructor(
private readonly userService: UserService,
private readonly assignmentService: UserAssignmentService, // <--- ✅ Inject Service เข้ามา
private readonly assignmentService: UserAssignmentService,
private readonly preferenceService: UserPreferenceService, // ✅ Inject Service
) {}
// --- User CRUD ---
// --- User Preferences (Me) ---
// ต้องวางไว้ก่อน :id เพื่อไม่ให้ route ชนกัน
@Get('me/preferences')
@ApiOperation({ summary: 'Get my preferences' })
@UseGuards(JwtAuthGuard) // Bypass RBAC check for self
getMyPreferences(@CurrentUser() user: User) {
return this.preferenceService.findByUser(user.user_id);
}
@Patch('me/preferences')
@ApiOperation({ summary: 'Update my preferences' })
@UseGuards(JwtAuthGuard) // Bypass RBAC check for self
updateMyPreferences(
@CurrentUser() user: User,
@Body() dto: UpdatePreferenceDto,
) {
return this.preferenceService.update(user.user_id, dto);
}
@Get('me/permissions')
@ApiOperation({ summary: 'Get my permissions' })
@UseGuards(JwtAuthGuard)
getMyPermissions(@CurrentUser() user: User) {
return this.userService.getUserPermissions(user.user_id);
}
// --- User CRUD (Admin) ---
@Post()
@ApiOperation({ summary: 'Create new user' })
@RequirePermission('user.create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
@ApiOperation({ summary: 'List all users' })
@RequirePermission('user.view')
findAll() {
return this.userService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get user details' })
@RequirePermission('user.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update user' })
@RequirePermission('user.edit')
update(
@Param('id', ParseIntPipe) id: number,
@@ -58,6 +100,7 @@ export class UserController {
}
@Delete(':id')
@ApiOperation({ summary: 'Delete user (Soft delete)' })
@RequirePermission('user.delete')
remove(@Param('id', ParseIntPipe) id: number) {
return this.userService.remove(id);
@@ -65,14 +108,10 @@ export class UserController {
// --- Role Assignment ---
@Post('assign-role') // <--- ✅ ต้องมี @ เสมอครับ
@Post('assign-role')
@ApiOperation({ summary: 'Assign role to user' })
@RequirePermission('permission.assign')
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
return this.assignmentService.assignRole(dto, req.user);
}
@Get('me/permissions')
@UseGuards(JwtAuthGuard) // No RbacGuard here to avoid circular dependency check issues
getMyPermissions(@Request() req: any) {
return this.userService.getUserPermissions(req.user.user_id);
assignRole(@Body() dto: AssignRoleDto, @CurrentUser() user: User) {
return this.assignmentService.assignRole(dto, user);
}
}

View File

@@ -1,24 +1,42 @@
// File: src/modules/user/user.module.ts
// บันทึกการแก้ไข: รวม UserPreferenceService และ RoleService (T1.3)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service.js';
import { UserController } from './user.controller.js'; // 1. Import Controller
import { User } from './entities/user.entity.js';
import { UserAssignmentService } from './user-assignment.service.js';
import { UserAssignment } from './entities/user-assignment.entity.js';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { UserAssignmentService } from './user-assignment.service';
import { UserPreferenceService } from './user-preference.service'; // ✅ เพิ่ม
// Entities
import { User } from './entities/user.entity';
import { UserAssignment } from './entities/user-assignment.entity';
import { UserPreference } from './entities/user-preference.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
@Module({
imports: [
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
TypeOrmModule.forFeature([User, UserAssignment]),
], // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
// ลงทะเบียน Entity ให้ครบ
TypeOrmModule.forFeature([
User,
UserAssignment,
UserPreference,
Role,
Permission,
]),
],
controllers: [UserController],
providers: [
UserService,
UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider
UserAssignmentService,
UserPreferenceService, // ✅ เพิ่ม Provider
],
exports: [
UserService,
UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้
], // Export ให้ AuthModule เรียกใช้ได้
UserAssignmentService,
UserPreferenceService, // Export ให้ Module อื่นใช้ (เช่น Notification)
],
})
export class UserModule {}