251123:0200 T6.1 to DO

This commit is contained in:
2025-11-23 02:23:38 +07:00
parent 17d9f172d4
commit 23006898d9
58 changed files with 3221 additions and 502 deletions

View File

@@ -0,0 +1,36 @@
import {
IsInt,
IsString,
IsOptional,
IsArray,
IsNotEmpty,
IsEnum,
} from 'class-validator';
import { Type } from 'class-transformer';
// Enum นี้ควรตรงกับใน Entity หรือสร้างไฟล์ enum แยก (ในที่นี้ใส่ไว้ใน DTO เพื่อความสะดวก)
export enum TransmittalPurpose {
FOR_APPROVAL = 'FOR_APPROVAL',
FOR_INFORMATION = 'FOR_INFORMATION',
FOR_REVIEW = 'FOR_REVIEW',
OTHER = 'OTHER',
}
export class CreateTransmittalDto {
@IsInt()
@IsNotEmpty()
projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร (Running 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,34 @@
import {
IsInt,
IsOptional,
IsString,
IsEnum,
IsNotEmpty,
} from 'class-validator';
import { Type } from 'class-transformer';
import { TransmittalPurpose } from './create-transmittal.dto';
export class SearchTransmittalDto {
@IsInt()
@Type(() => Number)
@IsNotEmpty()
projectId!: number; // บังคับระบุ Project
@IsEnum(TransmittalPurpose)
@IsOptional()
purpose?: TransmittalPurpose;
@IsString()
@IsOptional()
search?: string; // ค้นหาจากเลขที่เอกสาร หรือ remarks
@IsOptional()
@IsInt()
@Type(() => Number)
page: number = 1;
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20;
}

View File

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

View File

@@ -0,0 +1,36 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} 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' })
transmittalId!: number;
@Column({ name: 'item_correspondence_id' })
itemCorrespondenceId!: number;
@Column({ default: 1 })
quantity!: number;
@Column({ length: 255, nullable: true })
remarks?: string;
// Relations
@ManyToOne(() => Transmittal, (t) => t.items, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'transmittal_id' })
transmittal!: Transmittal;
@ManyToOne(() => Correspondence)
@JoinColumn({ name: 'item_correspondence_id' })
itemDocument!: Correspondence;
}

View File

@@ -0,0 +1,36 @@
import {
Entity,
PrimaryColumn,
Column,
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' })
correspondenceId!: number;
@Column({
type: 'enum',
enum: ['FOR_APPROVAL', 'FOR_INFORMATION', 'FOR_REVIEW', 'OTHER'],
nullable: true,
})
purpose?: string;
@Column({ type: 'text', nullable: true })
remarks?: string;
// Relations
@OneToOne(() => Correspondence)
@JoinColumn({ name: 'correspondence_id' })
correspondence!: Correspondence;
@OneToMany(() => TransmittalItem, (item) => item.transmittal, {
cascade: true,
})
items!: TransmittalItem[];
}

View File

@@ -0,0 +1,53 @@
import {
Controller,
Get,
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 { CurrentUser } from '../../common/decorators/current-user.decorator';
@ApiTags('Transmittals')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('transmittals')
export class TransmittalController {
constructor(private readonly transmittalService: TransmittalService) {}
@Post()
@ApiOperation({ summary: 'Create new Transmittal' })
@RequirePermission('transmittal.create') // สิทธิ์ ID 40
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);
}
*/
}

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
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 { TransmittalService } from './transmittal.service';
import { TransmittalController } from './transmittal.controller';
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
@Module({
imports: [
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
DocumentNumberingModule,
],
controllers: [TransmittalController],
providers: [TransmittalService],
exports: [TransmittalService],
})
export class TransmittalModule {}

View File

@@ -0,0 +1,95 @@
import {
Injectable,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { Transmittal } from './entities/transmittal.entity';
import { TransmittalItem } from './entities/transmittal-item.entity';
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';
@Injectable()
export class TransmittalService {
constructor(
@InjectRepository(Transmittal)
private transmittalRepo: Repository<Transmittal>,
@InjectRepository(TransmittalItem)
private transmittalItemRepo: Repository<TransmittalItem>,
@InjectRepository(Correspondence)
private correspondenceRepo: Repository<Correspondence>,
private numberingService: DocumentNumberingService,
private dataSource: DataSource,
) {}
async create(createDto: CreateTransmittalDto, user: User) {
// ✅ FIX: ตรวจสอบว่า User มีสังกัดองค์กรหรือไม่
if (!user.primaryOrganizationId) {
throw new BadRequestException(
'User must belong to an organization to create documents',
);
}
const userOrgId = user.primaryOrganizationId; // TypeScript จะรู้ว่าเป็น number แล้ว
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Generate Document Number
const transmittalTypeId = 3; // TODO: ควรดึง ID จริงจาก DB หรือ Config
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
const docNumber = await this.numberingService.generateNextNumber(
createDto.projectId,
userOrgId, // ✅ ส่งค่าที่เช็คแล้ว
transmittalTypeId,
new Date().getFullYear(),
{ TYPE_CODE: 'TR', ORG_CODE: orgCode },
);
// 2. Create Correspondence (Header)
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber,
correspondenceTypeId: transmittalTypeId,
projectId: createDto.projectId,
originatorId: userOrgId, // ✅ ส่งค่าที่เช็คแล้ว
isInternal: false,
createdBy: user.user_id,
});
const savedCorr = await queryRunner.manager.save(correspondence);
// 3. Create Transmittal (Detail)
const transmittal = queryRunner.manager.create(Transmittal, {
correspondenceId: savedCorr.id,
purpose: createDto.purpose,
remarks: createDto.remarks,
});
await queryRunner.manager.save(transmittal);
// 4. Link Items (Documents being sent)
if (createDto.itemIds && createDto.itemIds.length > 0) {
const items = createDto.itemIds.map((itemId) =>
queryRunner.manager.create(TransmittalItem, {
transmittalId: savedCorr.id,
itemCorrespondenceId: itemId,
quantity: 1,
}),
);
await queryRunner.manager.save(items);
}
await queryRunner.commitTransaction();
return { ...savedCorr, transmittal };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}