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
@@ -0,0 +1,83 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
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 นี้
@Injectable()
export class CirculationService {
constructor(
@InjectRepository(Circulation)
private circulationRepo: Repository<Circulation>,
@InjectRepository(CirculationRouting)
private routingRepo: Repository<CirculationRouting>,
private dataSource: DataSource,
) {}
async create(createDto: CreateCirculationDto, user: User) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Create Master Circulation
// TODO: Generate Circulation No. logic here (Simple format)
const circulationNo = `CIR-${Date.now()}`;
const circulation = queryRunner.manager.create(Circulation, {
organizationId: user.primaryOrganizationId,
correspondenceId: createDto.correspondenceId,
circulationNo: circulationNo,
subject: createDto.subject,
statusCode: 'OPEN',
createdByUserId: user.user_id,
});
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
assignedTo: userId,
status: 'PENDING',
}),
);
await queryRunner.manager.save(routings);
}
await queryRunner.commitTransaction();
return savedCirculation;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
async findOne(id: number) {
const circulation = await this.circulationRepo.findOne({
where: { id },
relations: ['routings', 'routings.assignee', 'correspondence'],
});
if (!circulation) throw new NotFoundException('Circulation not found');
return circulation;
}
// Method update status (Complete task)
async updateRoutingStatus(
routingId: number,
status: string,
comments: string,
user: User,
) {
// Logic to update routing status
// and Check if all routings are completed -> Close Circulation
}
}
@@ -0,0 +1,26 @@
import {
IsInt,
IsString,
IsNotEmpty,
IsArray,
IsOptional,
} from 'class-validator';
export class CreateCirculationDto {
@IsInt()
@IsNotEmpty()
correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน
@IsString()
@IsNotEmpty()
subject!: string; // หัวข้อเรื่อง (Subject)
@IsArray()
@IsInt({ each: true })
@IsNotEmpty()
assigneeIds!: number[]; // รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ)
@IsString()
@IsOptional()
remarks?: string; // หมายเหตุเพิ่มเติม (ถ้ามี)
}
@@ -0,0 +1,34 @@
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,62 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Circulation } from './circulation.entity';
import { Organization } from '../../project/entities/organization.entity';
import { User } from '../../user/entities/user.entity';
@Entity('circulation_routings')
export class CirculationRouting {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'circulation_id' })
circulationId!: number;
@Column({ name: 'step_number' })
stepNumber!: number;
@Column({ name: 'organization_id' })
organizationId!: number;
@Column({ name: 'assigned_to', nullable: true })
assignedTo?: number;
@Column({
type: 'enum',
enum: ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED'],
default: 'PENDING',
})
status!: string;
@Column({ type: 'text', nullable: true })
comments?: string;
@Column({ name: 'completed_at', type: 'datetime', nullable: true })
completedAt?: Date;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
// Relations
@ManyToOne(() => Circulation, (c) => c.routings, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'circulation_id' })
circulation!: Circulation;
@ManyToOne(() => Organization)
@JoinColumn({ name: 'organization_id' })
organization!: Organization;
@ManyToOne(() => User)
@JoinColumn({ name: 'assigned_to' })
assignee?: User;
}
@@ -0,0 +1,19 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('circulation_status_codes')
export class CirculationStatusCode {
@PrimaryGeneratedColumn()
id!: number;
@Column({ length: 20, unique: true })
code!: string;
@Column({ length: 50 })
description!: string;
@Column({ name: 'sort_order', default: 0 })
sortOrder!: number;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
}
@@ -0,0 +1,73 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
import { Organization } from '../../project/entities/organization.entity';
import { User } from '../../user/entities/user.entity';
import { CirculationStatusCode } from './circulation-status-code.entity';
import { CirculationRouting } from './circulation-routing.entity';
@Entity('circulations')
export class Circulation {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'correspondence_id', nullable: true })
correspondenceId?: number;
@Column({ name: 'organization_id' })
organizationId!: number;
@Column({ name: 'circulation_no', length: 100 })
circulationNo!: string;
@Column({ name: 'circulation_subject', length: 500 })
subject!: string;
@Column({ name: 'circulation_status_code' })
statusCode!: string;
@Column({ name: 'created_by_user_id' })
createdByUserId!: number;
@Column({ name: 'submitted_at', type: 'timestamp', nullable: true })
submittedAt?: Date;
@Column({ name: 'closed_at', type: 'timestamp', nullable: true })
closedAt?: Date;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
// Relations
@ManyToOne(() => Correspondence)
@JoinColumn({ name: 'correspondence_id' })
correspondence?: Correspondence;
@ManyToOne(() => Organization)
@JoinColumn({ name: 'organization_id' })
organization!: Organization;
@ManyToOne(() => CirculationStatusCode)
@JoinColumn({ name: 'circulation_status_code', referencedColumnName: 'code' })
status!: CirculationStatusCode;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by_user_id' })
creator!: User;
@OneToMany(() => CirculationRouting, (routing) => routing.circulation, {
cascade: true,
})
routings!: CirculationRouting[];
}