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
@@ -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);
}
}
@@ -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 {}
@@ -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;
}
}
@@ -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()
@@ -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
}
@@ -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;
}
@@ -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; // ความคิดเห็นเพิ่มเติม
}