251204:1700 Prepare to version 1.5.1

This commit is contained in:
admin
2025-12-04 16:50:09 +07:00
parent 05f8f4403a
commit dc8b80c5f9
34 changed files with 8518 additions and 3107 deletions
@@ -4,11 +4,12 @@ import {
IsOptional,
IsArray,
IsNotEmpty,
ValidateNested,
IsEnum,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
// Enum นี้ควรตรงกับใน Entity หรือสร้างไฟล์ enum แยก (ในที่นี้ใส่ไว้ใน DTO เพื่อความสะดวก)
export enum TransmittalPurpose {
FOR_APPROVAL = 'FOR_APPROVAL',
FOR_INFORMATION = 'FOR_INFORMATION',
@@ -16,21 +17,57 @@ export enum TransmittalPurpose {
OTHER = 'OTHER',
}
export class CreateTransmittalDto {
export class TransmittalItemDto {
@ApiProperty({
description: 'ประเภทรายการ (DRAWING, RFA, CORRESPONDENCE)',
example: 'DRAWING',
})
@IsString()
@IsNotEmpty()
itemType!: string;
@ApiProperty({ description: 'ID ของรายการ', example: 1 })
@IsInt()
@IsNotEmpty()
projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร (Running Number)
@IsEnum(TransmittalPurpose)
@IsOptional()
purpose?: TransmittalPurpose; // วัตถุประสงค์การส่ง
itemId!: number;
@ApiProperty({ description: 'รายละเอียดเพิ่มเติม', required: false })
@IsString()
@IsOptional()
remarks?: string; // หมายเหตุเพิ่มเติม
@IsArray()
@IsInt({ each: true })
@IsNotEmpty()
itemIds!: number[]; // ID ของเอกสาร (Correspondence IDs) ที่จะแนบไปใน Transmittal นี้
description?: string;
}
export class CreateTransmittalDto {
@ApiProperty({ description: 'ID ของโครงการ', example: 1 })
@IsInt()
@IsNotEmpty()
projectId!: number;
@ApiProperty({
description: 'เรื่อง',
example: 'Transmittal for Shop Drawings',
})
@IsString()
@IsNotEmpty()
subject!: string;
@ApiProperty({ description: 'ผู้รับ (Organization ID)', example: 2 })
@IsInt()
@IsNotEmpty()
recipientOrganizationId!: number;
@ApiProperty({
description: 'วัตถุประสงค์',
enum: TransmittalPurpose,
example: TransmittalPurpose.FOR_APPROVAL,
})
@IsEnum(TransmittalPurpose)
@IsOptional()
purpose?: TransmittalPurpose;
@ApiProperty({ description: 'รายการที่แนบ', type: [TransmittalItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => TransmittalItemDto)
items!: TransmittalItemDto[];
}
@@ -1,36 +1,22 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Entity, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { Transmittal } from './transmittal.entity';
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
@Entity('transmittal_items')
export class TransmittalItem {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'transmittal_id' })
@PrimaryColumn({ name: 'transmittal_id' })
transmittalId!: number;
@Column({ name: 'item_correspondence_id' })
itemCorrespondenceId!: number;
@PrimaryColumn({ name: 'item_type', length: 50 })
itemType!: string; // DRAWING, RFA, etc.
@Column({ default: 1 })
quantity!: number;
@PrimaryColumn({ name: 'item_id' })
itemId!: number;
@Column({ length: 255, nullable: true })
remarks?: string;
@Column({ type: 'text', nullable: true })
description?: string;
// Relations
@ManyToOne(() => Transmittal, (t) => t.items, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'transmittal_id' })
transmittal!: Transmittal;
@ManyToOne(() => Correspondence)
@JoinColumn({ name: 'item_correspondence_id' })
itemDocument!: Correspondence;
}
@@ -1,19 +1,29 @@
import {
Entity,
PrimaryColumn,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany,
OneToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
import { TransmittalItem } from './transmittal-item.entity';
@Entity('transmittals')
export class Transmittal {
@PrimaryColumn({ name: 'correspondence_id' })
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'correspondence_id', unique: true })
correspondenceId!: number;
@Column({ name: 'transmittal_no', length: 100 })
transmittalNo!: string;
@Column({ length: 500 })
subject!: string;
@Column({
type: 'enum',
enum: ['FOR_APPROVAL', 'FOR_INFORMATION', 'FOR_REVIEW', 'OTHER'],
@@ -24,6 +34,9 @@ export class Transmittal {
@Column({ type: 'text', nullable: true })
remarks?: string;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
// Relations
@OneToOne(() => Correspondence)
@JoinColumn({ name: 'correspondence_id' })
@@ -4,50 +4,32 @@ import {
Post,
Body,
Param,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { TransmittalService } from './transmittal.service';
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
import { SearchTransmittalDto } from './dto/search-transmittal.dto'; // เดี๋ยวสร้าง 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 { User } from '../user/entities/user.entity';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Audit } from '../../common/decorators/audit.decorator'; // Import
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
@ApiTags('Transmittals')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@UseGuards(JwtAuthGuard)
@Controller('transmittals')
export class TransmittalController {
constructor(private readonly transmittalService: TransmittalService) {}
@Post()
@ApiOperation({ summary: 'Create new Transmittal' })
@RequirePermission('transmittal.create') // สิทธิ์ ID 40
@Audit('transmittal.create', 'transmittal') // ✅ แปะตรงนี้
@ApiOperation({ summary: 'Create a new 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);
}
@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);
}
}
@@ -3,14 +3,23 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Transmittal } from './entities/transmittal.entity';
import { TransmittalItem } from './entities/transmittal-item.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.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';
import { SearchModule } from '../search/search.module'; // ✅ ต้อง Import เพราะ Service ใช้ (ที่เป็นสาเหตุ Error)
import { SearchModule } from '../search/search.module';
@Module({
imports: [
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
TypeOrmModule.forFeature([
Transmittal,
TransmittalItem,
Correspondence,
CorrespondenceType,
CorrespondenceStatus,
]),
DocumentNumberingModule,
UserModule,
SearchModule,
@@ -1,98 +1,143 @@
// File: src/modules/transmittal/transmittal.service.ts
import {
Injectable,
Logger,
NotFoundException,
InternalServerErrorException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { Transmittal } from './entities/transmittal.entity.js';
import { TransmittalItem } from './entities/transmittal-item.entity.js';
import { Correspondence } from '../correspondence/entities/correspondence.entity.js';
import { CreateTransmittalDto } from './dto/create-transmittal.dto.js';
import { User } from '../user/entities/user.entity.js';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
import { SearchService } from '../search/search.service.js';
import { Repository, DataSource } from 'typeorm';
import { Transmittal } from './entities/transmittal.entity';
import { TransmittalItem } from './entities/transmittal-item.entity';
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
import { User } from '../user/entities/user.entity';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
@Injectable()
export class TransmittalService {
private readonly logger = new Logger(TransmittalService.name);
constructor(
@InjectRepository(Transmittal)
private transmittalRepo: Repository<Transmittal>,
@InjectRepository(TransmittalItem)
private transmittalItemRepo: Repository<TransmittalItem>,
@InjectRepository(Correspondence)
private correspondenceRepo: Repository<Correspondence>,
private itemRepo: Repository<TransmittalItem>,
@InjectRepository(CorrespondenceType)
private typeRepo: Repository<CorrespondenceType>,
@InjectRepository(CorrespondenceStatus)
private statusRepo: Repository<CorrespondenceStatus>,
private numberingService: DocumentNumberingService,
private dataSource: DataSource,
private searchService: SearchService,
private dataSource: DataSource
) {}
async create(createDto: CreateTransmittalDto, user: User) {
if (!user.primaryOrganizationId) {
throw new BadRequestException(
'User must belong to an organization to create documents',
);
}
const userOrgId = user.primaryOrganizationId;
// 1. Get Transmittal Type (Assuming Code '901' or 'TRN')
const type = await this.typeRepo.findOne({
where: { typeCode: 'TRN' }, // Adjust code as per Master Data
});
if (!type) throw new NotFoundException('Transmittal Type (TRN) not found');
const statusDraft = await this.statusRepo.findOne({
where: { statusCode: 'DRAFT' },
});
if (!statusDraft)
throw new InternalServerErrorException('Status DRAFT not found');
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const transmittalTypeId = 3; // TODO: ดึง ID จริงจาก DB หรือ Config
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
if (!user.primaryOrganizationId) {
throw new BadRequestException(
'User must belong to an organization to create a transmittal'
);
}
// [FIXED] เรียกใช้แบบ Object Context
try {
// 2. Generate Number
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
originatorId: userOrgId,
typeId: transmittalTypeId,
originatorId: user.primaryOrganizationId,
typeId: type.id,
year: new Date().getFullYear(),
customTokens: {
TYPE_CODE: 'TR',
ORG_CODE: orgCode,
TYPE_CODE: type.typeCode,
ORG_CODE: 'ORG', // TODO: Fetch real ORG Code
},
});
// 3. Create Correspondence (Parent)
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber,
correspondenceTypeId: transmittalTypeId,
correspondenceTypeId: type.id,
projectId: createDto.projectId,
originatorId: userOrgId,
originatorId: user.primaryOrganizationId,
isInternal: false,
createdBy: user.user_id,
});
const savedCorr = await queryRunner.manager.save(correspondence);
// 4. Create Revision (Draft)
const revision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: savedCorr.id,
revisionNumber: 0,
revisionLabel: '0',
isCurrent: true,
statusId: statusDraft.id,
title: createDto.subject,
createdBy: user.user_id,
});
await queryRunner.manager.save(revision);
// 5. Create Transmittal
const transmittal = queryRunner.manager.create(Transmittal, {
correspondenceId: savedCorr.id,
purpose: createDto.purpose,
remarks: createDto.remarks,
transmittalNo: docNumber,
subject: createDto.subject,
});
await queryRunner.manager.save(transmittal);
const savedTransmittal = await queryRunner.manager.save(transmittal);
if (createDto.itemIds && createDto.itemIds.length > 0) {
const items = createDto.itemIds.map((itemId) =>
// 6. Create Items
if (createDto.items && createDto.items.length > 0) {
const items = createDto.items.map((item) =>
queryRunner.manager.create(TransmittalItem, {
transmittalId: savedCorr.id,
itemCorrespondenceId: itemId,
quantity: 1,
}),
transmittalId: savedTransmittal.id,
itemType: item.itemType,
itemId: item.itemId,
description: item.description,
})
);
await queryRunner.manager.save(items);
}
await queryRunner.commitTransaction();
return { ...savedCorr, transmittal };
return {
...savedTransmittal,
correspondence: savedCorr,
};
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create transmittal: ${(err as Error).message}`
);
throw err;
} finally {
await queryRunner.release();
}
}
}
async findOne(id: number) {
const transmittal = await this.transmittalRepo.findOne({
where: { id },
relations: ['correspondence', 'items'],
});
if (!transmittal)
throw new NotFoundException(`Transmittal ID ${id} not found`);
return transmittal;
}
}