260321:1700 Correct Coresspondence / Doing RFA
This commit is contained in:
@@ -4,10 +4,10 @@ services:
|
||||
container_name: mariadb-local
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: Center#2025
|
||||
MYSQL_ROOT_PASSWORD: Center2025
|
||||
MYSQL_DATABASE: lcbp3_dev
|
||||
MYSQL_USER: admin
|
||||
MYSQL_PASSWORD: Center#2025
|
||||
MYSQL_PASSWORD: Center2025
|
||||
ports:
|
||||
- '3306:3306'
|
||||
volumes:
|
||||
@@ -47,9 +47,9 @@ services:
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false # ปิด security เพื่อความง่ายใน Dev (Prod ต้องเปิด)
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
- 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- '9200:9200'
|
||||
volumes:
|
||||
- esdata:/usr/share/elasticsearch/data
|
||||
networks:
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.7.5",
|
||||
"@elastic/elasticsearch": "^9.3.4",
|
||||
"@elastic/elasticsearch": "^8.13.0",
|
||||
"@nestjs-modules/ioredis": "^2.0.2",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
@@ -61,9 +61,9 @@
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"joi": "^18.0.1",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.15.3",
|
||||
"ms": "^2.1.3",
|
||||
"nest-winston": "^1.10.2",
|
||||
"nodemailer": "^8.0.3",
|
||||
"opossum": "^9.0.0",
|
||||
@@ -92,8 +92,8 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/opossum": "^8.1.9",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
|
||||
@@ -102,4 +102,32 @@ export class UuidResolverService {
|
||||
async resolveContractId(contractId: number | string): Promise<number> {
|
||||
return this.resolve('Contract', 'contracts', 'id', contractId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve shopDrawingRevisionId (INT or UUID string) to internal INT ID.
|
||||
*/
|
||||
async resolveShopDrawingRevisionId(
|
||||
shopDrawingRevisionId: number | string
|
||||
): Promise<number> {
|
||||
return this.resolve(
|
||||
'Shop Drawing Revision',
|
||||
'shop_drawing_revisions',
|
||||
'id',
|
||||
shopDrawingRevisionId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve asBuiltDrawingRevisionId (INT or UUID string) to internal INT ID.
|
||||
*/
|
||||
async resolveAsBuiltDrawingRevisionId(
|
||||
asBuiltDrawingRevisionId: number | string
|
||||
): Promise<number> {
|
||||
return this.resolve(
|
||||
'As-Built Drawing Revision',
|
||||
'asbuilt_drawing_revisions',
|
||||
'id',
|
||||
asBuiltDrawingRevisionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,9 @@ export class CorrespondenceService {
|
||||
issuedDate: createDto.issuedDate
|
||||
? new Date(createDto.issuedDate)
|
||||
: undefined,
|
||||
receivedDate: createDto.receivedDate
|
||||
? new Date(createDto.receivedDate)
|
||||
: undefined,
|
||||
description: createDto.description,
|
||||
details: createDto.details,
|
||||
createdBy: user.user_id,
|
||||
@@ -521,6 +524,8 @@ export class CorrespondenceService {
|
||||
revisionUpdate.documentDate = new Date(updateDto.documentDate);
|
||||
if (updateDto.issuedDate)
|
||||
revisionUpdate.issuedDate = new Date(updateDto.issuedDate);
|
||||
if (updateDto.receivedDate)
|
||||
revisionUpdate.receivedDate = new Date(updateDto.receivedDate);
|
||||
if (updateDto.description)
|
||||
revisionUpdate.description = updateDto.description;
|
||||
if (updateDto.details) revisionUpdate.details = updateDto.details;
|
||||
|
||||
@@ -99,6 +99,14 @@ export class CreateCorrespondenceDto {
|
||||
@IsOptional()
|
||||
issuedDate?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Received Date (วันที่รับเอกสาร)',
|
||||
example: '2025-12-06T00:00:00Z',
|
||||
})
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
receivedDate?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Attachment temp IDs from upload phase (Two-Phase Storage)',
|
||||
example: ['uuid-temp-1', 'uuid-temp-2'],
|
||||
|
||||
+7
-7
@@ -32,7 +32,7 @@ export class DocumentNumberingAdminController {
|
||||
|
||||
@Get('templates')
|
||||
@ApiOperation({ summary: 'Get all document numbering templates' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.view_formats')
|
||||
async getTemplates(@Query('projectId') projectId?: number) {
|
||||
if (projectId) {
|
||||
return this.service.getTemplatesByProject(projectId);
|
||||
@@ -42,7 +42,7 @@ export class DocumentNumberingAdminController {
|
||||
|
||||
@Post('templates')
|
||||
@ApiOperation({ summary: 'Create or Update a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async saveTemplate(
|
||||
@Body() dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
|
||||
) {
|
||||
@@ -51,7 +51,7 @@ export class DocumentNumberingAdminController {
|
||||
|
||||
@Delete('templates/:id')
|
||||
@ApiOperation({ summary: 'Delete a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async deleteTemplate(@Param('id', ParseIntPipe) id: number) {
|
||||
await this.service.deleteTemplate(id);
|
||||
return { success: true };
|
||||
@@ -78,7 +78,7 @@ export class DocumentNumberingAdminController {
|
||||
@ApiOperation({
|
||||
summary: 'Manually override or set a document number counter',
|
||||
})
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async manualOverride(
|
||||
@Body() dto: ManualOverrideDto,
|
||||
@CurrentUser() user: User
|
||||
@@ -88,7 +88,7 @@ export class DocumentNumberingAdminController {
|
||||
|
||||
@Post('void-and-replace')
|
||||
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async voidAndReplace(
|
||||
@Body()
|
||||
dto: {
|
||||
@@ -104,7 +104,7 @@ export class DocumentNumberingAdminController {
|
||||
|
||||
@Post('cancel')
|
||||
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async cancelNumber(
|
||||
@Body()
|
||||
dto: {
|
||||
@@ -119,7 +119,7 @@ export class DocumentNumberingAdminController {
|
||||
|
||||
@Post('bulk-import')
|
||||
@ApiOperation({ summary: 'Bulk import/set document number counters' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async bulkImport(@Body() items: ManualOverrideDto[]) {
|
||||
return this.service.bulkImport(items);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DocumentNumberingController {
|
||||
|
||||
@Patch('counters/:id')
|
||||
@ApiOperation({ summary: 'Update counter sequence value (Admin only)' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
@RequirePermission('numbering.manage_formats')
|
||||
async updateCounter(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body('sequence') sequence: number
|
||||
@@ -105,7 +105,7 @@ export class DocumentNumberingController {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return this.numberingService.previewNumber({
|
||||
const result = await this.numberingService.previewNumber({
|
||||
projectId: resolvedProjectId,
|
||||
originatorOrganizationId: resolvedOriginatorId,
|
||||
typeId: dto.correspondenceTypeId,
|
||||
@@ -116,5 +116,7 @@ export class DocumentNumberingController {
|
||||
year: dto.year,
|
||||
customTokens: dto.customTokens,
|
||||
});
|
||||
console.log('[DocumentNumberingController] Preview result:', JSON.stringify(result));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Project } from '../../project/entities/project.entity';
|
||||
import { CorrespondenceType } from '../../correspondence/entities/correspondence-type.entity';
|
||||
|
||||
@Entity('document_number_formats')
|
||||
@Unique(['projectId', 'correspondenceTypeId'])
|
||||
@Unique(['projectId', 'correspondenceTypeId', 'disciplineId'])
|
||||
export class DocumentNumberFormat {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
@@ -25,6 +25,9 @@ export class DocumentNumberFormat {
|
||||
@Column({ name: 'correspondence_type_id', nullable: true })
|
||||
correspondenceTypeId?: number;
|
||||
|
||||
@Column({ name: 'discipline_id', default: 0 })
|
||||
disciplineId!: number;
|
||||
|
||||
@Column({ name: 'format_string', length: 100 })
|
||||
formatTemplate!: string;
|
||||
|
||||
@@ -35,6 +38,9 @@ export class DocumentNumberFormat {
|
||||
@Column({ name: 'reset_annually', default: true })
|
||||
resetSequenceYearly!: boolean;
|
||||
|
||||
@Column({ name: 'is_active', default: 1 })
|
||||
isActive!: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
||||
import { Repository, EntityManager } from 'typeorm';
|
||||
import {
|
||||
Repository,
|
||||
EntityManager,
|
||||
In,
|
||||
IsNull,
|
||||
Equal,
|
||||
} from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { DocumentNumberFormat } from '../entities/document-number-format.entity';
|
||||
@@ -121,7 +127,7 @@ export class DocumentNumberingService {
|
||||
const sequence = await this.counterService.incrementCounter(key);
|
||||
|
||||
// 4. Format Number
|
||||
const documentNumber = await this.formatService.format({
|
||||
const { previewNumber: documentNumber } = await this.formatService.format({
|
||||
projectId: ctx.projectId,
|
||||
correspondenceTypeId: ctx.typeId,
|
||||
subTypeId: ctx.subTypeId,
|
||||
@@ -193,7 +199,7 @@ export class DocumentNumberingService {
|
||||
|
||||
async previewNumber(
|
||||
ctx: GenerateNumberContext
|
||||
): Promise<{ previewNumber: string; nextSequence: number }> {
|
||||
): Promise<{ previewNumber: string; nextSequence: number; isDefault: boolean }> {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const resetScope = `YEAR_${currentYear}`;
|
||||
|
||||
@@ -211,7 +217,7 @@ export class DocumentNumberingService {
|
||||
const currentSeq = await this.counterService.getCurrentCounter(key);
|
||||
const nextSequence = currentSeq + 1;
|
||||
|
||||
const previewNumber = await this.formatService.format({
|
||||
const { previewNumber, isDefault } = await this.formatService.format({
|
||||
projectId: ctx.projectId,
|
||||
correspondenceTypeId: ctx.typeId,
|
||||
subTypeId: ctx.subTypeId,
|
||||
@@ -224,7 +230,7 @@ export class DocumentNumberingService {
|
||||
recipientOrganizationId: ctx.recipientOrganizationId,
|
||||
});
|
||||
|
||||
return { previewNumber, nextSequence };
|
||||
return { previewNumber, nextSequence, isDefault };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,10 +264,36 @@ export class DocumentNumberingService {
|
||||
async saveTemplate(
|
||||
dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
|
||||
) {
|
||||
if (dto.projectId) {
|
||||
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
|
||||
try {
|
||||
this.logger.log(`Saving numbering template: ${JSON.stringify(dto)}`);
|
||||
|
||||
// Resolve project ID if it's a UUID/String
|
||||
if (dto.projectId && typeof dto.projectId === 'string') {
|
||||
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
|
||||
}
|
||||
|
||||
// Upsert logic: If no ID provided, check for existing template with same business key
|
||||
if (!dto.id) {
|
||||
const existing = await this.formatRepo.findOne({
|
||||
where: {
|
||||
projectId: Number(dto.projectId),
|
||||
correspondenceTypeId: dto.correspondenceTypeId ? Equal(dto.correspondenceTypeId) : IsNull(),
|
||||
disciplineId: dto.disciplineId || 0,
|
||||
},
|
||||
});
|
||||
if (existing) {
|
||||
this.logger.log(`Found existing template ID: ${existing.id} for business key, updating instead of creating.`);
|
||||
dto.id = existing.id;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.formatRepo.save(dto);
|
||||
this.logger.log(`Successfully saved template ID: ${result.id}`);
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
this.logger.error(`Failed to save numbering template: ${e.message}`, e.stack);
|
||||
throw e;
|
||||
}
|
||||
return this.formatRepo.save(dto);
|
||||
}
|
||||
|
||||
async deleteTemplate(id: number) {
|
||||
|
||||
@@ -39,12 +39,14 @@ export class FormatService {
|
||||
private disciplineRepo: Repository<Discipline>
|
||||
) {}
|
||||
|
||||
async format(options: FormatOptions): Promise<string> {
|
||||
const { template } = await this.resolveFormatAndScope(options);
|
||||
async format(options: FormatOptions): Promise<{ previewNumber: string; isDefault: boolean }> {
|
||||
const { template, isDefault } = await this.resolveFormatAndScope(options);
|
||||
const currentYear = options.year || new Date().getFullYear();
|
||||
const tokens = await this.resolveTokens(options, currentYear);
|
||||
|
||||
return this.replaceTokens(template, tokens, options.sequence);
|
||||
const previewNumber = this.replaceTokens(template, tokens, options.sequence);
|
||||
console.log(`[FormatService] Generated: "${previewNumber}" | Template: "${template}" | isDefault: ${isDefault}`);
|
||||
return { previewNumber, isDefault };
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
@@ -52,6 +54,7 @@ export class FormatService {
|
||||
private async resolveFormatAndScope(options: FormatOptions): Promise<{
|
||||
template: string;
|
||||
resetSequenceYearly: boolean;
|
||||
isDefault: boolean;
|
||||
}> {
|
||||
// 1. Specific Format
|
||||
const specificFormat = await this.formatRepo.findOne({
|
||||
@@ -64,6 +67,7 @@ export class FormatService {
|
||||
return {
|
||||
template: specificFormat.formatTemplate,
|
||||
resetSequenceYearly: specificFormat.resetSequenceYearly,
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
// 2. Default Format
|
||||
@@ -74,12 +78,14 @@ export class FormatService {
|
||||
return {
|
||||
template: defaultFormat.formatTemplate,
|
||||
resetSequenceYearly: defaultFormat.resetSequenceYearly,
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
// 3. Fallback
|
||||
return {
|
||||
template: '{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}',
|
||||
resetSequenceYearly: true,
|
||||
isDefault: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export class ReservationService {
|
||||
const sequence = await this.counterService.incrementCounter(counterKey);
|
||||
|
||||
// Format document number
|
||||
const documentNumber = await this.formatService.format({
|
||||
const { previewNumber: documentNumber } = await this.formatService.format({
|
||||
...dto,
|
||||
sequence,
|
||||
resetScope: counterKey.resetScope,
|
||||
|
||||
@@ -12,6 +12,15 @@ export class CreateTagDto {
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'red',
|
||||
description: 'รหัสสี หรือชื่อคลาสสำหรับ UI',
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color_code?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'Project ID or UUID',
|
||||
|
||||
@@ -86,13 +86,21 @@ export class CreateRfaRevisionDto {
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
details?: Record<string, any>;
|
||||
details?: Record<string, unknown>;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Linked Shop Drawing Revision IDs',
|
||||
example: [1, 2],
|
||||
description: 'Linked Shop Drawing Revision IDs or UUIDs',
|
||||
example: ['shop-revision-uuid-1', 'shop-revision-uuid-2'],
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
shopDrawingRevisionIds?: number[]; // IDs of linked Shop Drawings
|
||||
shopDrawingRevisionIds?: Array<number | string>;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Linked As-Built Drawing Revision IDs or UUIDs',
|
||||
example: ['asbuilt-revision-uuid-1'],
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
asBuiltDrawingRevisionIds?: Array<number | string>;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ export class CreateRfaDto {
|
||||
@IsOptional()
|
||||
contractId?: string;
|
||||
|
||||
@ApiProperty({ description: 'To Organization ID or UUID', required: false })
|
||||
@IsOptional()
|
||||
toOrganizationId?: number | string;
|
||||
@ApiProperty({ description: 'To Organization ID or UUID' })
|
||||
@IsNotEmpty()
|
||||
toOrganizationId!: number | string;
|
||||
|
||||
@ApiProperty({ description: 'ID ของประเภท RFA', example: 1 })
|
||||
@IsInt()
|
||||
@@ -76,14 +76,23 @@ export class CreateRfaDto {
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
details?: Record<string, any>;
|
||||
details?: Record<string, unknown>;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'รายการ Shop Drawing Revisions ที่แนบมาด้วย',
|
||||
description: 'รายการ Shop Drawing Revision IDs หรือ UUIDs ที่แนบมาด้วย',
|
||||
required: false,
|
||||
type: [Number],
|
||||
type: [String],
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
shopDrawingRevisionIds?: number[];
|
||||
shopDrawingRevisionIds?: Array<number | string>;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'รายการ As-Built Drawing Revision IDs หรือ UUIDs ที่แนบมาด้วย',
|
||||
required: false,
|
||||
type: [String],
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
asBuiltDrawingRevisionIds?: Array<number | string>;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { Entity, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { RfaRevision } from './rfa-revision.entity';
|
||||
import { AsBuiltDrawingRevision } from '../../drawing/entities/asbuilt-drawing-revision.entity';
|
||||
import { ShopDrawingRevision } from '../../drawing/entities/shop-drawing-revision.entity';
|
||||
|
||||
@Entity('rfa_items')
|
||||
export class RfaItem {
|
||||
@PrimaryColumn({ name: 'rfa_revision_id' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'rfa_revision_id' })
|
||||
rfaRevisionId!: number;
|
||||
|
||||
@PrimaryColumn({ name: 'shop_drawing_revision_id' })
|
||||
shopDrawingRevisionId!: number;
|
||||
@Column({
|
||||
name: 'item_type',
|
||||
type: 'enum',
|
||||
enum: ['SHOP', 'AS_BUILT'],
|
||||
})
|
||||
itemType!: 'SHOP' | 'AS_BUILT';
|
||||
|
||||
@Column({ name: 'shop_drawing_revision_id', nullable: true })
|
||||
shopDrawingRevisionId?: number;
|
||||
|
||||
@Column({ name: 'asbuilt_drawing_revision_id', nullable: true })
|
||||
asBuiltDrawingRevisionId?: number;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => RfaRevision, (rfaRev) => rfaRev.items, {
|
||||
@@ -19,5 +39,9 @@ export class RfaItem {
|
||||
|
||||
@ManyToOne(() => ShopDrawingRevision)
|
||||
@JoinColumn({ name: 'shop_drawing_revision_id' })
|
||||
shopDrawingRevision!: ShopDrawingRevision;
|
||||
shopDrawingRevision?: ShopDrawingRevision;
|
||||
|
||||
@ManyToOne(() => AsBuiltDrawingRevision)
|
||||
@JoinColumn({ name: 'asbuilt_drawing_revision_id' })
|
||||
asBuiltDrawingRevision?: AsBuiltDrawingRevision;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ import { CorrespondenceRouting } from '../correspondence/entities/correspondence
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
|
||||
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
|
||||
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
|
||||
import { AsBuiltDrawingRevision } from '../drawing/entities/asbuilt-drawing-revision.entity';
|
||||
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { RfaApproveCode } from './entities/rfa-approve-code.entity';
|
||||
import { RfaItem } from './entities/rfa-item.entity';
|
||||
import { RfaRevision } from './entities/rfa-revision.entity';
|
||||
@@ -46,7 +49,10 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
|
||||
Correspondence,
|
||||
CorrespondenceRevision,
|
||||
CorrespondenceStatus,
|
||||
CorrespondenceType,
|
||||
AsBuiltDrawingRevision,
|
||||
ShopDrawingRevision,
|
||||
Discipline,
|
||||
RfaWorkflow,
|
||||
RfaWorkflowTemplate,
|
||||
RfaWorkflowTemplateStep,
|
||||
|
||||
@@ -13,12 +13,16 @@ import { DataSource, In, Repository } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity';
|
||||
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
|
||||
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
|
||||
import { AsBuiltDrawingRevision } from '../drawing/entities/asbuilt-drawing-revision.entity';
|
||||
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { RfaApproveCode } from './entities/rfa-approve-code.entity';
|
||||
import { RfaItem } from './entities/rfa-item.entity';
|
||||
@@ -72,10 +76,16 @@ export class RfaService {
|
||||
private corrRevRepo: Repository<CorrespondenceRevision>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private corrStatusRepo: Repository<CorrespondenceStatus>,
|
||||
@InjectRepository(CorrespondenceType)
|
||||
private correspondenceTypeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(Discipline)
|
||||
private disciplineRepo: Repository<Discipline>,
|
||||
@InjectRepository(RfaStatusCode)
|
||||
private rfaStatusRepo: Repository<RfaStatusCode>,
|
||||
@InjectRepository(RfaApproveCode)
|
||||
private rfaApproveRepo: Repository<RfaApproveCode>,
|
||||
@InjectRepository(AsBuiltDrawingRevision)
|
||||
private asBuiltDrawingRevRepo: Repository<AsBuiltDrawingRevision>,
|
||||
@InjectRepository(ShopDrawingRevision)
|
||||
private shopDrawingRevRepo: Repository<ShopDrawingRevision>,
|
||||
@InjectRepository(CorrespondenceRouting)
|
||||
@@ -105,6 +115,104 @@ export class RfaService {
|
||||
});
|
||||
if (!rfaType) throw new NotFoundException('RFA Type not found');
|
||||
|
||||
const rfaTypeCode = rfaType.typeCode.toUpperCase();
|
||||
const rawShopDrawingRefs = createDto.shopDrawingRevisionIds ?? [];
|
||||
const rawAsBuiltDrawingRefs = createDto.asBuiltDrawingRevisionIds ?? [];
|
||||
|
||||
if (['DDW', 'SDW'].includes(rfaTypeCode)) {
|
||||
if (rawShopDrawingRefs.length === 0) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type requires at least one Shop Drawing Revision'
|
||||
);
|
||||
}
|
||||
|
||||
if (rawAsBuiltDrawingRefs.length > 0) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type cannot reference As-Built Drawing Revisions'
|
||||
);
|
||||
}
|
||||
} else if (rfaTypeCode === 'ADW') {
|
||||
if (rawAsBuiltDrawingRefs.length === 0) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type requires at least one As-Built Drawing Revision'
|
||||
);
|
||||
}
|
||||
|
||||
if (rawShopDrawingRefs.length > 0) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type cannot reference Shop Drawing Revisions'
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
rawShopDrawingRefs.length > 0 ||
|
||||
rawAsBuiltDrawingRefs.length > 0
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type does not support drawing revision items'
|
||||
);
|
||||
}
|
||||
|
||||
const shopDrawingRevisionIds = Array.from(
|
||||
new Set(
|
||||
await Promise.all(
|
||||
rawShopDrawingRefs.map((ref) =>
|
||||
this.uuidResolver.resolveShopDrawingRevisionId(ref)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const asBuiltDrawingRevisionIds = Array.from(
|
||||
new Set(
|
||||
await Promise.all(
|
||||
rawAsBuiltDrawingRefs.map((ref) =>
|
||||
this.uuidResolver.resolveAsBuiltDrawingRevisionId(ref)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const correspondenceType = await this.correspondenceTypeRepo.findOne({
|
||||
where: { typeCode: 'RFA', isActive: true },
|
||||
});
|
||||
if (!correspondenceType) {
|
||||
throw new InternalServerErrorException(
|
||||
'Correspondence Type RFA not found in Master Data'
|
||||
);
|
||||
}
|
||||
|
||||
const internalContractId = createDto.contractId
|
||||
? await this.uuidResolver.resolveContractId(createDto.contractId)
|
||||
: rfaType.contractId;
|
||||
|
||||
if (rfaType.contractId !== internalContractId) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type does not belong to the selected contract'
|
||||
);
|
||||
}
|
||||
|
||||
if (createDto.disciplineId) {
|
||||
const discipline = await this.disciplineRepo.findOne({
|
||||
where: { id: createDto.disciplineId },
|
||||
});
|
||||
|
||||
if (!discipline) {
|
||||
throw new NotFoundException('Discipline not found');
|
||||
}
|
||||
|
||||
if (discipline.contractId !== internalContractId) {
|
||||
throw new BadRequestException(
|
||||
'Selected Discipline does not belong to the selected contract'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const internalRecipientOrgId = createDto.toOrganizationId
|
||||
? await this.uuidResolver.resolveOrganizationId(
|
||||
createDto.toOrganizationId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const statusDraft = await this.rfaStatusRepo.findOne({
|
||||
where: { statusCode: 'DFT' },
|
||||
});
|
||||
@@ -135,7 +243,9 @@ export class RfaService {
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: internalProjectId,
|
||||
originatorOrganizationId: userOrgId,
|
||||
typeId: createDto.rfaTypeId,
|
||||
recipientOrganizationId: internalRecipientOrgId,
|
||||
typeId: correspondenceType.id,
|
||||
rfaTypeId: createDto.rfaTypeId,
|
||||
disciplineId: createDto.disciplineId ?? 0, // ✅ ส่ง disciplineId ไปด้วย (0 ถ้าไม่มี)
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
@@ -159,7 +269,7 @@ export class RfaService {
|
||||
// 1. Create Correspondence Record
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber.number,
|
||||
correspondenceTypeId: createDto.rfaTypeId,
|
||||
correspondenceTypeId: correspondenceType.id,
|
||||
projectId: internalProjectId,
|
||||
originatorId: userOrgId,
|
||||
isInternal: false,
|
||||
@@ -168,6 +278,15 @@ export class RfaService {
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
if (internalRecipientOrgId) {
|
||||
const recipient = queryRunner.manager.create(CorrespondenceRecipient, {
|
||||
correspondenceId: savedCorr.id,
|
||||
recipientOrganizationId: internalRecipientOrgId,
|
||||
recipientType: 'TO',
|
||||
});
|
||||
await queryRunner.manager.save(recipient);
|
||||
}
|
||||
|
||||
// 2. Create Rfa Master Record
|
||||
const rfa = queryRunner.manager.create(Rfa, {
|
||||
id: savedCorr.id, // ✅ CTI Key share
|
||||
@@ -205,25 +324,51 @@ export class RfaService {
|
||||
});
|
||||
const savedRevision = await queryRunner.manager.save(rfaRevision);
|
||||
|
||||
// 4. Link Shop Drawings
|
||||
if (
|
||||
createDto.shopDrawingRevisionIds &&
|
||||
createDto.shopDrawingRevisionIds.length > 0
|
||||
) {
|
||||
const rfaItems: RfaItem[] = [];
|
||||
|
||||
if (shopDrawingRevisionIds.length > 0) {
|
||||
const shopDrawings = await this.shopDrawingRevRepo.findBy({
|
||||
id: In(createDto.shopDrawingRevisionIds),
|
||||
id: In(shopDrawingRevisionIds),
|
||||
});
|
||||
|
||||
if (shopDrawings.length !== createDto.shopDrawingRevisionIds.length) {
|
||||
if (shopDrawings.length !== shopDrawingRevisionIds.length) {
|
||||
throw new NotFoundException('Some Shop Drawing Revisions not found');
|
||||
}
|
||||
|
||||
const rfaItems = shopDrawings.map((sd) =>
|
||||
queryRunner.manager.create(RfaItem, {
|
||||
rfaRevisionId: savedRevision.id, // Correctly link to RfaRevision
|
||||
shopDrawingRevisionId: sd.id,
|
||||
})
|
||||
rfaItems.push(
|
||||
...shopDrawings.map((sd) =>
|
||||
queryRunner.manager.create(RfaItem, {
|
||||
rfaRevisionId: savedRevision.id,
|
||||
itemType: 'SHOP',
|
||||
shopDrawingRevisionId: sd.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (asBuiltDrawingRevisionIds.length > 0) {
|
||||
const asBuiltDrawings = await this.asBuiltDrawingRevRepo.findBy({
|
||||
id: In(asBuiltDrawingRevisionIds),
|
||||
});
|
||||
|
||||
if (asBuiltDrawings.length !== asBuiltDrawingRevisionIds.length) {
|
||||
throw new NotFoundException(
|
||||
'Some As-Built Drawing Revisions not found'
|
||||
);
|
||||
}
|
||||
|
||||
rfaItems.push(
|
||||
...asBuiltDrawings.map((ad) =>
|
||||
queryRunner.manager.create(RfaItem, {
|
||||
rfaRevisionId: savedRevision.id,
|
||||
itemType: 'AS_BUILT',
|
||||
asBuiltDrawingRevisionId: ad.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (rfaItems.length > 0) {
|
||||
await queryRunner.manager.save(rfaItems);
|
||||
}
|
||||
|
||||
@@ -303,7 +448,11 @@ export class RfaService {
|
||||
.leftJoinAndSelect('rfaRev.statusCode', 'status')
|
||||
.leftJoinAndSelect('rfaRev.items', 'items')
|
||||
.leftJoinAndSelect('items.shopDrawingRevision', 'sdRev')
|
||||
.leftJoinAndSelect('sdRev.attachments', 'attachments');
|
||||
.leftJoinAndSelect('sdRev.shopDrawing', 'shopDrawing')
|
||||
.leftJoinAndSelect('sdRev.attachments', 'shopAttachments')
|
||||
.leftJoinAndSelect('items.asBuiltDrawingRevision', 'adRev')
|
||||
.leftJoinAndSelect('adRev.asBuiltDrawing', 'asBuiltDrawing')
|
||||
.leftJoinAndSelect('adRev.attachments', 'asBuiltAttachments');
|
||||
|
||||
// Filter by Revision Status (from query param 'revisionStatus')
|
||||
if (revisionStatus === 'CURRENT') {
|
||||
@@ -405,6 +554,10 @@ export class RfaService {
|
||||
'correspondence.revisions.rfaRevision.items',
|
||||
'correspondence.revisions.rfaRevision.items.shopDrawingRevision',
|
||||
'correspondence.revisions.rfaRevision.items.shopDrawingRevision.shopDrawing',
|
||||
'correspondence.revisions.rfaRevision.items.shopDrawingRevision.attachments',
|
||||
'correspondence.revisions.rfaRevision.items.asBuiltDrawingRevision',
|
||||
'correspondence.revisions.rfaRevision.items.asBuiltDrawingRevision.asBuiltDrawing',
|
||||
'correspondence.revisions.rfaRevision.items.asBuiltDrawingRevision.attachments',
|
||||
],
|
||||
order: {
|
||||
correspondence: { revisions: { revisionNumber: 'DESC' } },
|
||||
|
||||
Reference in New Issue
Block a user