251121:1700 Backend T3 wait testt
This commit is contained in:
@@ -16,11 +16,22 @@ import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
|
||||
import { RbacGuard } from '../../common/auth/rbac.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
@Controller('correspondences')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class CorrespondenceController {
|
||||
constructor(private readonly correspondenceService: CorrespondenceService) {}
|
||||
|
||||
@Post(':id/workflow/action')
|
||||
@RequirePermission('workflow.action_review') // สิทธิ์ในการกดอนุมัติ/ตรวจสอบ
|
||||
processAction(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() actionDto: WorkflowActionDto,
|
||||
@Request() req: any,
|
||||
) {
|
||||
return this.correspondenceService.processAction(id, actionDto, req.user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
|
||||
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DocumentNumberingModule } from '../document-numbering/document-numberin
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
|
||||
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
|
||||
@@ -18,6 +18,10 @@ import { User } from '../user/entities/user.entity.js';
|
||||
|
||||
// DTOs
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
|
||||
// Interfaces
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
@@ -46,15 +50,9 @@ export class CorrespondenceService {
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* สร้างเอกสารใหม่ (Create Correspondence)
|
||||
* - ตรวจสอบสิทธิ์และข้อมูลพื้นฐาน
|
||||
* - Validate JSON Details ตาม Type
|
||||
* - ขอเลขที่เอกสาร (Redis Lock)
|
||||
* - บันทึกข้อมูลลง DB (Transaction)
|
||||
*/
|
||||
// --- 1. CREATE DOCUMENT ---
|
||||
async create(createDto: CreateCorrespondenceDto, user: User) {
|
||||
// 1. ตรวจสอบข้อมูลพื้นฐาน (Type, Status, Org)
|
||||
// 1.1 Validate Basic Info
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { id: createDto.typeId },
|
||||
});
|
||||
@@ -64,39 +62,29 @@ export class CorrespondenceService {
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DRAFT not found in Master Data',
|
||||
);
|
||||
throw new InternalServerErrorException('Status DRAFT not found');
|
||||
}
|
||||
|
||||
const userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
);
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
}
|
||||
|
||||
// 2. Validate JSON Details (ถ้ามี)
|
||||
// 1.2 Validate JSON Details
|
||||
if (createDto.details) {
|
||||
try {
|
||||
// ใช้ Type Code เป็น Key ในการค้นหา Schema (เช่น 'RFA', 'LETTER')
|
||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||
} catch (error: any) {
|
||||
// บันทึก Warning หรือ Throw Error ตามนโยบาย (ในที่นี้ให้ผ่านไปก่อนถ้ายังไม่สร้าง Schema)
|
||||
console.warn(
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`,
|
||||
);
|
||||
console.warn(`Schema validation warning: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. เริ่ม Transaction
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 3.1 ขอเลขที่เอกสาร (Double-Lock Mechanism)
|
||||
// Mock ค่า replacements ไว้ก่อน (จริงๆ ต้อง Join เอา Org Code มา)
|
||||
// 1.3 Generate Document Number (Double-Lock)
|
||||
const docNumber = await this.numberingService.generateNextNumber(
|
||||
createDto.projectId,
|
||||
userOrgId,
|
||||
@@ -104,11 +92,11 @@ export class CorrespondenceService {
|
||||
new Date().getFullYear(),
|
||||
{
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: 'ORG', // TODO: Fetch real organization code
|
||||
ORG_CODE: 'ORG', // In real app, fetch user's org code
|
||||
},
|
||||
);
|
||||
|
||||
// 3.2 สร้าง Correspondence (หัวจดหมาย)
|
||||
// 1.4 Save Head
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.typeId,
|
||||
@@ -119,7 +107,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 3.3 สร้าง Revision แรก (Rev 0)
|
||||
// 1.5 Save First Revision
|
||||
const revision = queryRunner.manager.create(CorrespondenceRevision, {
|
||||
correspondenceId: savedCorr.id,
|
||||
revisionNumber: 0,
|
||||
@@ -132,7 +120,6 @@ export class CorrespondenceService {
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 4. Commit Transaction
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
@@ -140,7 +127,6 @@ export class CorrespondenceService {
|
||||
currentRevision: revision,
|
||||
};
|
||||
} catch (err) {
|
||||
// Rollback หากเกิดข้อผิดพลาด
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -148,37 +134,29 @@ export class CorrespondenceService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึงข้อมูลเอกสารทั้งหมด (สำหรับ List Page)
|
||||
*/
|
||||
// --- READ ---
|
||||
async findAll() {
|
||||
return this.correspondenceRepo.find({
|
||||
relations: ['revisions', 'type', 'project', 'originator'],
|
||||
relations: ['revisions', 'type', 'project'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึงข้อมูลเอกสารรายตัว (Detail Page)
|
||||
*/
|
||||
async findOne(id: number) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['revisions', 'type', 'project', 'originator'],
|
||||
relations: ['revisions', 'type', 'project'],
|
||||
});
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`Correspondence with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return correspondence;
|
||||
}
|
||||
|
||||
/**
|
||||
* ส่งเอกสาร (Submit) เพื่อเริ่ม Workflow การอนุมัติ/ส่งต่อ
|
||||
*/
|
||||
// --- 2. SUBMIT WORKFLOW ---
|
||||
async submit(correspondenceId: number, templateId: number, user: User) {
|
||||
// 1. ดึงข้อมูลเอกสารและหา Revision ปัจจุบัน
|
||||
// 2.1 Get Document & Current Revision
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
@@ -188,13 +166,12 @@ export class CorrespondenceService {
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
}
|
||||
|
||||
// หา Revision ที่เป็น current
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision) {
|
||||
throw new NotFoundException('Current revision not found');
|
||||
}
|
||||
|
||||
// 2. ดึงข้อมูล Template และ Steps
|
||||
// 2.2 Get Template Config
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: templateId },
|
||||
relations: ['steps'],
|
||||
@@ -202,12 +179,9 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!template || !template.steps?.length) {
|
||||
throw new BadRequestException(
|
||||
'Invalid routing template or no steps defined',
|
||||
);
|
||||
throw new BadRequestException('Invalid routing template');
|
||||
}
|
||||
|
||||
// 3. เริ่ม Transaction
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
@@ -215,29 +189,23 @@ export class CorrespondenceService {
|
||||
try {
|
||||
const firstStep = template.steps[0];
|
||||
|
||||
// 3.1 สร้าง Routing Record แรก (Log การส่งต่อ)
|
||||
// 2.3 Create First Routing Record
|
||||
const routing = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id, // เชื่อมกับ Revision ID
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id, // ✅ Save templateId for reference
|
||||
sequence: 1,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: firstStep.toOrganizationId,
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT', // สถานะเริ่มต้นของการส่ง
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
),
|
||||
processedByUserId: user.user_id, // ผู้ส่ง (User ปัจจุบัน)
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
});
|
||||
await queryRunner.manager.save(routing);
|
||||
|
||||
// 3.2 (Optional) อัปเดตสถานะของ Revision เป็น 'SUBMITTED'
|
||||
// const statusSubmitted = await this.statusRepo.findOne({ where: { statusCode: 'SUBMITTED' } });
|
||||
// if (statusSubmitted) {
|
||||
// currentRevision.statusId = statusSubmitted.id;
|
||||
// await queryRunner.manager.save(currentRevision);
|
||||
// }
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return routing;
|
||||
} catch (err) {
|
||||
@@ -247,4 +215,138 @@ export class CorrespondenceService {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. PROCESS ACTION (Approve/Reject/Return) ---
|
||||
async processAction(
|
||||
correspondenceId: number,
|
||||
dto: WorkflowActionDto,
|
||||
user: User,
|
||||
) {
|
||||
// 3.1 Find Active Routing Step
|
||||
// Find correspondence first to ensure it exists
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
});
|
||||
|
||||
if (!correspondence)
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
|
||||
// Find the latest routing step
|
||||
const currentRouting = await this.routingRepo.findOne({
|
||||
where: {
|
||||
correspondenceId: currentRevision.id,
|
||||
// In real scenario, we might check status 'SENT' or 'RECEIVED'
|
||||
},
|
||||
order: { sequence: 'DESC' },
|
||||
relations: ['toOrganization'],
|
||||
});
|
||||
|
||||
if (
|
||||
!currentRouting ||
|
||||
currentRouting.status === 'ACTIONED' ||
|
||||
currentRouting.status === 'REJECTED'
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'No active workflow step found or step already processed',
|
||||
);
|
||||
}
|
||||
|
||||
// 3.2 Check Permissions
|
||||
// User must belong to the target organization of the current step
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'You are not authorized to process this step',
|
||||
);
|
||||
}
|
||||
|
||||
// 3.3 Load Template to find Next Step Config
|
||||
if (!currentRouting.templateId) {
|
||||
throw new InternalServerErrorException(
|
||||
'Routing record missing templateId',
|
||||
);
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: currentRouting.templateId },
|
||||
relations: ['steps'],
|
||||
});
|
||||
|
||||
if (!template || !template.steps) {
|
||||
throw new InternalServerErrorException('Template definition not found');
|
||||
}
|
||||
|
||||
const totalSteps = template.steps.length;
|
||||
const currentSeq = currentRouting.sequence;
|
||||
|
||||
// 3.4 Calculate Next State using Workflow Engine
|
||||
const result = this.workflowEngine.processAction(
|
||||
currentSeq,
|
||||
totalSteps,
|
||||
dto.action,
|
||||
dto.returnToSequence,
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 3.5 Update Current Step
|
||||
currentRouting.status =
|
||||
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.processedByUserId = user.user_id;
|
||||
currentRouting.processedAt = new Date();
|
||||
currentRouting.comments = dto.comments;
|
||||
|
||||
await queryRunner.manager.save(currentRouting);
|
||||
|
||||
// 3.6 Create Next Step (If exists and not rejected)
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
// ✅ Find config for next step from Template
|
||||
const nextStepConfig = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence,
|
||||
);
|
||||
|
||||
if (!nextStepConfig) {
|
||||
throw new InternalServerErrorException(
|
||||
`Configuration for step ${result.nextStepSequence} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextRouting = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id,
|
||||
sequence: result.nextStepSequence,
|
||||
fromOrganizationId: user.primaryOrganizationId, // Forwarded by current user
|
||||
toOrganizationId: nextStepConfig.toOrganizationId, // ✅ Real Target from Template
|
||||
stepPurpose: nextStepConfig.stepPurpose, // ✅ Real Purpose from Template
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() +
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
),
|
||||
});
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
|
||||
// 3.7 Update Document Status (Optional - if Engine suggests)
|
||||
if (result.shouldUpdateStatus) {
|
||||
// Example: Update revision status to APPROVED or REJECTED
|
||||
// await this.updateDocumentStatus(currentRevision, result.documentStatus);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { message: 'Action processed successfully', result };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsEnum, IsString, IsOptional, IsInt } from 'class-validator';
|
||||
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface.js';
|
||||
|
||||
export class WorkflowActionDto {
|
||||
@IsEnum(WorkflowAction)
|
||||
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comments?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
returnToSequence?: number; // ใช้กรณี action = RETURN
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { RoutingTemplate } from './routing-template.entity.js'; // <--- ✅ เพิ่ม Import นี้ครับ
|
||||
|
||||
@Entity('correspondence_routings')
|
||||
export class CorrespondenceRouting {
|
||||
@@ -16,7 +17,10 @@ export class CorrespondenceRouting {
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id' })
|
||||
correspondenceId!: number; // FK -> CorrespondenceRevision
|
||||
correspondenceId!: number;
|
||||
|
||||
@Column({ name: 'template_id', nullable: true })
|
||||
templateId?: number;
|
||||
|
||||
@Column()
|
||||
sequence!: number;
|
||||
@@ -31,7 +35,7 @@ export class CorrespondenceRouting {
|
||||
stepPurpose!: string;
|
||||
|
||||
@Column({ default: 'SENT' })
|
||||
status!: string; // SENT, RECEIVED, ACTIONED, FORWARDED, REPLIED
|
||||
status!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
comments?: string;
|
||||
@@ -53,6 +57,10 @@ export class CorrespondenceRouting {
|
||||
@JoinColumn({ name: 'correspondence_id' })
|
||||
correspondenceRevision?: CorrespondenceRevision;
|
||||
|
||||
@ManyToOne(() => RoutingTemplate) // ตอนนี้ TypeScript จะรู้จัก RoutingTemplate แล้ว
|
||||
@JoinColumn({ name: 'template_id' })
|
||||
template?: RoutingTemplate;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'from_organization_id' })
|
||||
fromOrganization?: Organization;
|
||||
|
||||
24
backend/src/modules/user/dto/assign-role.dto.ts
Normal file
24
backend/src/modules/user/dto/assign-role.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsInt, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
|
||||
|
||||
export class AssignRoleDto {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
roleId!: number;
|
||||
|
||||
// Scope (ต้องส่งมาอย่างน้อย 1 อัน หรือไม่ส่งเลยถ้าเป็น Global)
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
organizationId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
projectId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
contractId?: number;
|
||||
}
|
||||
42
backend/src/modules/user/entities/user-assignment.entity.ts
Normal file
42
backend/src/modules/user/entities/user-assignment.entity.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity.js';
|
||||
// Import Role, Org, Project, Contract entities...
|
||||
|
||||
@Entity('user_assignments')
|
||||
export class UserAssignment {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId!: number;
|
||||
|
||||
@Column({ name: 'role_id' })
|
||||
roleId!: number;
|
||||
|
||||
@Column({ name: 'organization_id', nullable: true })
|
||||
organizationId?: number;
|
||||
|
||||
@Column({ name: 'project_id', nullable: true })
|
||||
projectId?: number;
|
||||
|
||||
@Column({ name: 'contract_id', nullable: true })
|
||||
contractId?: number;
|
||||
|
||||
@Column({ name: 'assigned_by_user_id', nullable: true })
|
||||
assignedByUserId?: number;
|
||||
|
||||
@CreateDateColumn({ name: 'assigned_at' })
|
||||
assignedAt!: Date;
|
||||
|
||||
// Relation กลับไปหา User (เจ้าของสิทธิ์)
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User;
|
||||
}
|
||||
38
backend/src/modules/user/user-assignment.service.ts
Normal file
38
backend/src/modules/user/user-assignment.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserAssignment } from './entities/user-assignment.entity.js'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
|
||||
import { AssignRoleDto } from './dto/assign-role.dto.js';
|
||||
import { User } from './entities/user.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserAssignmentService {
|
||||
constructor(
|
||||
@InjectRepository(UserAssignment)
|
||||
private assignmentRepo: Repository<UserAssignment>,
|
||||
) {}
|
||||
|
||||
async assignRole(dto: AssignRoleDto, assigner: User) {
|
||||
// Validation: ตรวจสอบกฎเหล็ก (เลือกได้แค่ Scope เดียว)
|
||||
const scopes = [dto.organizationId, dto.projectId, dto.contractId].filter(
|
||||
(v) => v != null,
|
||||
);
|
||||
if (scopes.length > 1) {
|
||||
throw new BadRequestException(
|
||||
'Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.',
|
||||
);
|
||||
}
|
||||
|
||||
// สร้าง Assignment
|
||||
const assignment = this.assignmentRepo.create({
|
||||
userId: dto.userId,
|
||||
roleId: dto.roleId,
|
||||
organizationId: dto.organizationId,
|
||||
projectId: dto.projectId,
|
||||
contractId: dto.contractId,
|
||||
assignedByUserId: assigner.user_id, // เก็บ Log ว่าใครเป็นคนให้สิทธิ์
|
||||
});
|
||||
|
||||
return this.assignmentRepo.save(assignment);
|
||||
}
|
||||
}
|
||||
@@ -8,40 +8,48 @@ 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 { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
import { RbacGuard } from '../../common/auth/rbac.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard) // 🔒 เพิ่ม RbacGuard ต่อท้าย) // 🔒 บังคับ Login ทุก Endpoints ในนี้
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly assignmentService: UserAssignmentService, // <--- ✅ Inject Service เข้ามา
|
||||
) {}
|
||||
|
||||
// --- User CRUD ---
|
||||
|
||||
// 1. สร้างผู้ใช้ใหม่
|
||||
@Post()
|
||||
@RequirePermission('user.create') // 🔒 ต้องมีสิทธิ์ user.create ถึงจะเข้าได้
|
||||
@RequirePermission('user.create')
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.userService.create(createUserDto);
|
||||
}
|
||||
|
||||
// 2. ดูรายชื่อผู้ใช้ทั้งหมด
|
||||
@Get()
|
||||
@RequirePermission('user.view')
|
||||
findAll() {
|
||||
return this.userService.findAll();
|
||||
}
|
||||
|
||||
// 3. ดูข้อมูลผู้ใช้รายคน (ตาม ID)
|
||||
@Get(':id')
|
||||
@RequirePermission('user.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.findOne(id);
|
||||
}
|
||||
|
||||
// 4. แก้ไขข้อมูลผู้ใช้
|
||||
@Patch(':id')
|
||||
@RequirePermission('user.edit')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
@@ -49,9 +57,17 @@ export class UserController {
|
||||
return this.userService.update(id, updateUserDto);
|
||||
}
|
||||
|
||||
// 5. ลบผู้ใช้ (Soft Delete)
|
||||
@Delete(':id')
|
||||
@RequirePermission('user.delete')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.remove(id);
|
||||
}
|
||||
|
||||
// --- Role Assignment ---
|
||||
|
||||
@Post('assign-role') // <--- ✅ ต้องมี @ เสมอครับ
|
||||
@RequirePermission('permission.assign')
|
||||
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
|
||||
return this.assignmentService.assignRole(dto, req.user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,22 @@ 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';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])], // จดทะเบียน Entity
|
||||
// 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
|
||||
imports: [
|
||||
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
|
||||
TypeOrmModule.forFeature([User, UserAssignment]),
|
||||
], // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService], // Export ให้ AuthModule เรียกใช้ได้
|
||||
providers: [
|
||||
UserService,
|
||||
UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider
|
||||
],
|
||||
exports: [
|
||||
UserService,
|
||||
UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้
|
||||
], // Export ให้ AuthModule เรียกใช้ได้
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
import { WorkflowAction } from './interfaces/workflow.interface';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
describe('WorkflowEngineService', () => {
|
||||
let service: WorkflowEngineService;
|
||||
@@ -15,4 +17,50 @@ describe('WorkflowEngineService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('processAction', () => {
|
||||
// 🟢 กรณี: อนุมัติทั่วไป (ไปขั้นต่อไป)
|
||||
it('should move to next step on APPROVE', () => {
|
||||
const result = service.processAction(1, 3, WorkflowAction.APPROVE);
|
||||
expect(result.nextStepSequence).toBe(2);
|
||||
expect(result.shouldUpdateStatus).toBe(false);
|
||||
});
|
||||
|
||||
// 🟢 กรณี: อนุมัติขั้นตอนสุดท้าย (จบงาน)
|
||||
it('should complete workflow on APPROVE at last step', () => {
|
||||
const result = service.processAction(3, 3, WorkflowAction.APPROVE);
|
||||
expect(result.nextStepSequence).toBeNull(); // ไม่มีขั้นต่อไป
|
||||
expect(result.shouldUpdateStatus).toBe(true);
|
||||
expect(result.documentStatus).toBe('COMPLETED');
|
||||
});
|
||||
|
||||
// 🔴 กรณี: ปฏิเสธ (จบงานทันที)
|
||||
it('should stop workflow on REJECT', () => {
|
||||
const result = service.processAction(1, 3, WorkflowAction.REJECT);
|
||||
expect(result.nextStepSequence).toBeNull();
|
||||
expect(result.shouldUpdateStatus).toBe(true);
|
||||
expect(result.documentStatus).toBe('REJECTED');
|
||||
});
|
||||
|
||||
// 🟠 กรณี: ส่งกลับ (ย้อนกลับ 1 ขั้น)
|
||||
it('should return to previous step on RETURN', () => {
|
||||
const result = service.processAction(2, 3, WorkflowAction.RETURN);
|
||||
expect(result.nextStepSequence).toBe(1);
|
||||
expect(result.shouldUpdateStatus).toBe(true);
|
||||
expect(result.documentStatus).toBe('REVISE_REQUIRED');
|
||||
});
|
||||
|
||||
// 🟠 กรณี: ส่งกลับ (ระบุขั้น)
|
||||
it('should return to specific step on RETURN', () => {
|
||||
const result = service.processAction(3, 5, WorkflowAction.RETURN, 1);
|
||||
expect(result.nextStepSequence).toBe(1);
|
||||
});
|
||||
|
||||
// ❌ กรณี: Error (ส่งกลับต่ำกว่า 1)
|
||||
it('should throw error if return step is invalid', () => {
|
||||
expect(() => {
|
||||
service.processAction(1, 3, WorkflowAction.RETURN);
|
||||
}).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
BIN
backend/uploads/temp/d60d9807-a22d-4ca0-b99a-5d5d8b81b3e8.pdf
Normal file
BIN
backend/uploads/temp/d60d9807-a22d-4ca0-b99a-5d5d8b81b3e8.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user