This commit is contained in:
@@ -1,133 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const filepath = 'd:/nap-dms.lcbp3/specs/03-Data-and-Storage/n8n.workflow.json';
|
|
||||||
const workflow = JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
|
||||||
|
|
||||||
const switchNodeIndex = workflow.nodes.findIndex(n => n.name === 'Route by Confidence');
|
|
||||||
|
|
||||||
if (switchNodeIndex > -1) {
|
|
||||||
workflow.nodes[switchNodeIndex] = {
|
|
||||||
id: "23d11b5e-49b4-4b53-911b-76b6bb77aab8",
|
|
||||||
name: "Route by Confidence",
|
|
||||||
type: "n8n-nodes-base.switch",
|
|
||||||
typeVersion: 3.2,
|
|
||||||
position: [6840, 3696],
|
|
||||||
parameters: {
|
|
||||||
rules: {
|
|
||||||
values: [
|
|
||||||
{
|
|
||||||
conditions: {
|
|
||||||
options: {
|
|
||||||
caseSensitive: true,
|
|
||||||
leftValue: "",
|
|
||||||
typeValidation: "strict",
|
|
||||||
version: 2
|
|
||||||
},
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
leftValue: "={{ $json.route_index }}",
|
|
||||||
rightValue: 0,
|
|
||||||
operator: {
|
|
||||||
type: "number",
|
|
||||||
operation: "equals",
|
|
||||||
singleValue: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
combinator: "and"
|
|
||||||
},
|
|
||||||
renameOutput: true,
|
|
||||||
outputKey: "Auto Ingest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
conditions: {
|
|
||||||
options: {
|
|
||||||
caseSensitive: true,
|
|
||||||
leftValue: "",
|
|
||||||
typeValidation: "strict",
|
|
||||||
version: 2
|
|
||||||
},
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
leftValue: "={{ $json.route_index }}",
|
|
||||||
rightValue: 1,
|
|
||||||
operator: {
|
|
||||||
type: "number",
|
|
||||||
operation: "equals",
|
|
||||||
singleValue: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
combinator: "and"
|
|
||||||
},
|
|
||||||
renameOutput: true,
|
|
||||||
outputKey: "Review Queue"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
conditions: {
|
|
||||||
options: {
|
|
||||||
caseSensitive: true,
|
|
||||||
leftValue: "",
|
|
||||||
typeValidation: "strict",
|
|
||||||
version: 2
|
|
||||||
},
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
leftValue: "={{ $json.route_index }}",
|
|
||||||
rightValue: 2,
|
|
||||||
operator: {
|
|
||||||
type: "number",
|
|
||||||
operation: "equals",
|
|
||||||
singleValue: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
combinator: "and"
|
|
||||||
},
|
|
||||||
renameOutput: true,
|
|
||||||
outputKey: "Reject"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
conditions: {
|
|
||||||
options: {
|
|
||||||
caseSensitive: true,
|
|
||||||
leftValue: "",
|
|
||||||
typeValidation: "strict",
|
|
||||||
version: 2
|
|
||||||
},
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
leftValue: "={{ $json.route_index }}",
|
|
||||||
rightValue: 3,
|
|
||||||
operator: {
|
|
||||||
type: "number",
|
|
||||||
operation: "equals",
|
|
||||||
singleValue: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
combinator: "and"
|
|
||||||
},
|
|
||||||
renameOutput: true,
|
|
||||||
outputKey: "Error Log"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (workflow.connections['Confidence Router'] || workflow.connections['Route by Confidence']) {
|
|
||||||
workflow.connections['Route by Confidence'] = {
|
|
||||||
'main': [
|
|
||||||
[ { 'node': 'Import to Backend', 'type': 'main', 'index': 0 } ],
|
|
||||||
[ { 'node': 'Insert Review Queue', 'type': 'main', 'index': 0 } ],
|
|
||||||
[ { 'node': 'Log Reject to CSV', 'type': 'main', 'index': 0 } ],
|
|
||||||
[ { 'node': 'Log Error to CSV', 'type': 'main', 'index': 0 } ]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(filepath, JSON.stringify(workflow, null, 2));
|
|
||||||
console.log("Updated Switch node to use typeVersion 3.2 properly.");
|
|
||||||
} else {
|
|
||||||
console.log("Could not find Route by Confidence node.");
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "1.5.1",
|
"version": "1.8.0",
|
||||||
"description": "<p align=\"center\">\r <a href=\"http://nestjs.com/\" target=\"blank\"><img src=\"https://nestjs.com/img/logo-small.svg\" width=\"120\" alt=\"Nest Logo\" /></a>\r </p>",
|
"description": "<p align=\"center\">\r <a href=\"http://nestjs.com/\" target=\"blank\"><img src=\"https://nestjs.com/img/logo-small.svg\" width=\"120\" alt=\"Nest Logo\" /></a>\r </p>",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ describe('CorrespondenceController', () => {
|
|||||||
|
|
||||||
const mockReq = { user: { user_id: 1 } };
|
const mockReq = { user: { user_id: 1 } };
|
||||||
const result = await controller.submit(
|
const result = await controller.submit(
|
||||||
1,
|
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
||||||
{ note: 'Test note' },
|
{ note: 'Test note' },
|
||||||
mockReq as Parameters<typeof controller.submit>[2]
|
mockReq as Parameters<typeof controller.submit>[2]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
Param,
|
Param,
|
||||||
ParseIntPipe,
|
|
||||||
Query,
|
Query,
|
||||||
Delete,
|
Delete,
|
||||||
Put,
|
Put,
|
||||||
@@ -43,7 +42,7 @@ export class CorrespondenceController {
|
|||||||
private readonly workflowService: CorrespondenceWorkflowService
|
private readonly workflowService: CorrespondenceWorkflowService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post(':id/workflow/action')
|
@Post(':uuid/workflow/action')
|
||||||
@ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' })
|
@ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' })
|
||||||
@ApiResponse({ status: 201, description: 'Action processed successfully.' })
|
@ApiResponse({ status: 201, description: 'Action processed successfully.' })
|
||||||
@RequirePermission('workflow.action_review')
|
@RequirePermission('workflow.action_review')
|
||||||
@@ -188,15 +187,16 @@ export class CorrespondenceController {
|
|||||||
return this.correspondenceService.addReference(corr.id, dto);
|
return this.correspondenceService.addReference(corr.id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':uuid/references/:targetId')
|
@Delete(':uuid/references/:targetUuid')
|
||||||
@ApiOperation({ summary: 'Remove reference' })
|
@ApiOperation({ summary: 'Remove reference' })
|
||||||
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
||||||
@RequirePermission('document.edit')
|
@RequirePermission('document.edit')
|
||||||
async removeReference(
|
async removeReference(
|
||||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Param('targetId', ParseIntPipe) targetId: number
|
@Param('targetUuid', ParseUuidPipe) targetUuid: string
|
||||||
) {
|
) {
|
||||||
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||||
return this.correspondenceService.removeReference(corr.id, targetId);
|
const target = await this.correspondenceService.findOneByUuid(targetUuid);
|
||||||
|
return this.correspondenceService.removeReference(corr.id, target.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,8 +423,9 @@ export class CorrespondenceService {
|
|||||||
|
|
||||||
async addReference(id: number, dto: AddReferenceDto) {
|
async addReference(id: number, dto: AddReferenceDto) {
|
||||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||||
|
// ADR-019: Resolve target UUID → internal INT id
|
||||||
const target = await this.correspondenceRepo.findOne({
|
const target = await this.correspondenceRepo.findOne({
|
||||||
where: { id: dto.targetId },
|
where: { uuid: dto.targetUuid },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!source || !target) {
|
if (!source || !target) {
|
||||||
@@ -438,7 +439,7 @@ export class CorrespondenceService {
|
|||||||
const exists = await this.referenceRepo.findOne({
|
const exists = await this.referenceRepo.findOne({
|
||||||
where: {
|
where: {
|
||||||
sourceId: id,
|
sourceId: id,
|
||||||
targetId: dto.targetId,
|
targetId: target.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -448,7 +449,7 @@ export class CorrespondenceService {
|
|||||||
|
|
||||||
const ref = this.referenceRepo.create({
|
const ref = this.referenceRepo.create({
|
||||||
sourceId: id,
|
sourceId: id,
|
||||||
targetId: dto.targetId,
|
targetId: target.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.referenceRepo.save(ref);
|
return this.referenceRepo.save(ref);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
import { IsUUID, IsNotEmpty } from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class AddReferenceDto {
|
export class AddReferenceDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Target Correspondence ID to reference',
|
description: 'Target Correspondence UUID to reference (ADR-019)',
|
||||||
example: 20,
|
example: '019505a1-7c3e-7000-8000-abc123def456',
|
||||||
})
|
})
|
||||||
@IsInt()
|
@IsUUID('all')
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
targetId!: number;
|
targetUuid!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class CreateCorrespondenceDto {
|
|||||||
})
|
})
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
details?: Record<string, any>; // ข้อมูล JSON (เช่น RFI question)
|
details?: Record<string, unknown>; // ข้อมูล JSON (เช่น RFI question)
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Is internal document?', default: false })
|
@ApiPropertyOptional({ description: 'Is internal document?', default: false })
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class CorrespondenceRevision extends UuidBaseEntity {
|
|||||||
remarks?: string;
|
remarks?: string;
|
||||||
|
|
||||||
@Column({ type: 'json', nullable: true })
|
@Column({ type: 'json', nullable: true })
|
||||||
details?: any; // เก็บข้อมูลแบบ Dynamic ตาม Type
|
details?: object; // Dynamic JSON — typed as `object` per TypeORM JSON column convention (no-any, ADR-019)
|
||||||
|
|
||||||
@Column({ name: 'schema_version', default: 1 })
|
@Column({ name: 'schema_version', default: 1 })
|
||||||
schemaVersion!: number;
|
schemaVersion!: number;
|
||||||
|
|||||||
@@ -267,12 +267,17 @@ export class DocumentNumberingService {
|
|||||||
// --- Admin / Legacy ---
|
// --- Admin / Legacy ---
|
||||||
|
|
||||||
async getTemplates() {
|
async getTemplates() {
|
||||||
return this.formatRepo.find();
|
return this.formatRepo.find({
|
||||||
|
relations: ['project', 'correspondenceType'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTemplatesByProject(projectId: number | string) {
|
async getTemplatesByProject(projectId: number | string) {
|
||||||
const internalId = await this.resolveProjectId(projectId);
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
return this.formatRepo.find({ where: { projectId: internalId } });
|
return this.formatRepo.find({
|
||||||
|
where: { projectId: internalId },
|
||||||
|
relations: ['project', 'correspondenceType'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTemplate(dto: any) {
|
async saveTemplate(dto: any) {
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ describe('MigrationController', () => {
|
|||||||
it('should call importCorrespondence on service', async () => {
|
it('should call importCorrespondence on service', async () => {
|
||||||
const dto: ImportCorrespondenceDto = {
|
const dto: ImportCorrespondenceDto = {
|
||||||
document_number: 'DOC-001',
|
document_number: 'DOC-001',
|
||||||
title: 'Legacy Record',
|
subject: 'Legacy Record',
|
||||||
category: 'Correspondence',
|
category: 'Correspondence',
|
||||||
source_file_path: '/staging_ai/test.pdf',
|
source_file_path: '/staging_ai/test.pdf',
|
||||||
migrated_by: 'SYSTEM_IMPORT',
|
migrated_by: 'SYSTEM_IMPORT',
|
||||||
batch_id: 'batch1',
|
batch_id: 'batch1',
|
||||||
|
project_id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const idempotencyKey = 'key123';
|
const idempotencyKey = 'key123';
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { IsInt, IsOptional, IsString, IsNotEmpty } from 'class-validator';
|
import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class SearchRfaDto {
|
export class SearchRfaDto {
|
||||||
@IsInt()
|
@IsUUID('all')
|
||||||
@Type(() => Number)
|
projectUuid!: string; // ADR-019: Public UUID of the project
|
||||||
@IsNotEmpty()
|
|
||||||
projectId!: number; // บังคับระบุ Project
|
/** @internal Resolved INT ID — set by controller, do NOT expose in API */
|
||||||
|
projectId?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@@ -13,9 +14,12 @@ export class SearchRfaDto {
|
|||||||
rfaTypeId?: number; // กรองตามประเภท RFA
|
rfaTypeId?: number; // กรองตามประเภท RFA
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsString()
|
||||||
@Type(() => Number)
|
statusCode?: string; // กรองตามสถานะโดยใช้ status code เช่น 'DFT', 'FAP'
|
||||||
statusId?: number; // กรองตามสถานะ (เช่น Draft, For Approve)
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
revisionStatus?: string; // 'CURRENT' | 'OLD' | 'ALL' — default 'CURRENT'
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class RfaRevision {
|
|||||||
// --- JSON & Schema Section ---
|
// --- JSON & Schema Section ---
|
||||||
|
|
||||||
@Column({ type: 'json', nullable: true })
|
@Column({ type: 'json', nullable: true })
|
||||||
details?: any;
|
details?: object; // Dynamic JSON — typed as `object` per TypeORM JSON column convention (no-any, ADR-019)
|
||||||
|
|
||||||
// ✅ [New] จำเป็นสำหรับ Data Migration (T2.5.5)
|
// ✅ [New] จำเป็นสำหรับ Data Migration (T2.5.5)
|
||||||
@Column({ name: 'schema_version', default: 1 })
|
@Column({ name: 'schema_version', default: 1 })
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Param,
|
Param,
|
||||||
ParseIntPipe,
|
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -22,6 +21,7 @@ import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
|||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
import { CreateRfaDto } from './dto/create-rfa.dto';
|
||||||
import { SubmitRfaDto } from './dto/submit-rfa.dto';
|
import { SubmitRfaDto } from './dto/submit-rfa.dto';
|
||||||
|
import { SearchRfaDto } from './dto/search-rfa.dto';
|
||||||
import { RfaService } from './rfa.service';
|
import { RfaService } from './rfa.service';
|
||||||
|
|
||||||
import { Audit } from '../../common/decorators/audit.decorator';
|
import { Audit } from '../../common/decorators/audit.decorator';
|
||||||
@@ -29,13 +29,18 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
import { ProjectService } from '../project/project.service';
|
||||||
|
|
||||||
@ApiTags('RFA (Request for Approval)')
|
@ApiTags('RFA (Request for Approval)')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
@Controller('rfas')
|
@Controller('rfas')
|
||||||
export class RfaController {
|
export class RfaController {
|
||||||
constructor(private readonly rfaService: RfaService) {}
|
constructor(
|
||||||
|
private readonly rfaService: RfaService,
|
||||||
|
private readonly projectService: ProjectService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create new RFA (Draft)' })
|
@ApiOperation({ summary: 'Create new RFA (Draft)' })
|
||||||
@@ -47,24 +52,26 @@ export class RfaController {
|
|||||||
return this.rfaService.create(createDto, user);
|
return this.rfaService.create(createDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/submit')
|
@Post(':uuid/submit')
|
||||||
@ApiOperation({ summary: 'Submit RFA to Workflow' })
|
@ApiOperation({ summary: 'Submit RFA to Workflow' })
|
||||||
@ApiParam({ name: 'id', description: 'RFA ID' })
|
@ApiParam({ name: 'uuid', description: 'RFA UUID (from correspondences.uuid)' })
|
||||||
@ApiBody({ type: SubmitRfaDto })
|
@ApiBody({ type: SubmitRfaDto })
|
||||||
@ApiResponse({ status: 200, description: 'RFA submitted successfully' })
|
@ApiResponse({ status: 200, description: 'RFA submitted successfully' })
|
||||||
@RequirePermission('rfa.create')
|
@RequirePermission('rfa.create')
|
||||||
@Audit('rfa.submit', 'rfa')
|
@Audit('rfa.submit', 'rfa')
|
||||||
submit(
|
async submit(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() submitDto: SubmitRfaDto,
|
@Body() submitDto: SubmitRfaDto,
|
||||||
@CurrentUser() user: User
|
@CurrentUser() user: User
|
||||||
) {
|
) {
|
||||||
return this.rfaService.submit(id, submitDto.templateId, user);
|
// ADR-019: resolve UUID → internal INT id via findOneByUuidRaw
|
||||||
|
const rfa = await this.rfaService.findOneByUuidRaw(uuid);
|
||||||
|
return this.rfaService.submit(rfa.id, submitDto.templateId, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/action')
|
@Post(':uuid/action')
|
||||||
@ApiOperation({ summary: 'Process Workflow Action (Approve/Reject)' })
|
@ApiOperation({ summary: 'Process Workflow Action (Approve/Reject)' })
|
||||||
@ApiParam({ name: 'id', description: 'RFA ID' })
|
@ApiParam({ name: 'uuid', description: 'RFA UUID (from correspondences.uuid)' })
|
||||||
@ApiBody({ type: WorkflowActionDto })
|
@ApiBody({ type: WorkflowActionDto })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -72,28 +79,33 @@ export class RfaController {
|
|||||||
})
|
})
|
||||||
@RequirePermission('workflow.action_review')
|
@RequirePermission('workflow.action_review')
|
||||||
@Audit('rfa.action', 'rfa')
|
@Audit('rfa.action', 'rfa')
|
||||||
processAction(
|
async processAction(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() actionDto: WorkflowActionDto,
|
@Body() actionDto: WorkflowActionDto,
|
||||||
@CurrentUser() user: User
|
@CurrentUser() user: User
|
||||||
) {
|
) {
|
||||||
return this.rfaService.processAction(id, actionDto, user);
|
// ADR-019: resolve UUID → internal INT id
|
||||||
|
const rfa = await this.rfaService.findOneByUuidRaw(uuid);
|
||||||
|
return this.rfaService.processAction(rfa.id, actionDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'List all RFAs with pagination' })
|
@ApiOperation({ summary: 'List all RFAs with pagination' })
|
||||||
@ApiResponse({ status: 200, description: 'List of RFAs' })
|
@ApiResponse({ status: 200, description: 'List of RFAs' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findAll(@Query() query: any) {
|
async findAll(@Query() query: SearchRfaDto) {
|
||||||
|
// ADR-019: resolve projectUuid → internal INT projectId
|
||||||
|
const project = await this.projectService.findOneByUuid(query.projectUuid);
|
||||||
|
query.projectId = project.id;
|
||||||
return this.rfaService.findAll(query);
|
return this.rfaService.findAll(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get RFA details with revisions and items' })
|
@ApiOperation({ summary: 'Get RFA details with revisions and items' })
|
||||||
@ApiParam({ name: 'id', description: 'RFA ID' })
|
@ApiParam({ name: 'uuid', description: 'RFA UUID (from correspondences.uuid)' })
|
||||||
@ApiResponse({ status: 200, description: 'RFA details' })
|
@ApiResponse({ status: 200, description: 'RFA details' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.rfaService.findOne(id);
|
return this.rfaService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { RfaService } from './rfa.service';
|
|||||||
// External Modules
|
// External Modules
|
||||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||||
import { NotificationModule } from '../notification/notification.module';
|
import { NotificationModule } from '../notification/notification.module';
|
||||||
|
import { ProjectModule } from '../project/project.module';
|
||||||
import { SearchModule } from '../search/search.module';
|
import { SearchModule } from '../search/search.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
|
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
|
||||||
@@ -56,6 +57,7 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
|
|||||||
]),
|
]),
|
||||||
DocumentNumberingModule,
|
DocumentNumberingModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
ProjectModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
WorkflowEngineModule,
|
WorkflowEngineModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ import { Rfa } from './entities/rfa.entity';
|
|||||||
// DTOs
|
// DTOs
|
||||||
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
||||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
import { CreateRfaDto } from './dto/create-rfa.dto';
|
||||||
|
import { SearchRfaDto } from './dto/search-rfa.dto';
|
||||||
|
|
||||||
|
// ------- Local type helpers (no-any ADR-019) -------
|
||||||
|
/** CorrespondenceRevision with the rfaRevision relation loaded at runtime */
|
||||||
|
type CorrRevWithRfa = CorrespondenceRevision & { rfaRevision?: RfaRevision };
|
||||||
|
|
||||||
|
/** RFA entity + a flat `revisions` convenience array for the frontend */
|
||||||
|
export interface RfaMapped extends Rfa {
|
||||||
|
uuid?: string; // ADR-019: top-level UUID from correspondence
|
||||||
|
revisions: CorrRevWithRfa[];
|
||||||
|
}
|
||||||
|
|
||||||
// Interfaces & Enums
|
// Interfaces & Enums
|
||||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||||
@@ -272,6 +283,7 @@ export class RfaService {
|
|||||||
this.searchService
|
this.searchService
|
||||||
.indexDocument({
|
.indexDocument({
|
||||||
id: savedCorr.id,
|
id: savedCorr.id,
|
||||||
|
uuid: savedCorr.uuid, // ADR-019: index UUID for search
|
||||||
type: 'rfa',
|
type: 'rfa',
|
||||||
docNumber: docNumber.number,
|
docNumber: docNumber.number,
|
||||||
title: createDto.subject,
|
title: createDto.subject,
|
||||||
@@ -298,13 +310,19 @@ export class RfaService {
|
|||||||
|
|
||||||
// ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ...
|
// ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ...
|
||||||
|
|
||||||
async findAll(query: any) {
|
async findAll(query: SearchRfaDto) {
|
||||||
const { page = 1, limit = 20, projectId, status, search } = query;
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 20,
|
||||||
|
projectId,
|
||||||
|
search,
|
||||||
|
revisionStatus = 'CURRENT',
|
||||||
|
statusCode,
|
||||||
|
} = query;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
// Fix: Start query from Rfa entity instead of Correspondence,
|
// Fix: Start query from Rfa entity instead of Correspondence,
|
||||||
// because Correspondence has no 'rfas' relation.
|
// because Correspondence has no 'rfas' relation.
|
||||||
// [Force Rebuild]
|
|
||||||
const queryBuilder = this.rfaRepo
|
const queryBuilder = this.rfaRepo
|
||||||
.createQueryBuilder('rfa')
|
.createQueryBuilder('rfa')
|
||||||
.leftJoinAndSelect('rfa.correspondence', 'corr')
|
.leftJoinAndSelect('rfa.correspondence', 'corr')
|
||||||
@@ -318,11 +336,9 @@ export class RfaService {
|
|||||||
.leftJoinAndSelect('sdRev.attachments', 'attachments');
|
.leftJoinAndSelect('sdRev.attachments', 'attachments');
|
||||||
|
|
||||||
// Filter by Revision Status (from query param 'revisionStatus')
|
// Filter by Revision Status (from query param 'revisionStatus')
|
||||||
const revStatus = query.revisionStatus || 'CURRENT';
|
if (revisionStatus === 'CURRENT') {
|
||||||
|
|
||||||
if (revStatus === 'CURRENT') {
|
|
||||||
queryBuilder.where('corrRev.isCurrent = :isCurrent', { isCurrent: true });
|
queryBuilder.where('corrRev.isCurrent = :isCurrent', { isCurrent: true });
|
||||||
} else if (revStatus === 'OLD') {
|
} else if (revisionStatus === 'OLD') {
|
||||||
queryBuilder.where('corrRev.isCurrent = :isCurrent', {
|
queryBuilder.where('corrRev.isCurrent = :isCurrent', {
|
||||||
isCurrent: false,
|
isCurrent: false,
|
||||||
});
|
});
|
||||||
@@ -333,8 +349,8 @@ export class RfaService {
|
|||||||
queryBuilder.andWhere('corr.projectId = :projectId', { projectId });
|
queryBuilder.andWhere('corr.projectId = :projectId', { projectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (statusCode) {
|
||||||
queryBuilder.andWhere('status.statusCode = :status', { status });
|
queryBuilder.andWhere('status.statusCode = :statusCode', { statusCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
@@ -355,15 +371,18 @@ export class RfaService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Map `revisions` property back to the expected payload for the frontend
|
// Map `revisions` property back to the expected payload for the frontend
|
||||||
const mappedItems = items.map((rfa) => {
|
const mappedItems: RfaMapped[] = items.map((rfa) => {
|
||||||
const mappedRfa = { ...rfa } as any;
|
const revisions =
|
||||||
mappedRfa.revisions =
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
rfa.correspondence?.revisions?.map((cr) => ({
|
return {
|
||||||
|
...rfa,
|
||||||
|
uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level
|
||||||
|
revisions: revisions.map((cr) => ({
|
||||||
...cr,
|
...cr,
|
||||||
...(cr.rfaRevision || {}),
|
...(cr.rfaRevision ?? {}),
|
||||||
id: cr.rfaRevision?.id || cr.id,
|
id: cr.rfaRevision?.id ?? cr.id,
|
||||||
})) || [];
|
})) as CorrRevWithRfa[],
|
||||||
return mappedRfa;
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -377,6 +396,32 @@ export class RfaService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADR-019: Find RFA by the parent Correspondence UUID (public identifier).
|
||||||
|
* Resolves correspondence.uuid → internal rfa.id
|
||||||
|
*/
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const correspondence = await this.correspondenceRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
if (!correspondence) {
|
||||||
|
throw new NotFoundException(`RFA with UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
return this.findOne(correspondence.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByUuidRaw(uuid: string) {
|
||||||
|
const correspondence = await this.correspondenceRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
if (!correspondence) {
|
||||||
|
throw new NotFoundException(`RFA with UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
return this.findOne(correspondence.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
async findOne(id: number, rawEntities = false) {
|
async findOne(id: number, rawEntities = false) {
|
||||||
const rfa = await this.rfaRepo.findOne({
|
const rfa = await this.rfaRepo.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -405,22 +450,26 @@ export class RfaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map to structure expected by frontend DTO
|
// Map to structure expected by frontend DTO
|
||||||
const mappedRfa = { ...rfa } as any;
|
const revisions =
|
||||||
mappedRfa.revisions =
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
rfa.correspondence?.revisions?.map((cr) => ({
|
const mappedRfa: RfaMapped = {
|
||||||
|
...rfa,
|
||||||
|
uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level
|
||||||
|
revisions: revisions.map((cr) => ({
|
||||||
...cr,
|
...cr,
|
||||||
...(cr.rfaRevision || {}),
|
...(cr.rfaRevision ?? {}),
|
||||||
id: cr.rfaRevision?.id || cr.id,
|
id: cr.rfaRevision?.id ?? cr.id,
|
||||||
})) || [];
|
})) as CorrRevWithRfa[],
|
||||||
|
};
|
||||||
|
|
||||||
return mappedRfa;
|
return mappedRfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(rfaId: number, templateId: number, user: User) {
|
async submit(rfaId: number, templateId: number, user: User) {
|
||||||
const rfa = await this.findOne(rfaId, true);
|
const rfa = await this.findOne(rfaId, true);
|
||||||
const currentCorrRev = rfa.correspondence?.revisions?.find(
|
const corrRevisions =
|
||||||
(r: any) => r.isCurrent
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
);
|
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||||
throw new NotFoundException('Current revision not found');
|
throw new NotFoundException('Current revision not found');
|
||||||
|
|
||||||
@@ -512,9 +561,9 @@ export class RfaService {
|
|||||||
async processAction(rfaId: number, dto: WorkflowActionDto, user: User) {
|
async processAction(rfaId: number, dto: WorkflowActionDto, user: User) {
|
||||||
// Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB
|
// Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB
|
||||||
const rfa = await this.findOne(rfaId, true);
|
const rfa = await this.findOne(rfaId, true);
|
||||||
const currentCorrRev = rfa.correspondence?.revisions?.find(
|
const corrRevisions =
|
||||||
(r: any) => r.isCurrent
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
);
|
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||||
throw new NotFoundException('Current revision not found');
|
throw new NotFoundException('Current revision not found');
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class SearchService implements OnModuleInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly esService: ElasticsearchService,
|
private readonly esService: ElasticsearchService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
@@ -34,6 +34,7 @@ export class SearchService implements OnModuleInit {
|
|||||||
mappings: {
|
mappings: {
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'integer' },
|
id: { type: 'integer' },
|
||||||
|
uuid: { type: 'keyword' }, // ADR-019: public identifier
|
||||||
type: { type: 'keyword' }, // correspondence, rfa, drawing
|
type: { type: 'keyword' }, // correspondence, rfa, drawing
|
||||||
docNumber: { type: 'text' },
|
docNumber: { type: 'text' },
|
||||||
title: { type: 'text', analyzer: 'standard' },
|
title: { type: 'text', analyzer: 'standard' },
|
||||||
@@ -60,12 +61,12 @@ export class SearchService implements OnModuleInit {
|
|||||||
try {
|
try {
|
||||||
return await this.esService.index({
|
return await this.esService.index({
|
||||||
index: this.indexName,
|
index: this.indexName,
|
||||||
id: `${doc.type}_${doc.id}`, // Unique ID: rfa_101
|
id: doc.uuid ? `${doc.type}_${doc.uuid}` : `${doc.type}_${doc.id}`, // ADR-019: prefer UUID key
|
||||||
document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน
|
document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to index document: ${(error as Error).message}`,
|
`Failed to index document: ${(error as Error).message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ export class SearchService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to remove document: ${(error as Error).message}`,
|
`Failed to remove document: ${(error as Error).message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import {
|
|||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsUUID,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { TransmittalPurpose } from './create-transmittal.dto';
|
import { TransmittalPurpose } from './create-transmittal.dto';
|
||||||
|
|
||||||
export class SearchTransmittalDto {
|
export class SearchTransmittalDto {
|
||||||
@IsInt()
|
@IsUUID('all')
|
||||||
@Type(() => Number)
|
@IsOptional()
|
||||||
@IsNotEmpty()
|
projectUuid?: string; // ADR-019: Public UUID of the project
|
||||||
projectId!: number; // บังคับระบุ Project
|
|
||||||
|
/** @internal Resolved INT ID — set by controller, do NOT expose in API */
|
||||||
|
projectId?: number;
|
||||||
|
|
||||||
@IsEnum(TransmittalPurpose)
|
@IsEnum(TransmittalPurpose)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -5,39 +5,59 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
|
||||||
Query,
|
Query,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { TransmittalService } from './transmittal.service';
|
import { TransmittalService } from './transmittal.service';
|
||||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||||
|
import { SearchTransmittalDto } from './dto/search-transmittal.dto';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
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 { User } from '../user/entities/user.entity';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
import { ProjectService } from '../project/project.service';
|
||||||
|
|
||||||
@ApiTags('Transmittals')
|
@ApiTags('Transmittals')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
@Controller('transmittals')
|
@Controller('transmittals')
|
||||||
export class TransmittalController {
|
export class TransmittalController {
|
||||||
constructor(private readonly transmittalService: TransmittalService) {}
|
constructor(
|
||||||
|
private readonly transmittalService: TransmittalService,
|
||||||
|
private readonly projectService: ProjectService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new Transmittal' })
|
@ApiOperation({ summary: 'Create a new Transmittal' })
|
||||||
|
@RequirePermission('document.create')
|
||||||
create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) {
|
create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) {
|
||||||
return this.transmittalService.create(createDto, user);
|
return this.transmittalService.create(createDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Search Transmittals' })
|
@ApiOperation({ summary: 'Search Transmittals' })
|
||||||
findAll(@Query() searchDto: any) {
|
@RequirePermission('document.view')
|
||||||
// Using any for simplicity as I can't import SearchTransmittalDto easily without checking its export
|
async findAll(
|
||||||
|
@Query() searchDto: SearchTransmittalDto,
|
||||||
|
@CurrentUser() _user: User
|
||||||
|
) {
|
||||||
|
// ADR-019: resolve projectUuid → internal INT projectId if needed
|
||||||
|
if (searchDto.projectUuid) {
|
||||||
|
const project = await this.projectService.findOneByUuid(
|
||||||
|
searchDto.projectUuid
|
||||||
|
);
|
||||||
|
searchDto.projectId = project.id;
|
||||||
|
}
|
||||||
return this.transmittalService.findAll(searchDto);
|
return this.transmittalService.findAll(searchDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Transmittal details' })
|
@ApiOperation({ summary: 'Get Transmittal details' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
@ApiParam({ name: 'uuid', description: 'Transmittal UUID (from correspondences.uuid)' })
|
||||||
return this.transmittalService.findOne(id);
|
@RequirePermission('document.view')
|
||||||
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
|
return this.transmittalService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { CorrespondenceStatus } from '../correspondence/entities/correspondence-
|
|||||||
import { TransmittalService } from './transmittal.service';
|
import { TransmittalService } from './transmittal.service';
|
||||||
import { TransmittalController } from './transmittal.controller';
|
import { TransmittalController } from './transmittal.controller';
|
||||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||||
|
import { ProjectModule } from '../project/project.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { SearchModule } from '../search/search.module';
|
import { SearchModule } from '../search/search.module';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ import { SearchModule } from '../search/search.module';
|
|||||||
CorrespondenceStatus,
|
CorrespondenceStatus,
|
||||||
]),
|
]),
|
||||||
DocumentNumberingModule,
|
DocumentNumberingModule,
|
||||||
|
ProjectModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository, DataSource } from 'typeorm';
|
import { Repository, DataSource } from 'typeorm';
|
||||||
import { Transmittal } from './entities/transmittal.entity';
|
import { Transmittal } from './entities/transmittal.entity';
|
||||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
import {
|
||||||
|
CreateTransmittalDto,
|
||||||
|
TransmittalItemDto,
|
||||||
|
} from './dto/create-transmittal.dto';
|
||||||
|
import { SearchTransmittalDto } from './dto/search-transmittal.dto';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||||
@@ -125,14 +129,7 @@ export class TransmittalService {
|
|||||||
|
|
||||||
// 6. Create Items
|
// 6. Create Items
|
||||||
if (createDto.items && createDto.items.length > 0) {
|
if (createDto.items && createDto.items.length > 0) {
|
||||||
// Filter only items that are effectively correspondences (or mapped as such)
|
const items = createDto.items.map((item: TransmittalItemDto) =>
|
||||||
// For now, assuming itemId refers to correspondenceId if itemType is CORRESPONDENCE
|
|
||||||
// If itemType is DRAWING, we skip or throw error (Schema Restriction)
|
|
||||||
const validItems = createDto.items.filter(
|
|
||||||
(i) => i.itemType === 'CORRESPONDENCE' || i.itemType === 'DRAWING' // Temporary allow DRAWING if ID matches Correspondence? Unsafe.
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = createDto.items.map((item) =>
|
|
||||||
queryRunner.manager.create(TransmittalItem, {
|
queryRunner.manager.create(TransmittalItem, {
|
||||||
transmittalId: savedCorr.id,
|
transmittalId: savedCorr.id,
|
||||||
itemCorrespondenceId: item.itemId, // Direct mapping forced by Schema
|
itemCorrespondenceId: item.itemId, // Direct mapping forced by Schema
|
||||||
@@ -160,6 +157,21 @@ export class TransmittalService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADR-019: Find Transmittal by parent Correspondence UUID (public identifier).
|
||||||
|
* Resolves correspondence.uuid → internal correspondenceId (INT)
|
||||||
|
*/
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const correspondence = await this.dataSource.manager.findOne(
|
||||||
|
Correspondence,
|
||||||
|
{ where: { uuid }, select: ['id'] }
|
||||||
|
);
|
||||||
|
if (!correspondence) {
|
||||||
|
throw new NotFoundException(`Transmittal with UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
return this.findOne(correspondence.id);
|
||||||
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
async findOne(id: number) {
|
||||||
const transmittal = await this.transmittalRepo.findOne({
|
const transmittal = await this.transmittalRepo.findOne({
|
||||||
where: { correspondenceId: id },
|
where: { correspondenceId: id },
|
||||||
@@ -170,9 +182,9 @@ export class TransmittalService {
|
|||||||
return transmittal;
|
return transmittal;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(query: any) {
|
async findAll(query: SearchTransmittalDto) {
|
||||||
const { page = 1, limit = 20, projectId, search } = query;
|
const { page = 1, limit = 20, projectId, search } = query;
|
||||||
const skip = (page - 1) * limit;
|
const skip = ((page ?? 1) - 1) * (limit ?? 20);
|
||||||
|
|
||||||
const queryBuilder = this.transmittalRepo
|
const queryBuilder = this.transmittalRepo
|
||||||
.createQueryBuilder('transmittal')
|
.createQueryBuilder('transmittal')
|
||||||
@@ -205,8 +217,14 @@ export class TransmittalService {
|
|||||||
.take(limit)
|
.take(limit)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
|
// ADR-019: Map correspondence.uuid to top level for frontend convenience
|
||||||
|
const mappedItems = items.map((t) => ({
|
||||||
|
...t,
|
||||||
|
uuid: t.correspondence?.uuid,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: items,
|
data: mappedItems,
|
||||||
meta: {
|
meta: {
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function NumberingPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projects.length > 0 && !selectedProjectId) {
|
if (projects.length > 0 && !selectedProjectId) {
|
||||||
const first = projects[0] as any;
|
const first = projects[0] as any;
|
||||||
setSelectedProjectId(String(first.id || first.uuid));
|
setSelectedProjectId(String(first.id ?? first.uuid));
|
||||||
}
|
}
|
||||||
}, [projects, selectedProjectId]);
|
}, [projects, selectedProjectId]);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export default function NumberingPage() {
|
|||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
|
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
|
||||||
|
|
||||||
const selectedProject = projects.find((p: any) => String(p.id || p.uuid) === selectedProjectId) as any;
|
const selectedProject = projects.find((p: any) => String(p.id ?? p.uuid) === selectedProjectId) as any;
|
||||||
const selectedProjectName = selectedProject?.projectName || 'Unknown Project';
|
const selectedProjectName = selectedProject?.projectName || 'Unknown Project';
|
||||||
|
|
||||||
// Master Data
|
// Master Data
|
||||||
@@ -109,7 +109,7 @@ export default function NumberingPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(projects as any[]).map((project) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
<SelectItem key={project.id ?? project.uuid} value={String(project.id ?? project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -137,7 +137,7 @@ export default function NumberingPage() {
|
|||||||
<div className="lg:col-span-2 space-y-4">
|
<div className="lg:col-span-2 space-y-4">
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{templates
|
{templates
|
||||||
.filter((t: any) => !t.projectId || String(t.projectId) === selectedProjectId || t.project?.uuid === selectedProjectId)
|
.filter((t: any) => !t.projectId || String(t.project?.id ?? t.project?.uuid) === selectedProjectId || t.project?.uuid === selectedProjectId)
|
||||||
.map((template) => (
|
.map((template) => (
|
||||||
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
|
|||||||
+3
-3
@@ -6,11 +6,11 @@ import { useRFA } from "@/hooks/use-rfa";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export default function RFADetailPage() {
|
export default function RFADetailPage() {
|
||||||
const { id } = useParams();
|
const { uuid } = useParams();
|
||||||
|
|
||||||
if (!id) notFound();
|
if (!uuid) notFound();
|
||||||
|
|
||||||
const { data: rfa, isLoading, isError } = useRFA(String(id));
|
const { data: rfa, isLoading, isError } = useRFA(String(uuid));
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
+5
-5
@@ -22,12 +22,12 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
export default function TransmittalDetailPage() {
|
export default function TransmittalDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const uuid = params.uuid as string;
|
||||||
|
|
||||||
const { data: transmittal, isLoading, error } = useQuery<Transmittal>({
|
const { data: transmittal, isLoading, error } = useQuery<Transmittal>({
|
||||||
queryKey: ["transmittal", id],
|
queryKey: ["transmittal", uuid],
|
||||||
queryFn: () => transmittalService.getById(id),
|
queryFn: () => transmittalService.getByUuid(uuid),
|
||||||
enabled: !!id,
|
enabled: !!uuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePrint = () => {
|
const handlePrint = () => {
|
||||||
@@ -100,7 +100,7 @@ export default function TransmittalDetailPage() {
|
|||||||
<p className="text-sm text-muted-foreground">Generated From</p>
|
<p className="text-sm text-muted-foreground">Generated From</p>
|
||||||
{transmittal.correspondence ? (
|
{transmittal.correspondence ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/correspondences/${transmittal.correspondenceId}`}
|
href={`/correspondences/${transmittal.correspondence.uuid}`}
|
||||||
className="font-medium text-primary hover:underline"
|
className="font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{transmittal.correspondence.correspondence_number}
|
{transmittal.correspondence.correspondence_number}
|
||||||
@@ -1,22 +1,41 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { TransmittalList } from "@/components/transmittal/transmittal-list";
|
import { TransmittalList } from "@/components/transmittal/transmittal-list";
|
||||||
import { transmittalService } from "@/lib/services/transmittal.service";
|
import { transmittalService } from "@/lib/services/transmittal.service";
|
||||||
|
import { projectService } from "@/lib/services/project.service";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Plus, RefreshCw } from "lucide-react";
|
import { Plus, RefreshCw } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TransmittalListResponse } from "@/types/transmittal";
|
import { TransmittalListResponse } from "@/types/transmittal";
|
||||||
|
|
||||||
export default function TransmittalPage() {
|
export default function TransmittalPage() {
|
||||||
|
// ADR-019: Dynamic project selection via UUID
|
||||||
|
const [selectedProjectUuid, setSelectedProjectUuid] = useState<string>("");
|
||||||
|
|
||||||
|
const { data: projectsData } = useQuery({
|
||||||
|
queryKey: ["projects-for-transmittals"],
|
||||||
|
queryFn: () => projectService.getAll(),
|
||||||
|
});
|
||||||
|
const projects = projectsData?.data || projectsData || [];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery<TransmittalListResponse>({
|
} = useQuery<TransmittalListResponse>({
|
||||||
queryKey: ["transmittals"],
|
queryKey: ["transmittals", selectedProjectUuid],
|
||||||
queryFn: () => transmittalService.getAll({ projectId: 1 }),
|
queryFn: () => transmittalService.getAll({ projectId: selectedProjectUuid }),
|
||||||
|
enabled: !!selectedProjectUuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,6 +66,23 @@ export default function TransmittalPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ADR-019: Project filter */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Project:</span>
|
||||||
|
<Select value={selectedProjectUuid} onValueChange={setSelectedProjectUuid}>
|
||||||
|
<SelectTrigger className="w-[280px]">
|
||||||
|
<SelectValue placeholder="Select a project" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(Array.isArray(projects) ? projects : []).map((p: { uuid: string; projectName?: string; projectCode?: string }) => (
|
||||||
|
<SelectItem key={p.uuid} value={p.uuid}>
|
||||||
|
{p.projectName || p.projectCode}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-destructive/10 text-destructive px-4 py-3 rounded-md">
|
<div className="bg-destructive/10 text-destructive px-4 py-3 rounded-md">
|
||||||
Failed to load transmittals.
|
Failed to load transmittals.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// Master Data Hooks
|
// Master Data Hooks
|
||||||
const projectId = template?.projectId || 1;
|
const projectId = (template as any)?.project?.id ?? (template as any)?.project?.uuid ?? template?.projectId ?? 1;
|
||||||
const { data: organizations } = useOrganizations({ isActive: true });
|
const { data: organizations } = useOrganizations({ isActive: true });
|
||||||
const { data: correspondenceTypes } = useCorrespondenceTypes();
|
const { data: correspondenceTypes } = useCorrespondenceTypes();
|
||||||
const { data: contracts } = useContracts(projectId);
|
const { data: contracts } = useContracts(projectId);
|
||||||
@@ -117,7 +117,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(organizations as Organization[])?.map((org) => (
|
{(organizations as Organization[])?.map((org) => (
|
||||||
<SelectItem key={org.uuid} value={(org.id ?? org.uuid).toString()}>
|
<SelectItem key={org.uuid} value={org.uuid}>
|
||||||
{org.organizationCode} - {org.organizationName}
|
{org.organizationCode} - {org.organizationName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -137,7 +137,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(organizations as Organization[])?.map((org) => (
|
{(organizations as Organization[])?.map((org) => (
|
||||||
<SelectItem key={org.uuid} value={(org.id ?? org.uuid).toString()}>
|
<SelectItem key={org.uuid} value={org.uuid}>
|
||||||
{org.organizationCode} - {org.organizationName}
|
{org.organizationCode} - {org.organizationName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function RFADetail({ data }: RFADetailProps) {
|
|||||||
|
|
||||||
processMutation.mutate(
|
processMutation.mutate(
|
||||||
{
|
{
|
||||||
id: data.rfaId,
|
uuid: data.uuid,
|
||||||
data: {
|
data: {
|
||||||
action: apiAction,
|
action: apiAction,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCreateRFA } from "@/hooks/use-rfa";
|
import { useCreateRFA } from "@/hooks/use-rfa";
|
||||||
import { useDisciplines, useContracts } from "@/hooks/use-master-data";
|
import { useDisciplines, useContracts } from "@/hooks/use-master-data";
|
||||||
|
import { useProjects } from "@/hooks/use-projects";
|
||||||
import { CreateRFADto } from "@/types/rfa";
|
import { CreateRFADto } from "@/types/rfa";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { correspondenceService } from "@/lib/services/correspondence.service";
|
import { correspondenceService } from "@/lib/services/correspondence.service";
|
||||||
@@ -30,6 +31,7 @@ const rfaItemSchema = z.object({
|
|||||||
unit: z.string().min(1, "Unit is required"),
|
unit: z.string().min(1, "Unit is required"),
|
||||||
});
|
});
|
||||||
const rfaSchema = z.object({
|
const rfaSchema = z.object({
|
||||||
|
projectId: z.string().min(1, "Project is required"), // ADR-019: UUID
|
||||||
contractId: z.string().min(1, "Contract is required"),
|
contractId: z.string().min(1, "Contract is required"),
|
||||||
disciplineId: z.number().min(1, "Discipline is required"),
|
disciplineId: z.number().min(1, "Discipline is required"),
|
||||||
rfaTypeId: z.number().min(1, "Type is required"),
|
rfaTypeId: z.number().min(1, "Type is required"),
|
||||||
@@ -49,9 +51,9 @@ export function RFAForm() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createMutation = useCreateRFA();
|
const createMutation = useCreateRFA();
|
||||||
|
|
||||||
// Dynamic Contract Loading (Default Project Context: 1)
|
// ADR-019: Dynamic project selection
|
||||||
const currentProjectId = 1;
|
const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
|
||||||
const { data: contracts, isLoading: isLoadingContracts } = useContracts(currentProjectId);
|
const projects = projectsData?.data || projectsData || [];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -63,6 +65,7 @@ export function RFAForm() {
|
|||||||
} = useForm<RFAFormData>({
|
} = useForm<RFAFormData>({
|
||||||
resolver: zodResolver(rfaSchema),
|
resolver: zodResolver(rfaSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
projectId: "",
|
||||||
contractId: "",
|
contractId: "",
|
||||||
disciplineId: 0,
|
disciplineId: 0,
|
||||||
rfaTypeId: 0,
|
rfaTypeId: 0,
|
||||||
@@ -77,6 +80,9 @@ export function RFAForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedProjectId = watch("projectId");
|
||||||
|
const { data: contracts, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
|
||||||
|
|
||||||
const selectedContractId = watch("contractId");
|
const selectedContractId = watch("contractId");
|
||||||
const { data: disciplines, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);
|
const { data: disciplines, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);
|
||||||
|
|
||||||
@@ -97,7 +103,7 @@ export function RFAForm() {
|
|||||||
const fetchPreview = async () => {
|
const fetchPreview = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await correspondenceService.previewNumber({
|
const res = await correspondenceService.previewNumber({
|
||||||
projectId: currentProjectId,
|
projectId: selectedProjectId,
|
||||||
typeId: rfaTypeId, // RfaTypeId acts as TypeId
|
typeId: rfaTypeId, // RfaTypeId acts as TypeId
|
||||||
disciplineId,
|
disciplineId,
|
||||||
// RFA uses 'TO' organization as recipient
|
// RFA uses 'TO' organization as recipient
|
||||||
@@ -112,7 +118,7 @@ export function RFAForm() {
|
|||||||
|
|
||||||
const timer = setTimeout(fetchPreview, 500);
|
const timer = setTimeout(fetchPreview, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [rfaTypeId, disciplineId, toOrganizationId, currentProjectId]);
|
}, [rfaTypeId, disciplineId, toOrganizationId, selectedProjectId]);
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@@ -122,7 +128,7 @@ export function RFAForm() {
|
|||||||
const onSubmit = (data: RFAFormData) => {
|
const onSubmit = (data: RFAFormData) => {
|
||||||
const payload: CreateRFADto = {
|
const payload: CreateRFADto = {
|
||||||
...data,
|
...data,
|
||||||
projectId: currentProjectId,
|
// ADR-019: projectId is already a UUID string from the form
|
||||||
};
|
};
|
||||||
createMutation.mutate(payload as any, {
|
createMutation.mutate(payload as any, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -178,19 +184,45 @@ export function RFAForm() {
|
|||||||
<Input id="description" {...register("description")} placeholder="Enter key description" />
|
<Input id="description" {...register("description")} placeholder="Enter key description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ADR-019: Project selector */}
|
||||||
|
<div>
|
||||||
|
<Label>Project *</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setValue("projectId", val);
|
||||||
|
setValue("contractId", ""); // Reset contract when project changes
|
||||||
|
}}
|
||||||
|
disabled={isLoadingProjects}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={isLoadingProjects ? "Loading..." : "Select Project"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(Array.isArray(projects) ? projects : []).map((p: { uuid: string; projectName?: string; projectCode?: string }) => (
|
||||||
|
<SelectItem key={p.uuid} value={p.uuid}>
|
||||||
|
{p.projectName || p.projectCode}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.projectId && (
|
||||||
|
<p className="text-sm text-destructive mt-1">{errors.projectId.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Contract *</Label>
|
<Label>Contract *</Label>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(val) => setValue("contractId", val)}
|
onValueChange={(val) => setValue("contractId", val)}
|
||||||
disabled={isLoadingContracts}
|
disabled={!selectedProjectId || isLoadingContracts}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={isLoadingContracts ? "Loading..." : "Select Contract"} />
|
<SelectValue placeholder={isLoadingContracts ? "Loading..." : "Select Contract"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{contracts?.map((c: any) => (
|
{contracts?.map((c: { uuid: string; contractName?: string; name?: string; contractCode?: string }) => (
|
||||||
<SelectItem key={c.id} value={String(c.id)}>
|
<SelectItem key={c.uuid} value={c.uuid}>
|
||||||
{c.contractName || c.name || c.contractCode}
|
{c.contractName || c.name || c.contractCode}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function RFAList({ data }: RFAListProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Link href={`/rfas/${row.original.id}`}>
|
<Link href={`/rfas/${row.original.uuid}`}>
|
||||||
<Button variant="ghost" size="icon" title="View Details">
|
<Button variant="ghost" size="icon" title="View Details">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -97,7 +97,7 @@ export function RFAList({ data }: RFAListProps) {
|
|||||||
<Button variant="ghost" size="icon" title="View File" onClick={handleViewFile}>
|
<Button variant="ghost" size="icon" title="View File" onClick={handleViewFile}>
|
||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Link href={`/rfas/${row.original.id}/edit`}>
|
<Link href={`/rfas/${row.original.uuid}/edit`}>
|
||||||
<Button variant="ghost" size="icon" title="Edit">
|
<Button variant="ghost" size="icon" title="Edit">
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { transmittalService } from "@/lib/services/transmittal.service";
|
import { transmittalService } from "@/lib/services/transmittal.service";
|
||||||
import { correspondenceService } from "@/lib/services/correspondence.service";
|
import { correspondenceService } from "@/lib/services/correspondence.service";
|
||||||
|
import { projectService } from "@/lib/services/project.service";
|
||||||
|
import { organizationService } from "@/lib/services/organization.service";
|
||||||
import { CreateTransmittalDto } from "@/types/dto/transmittal/transmittal.dto";
|
import { CreateTransmittalDto } from "@/types/dto/transmittal/transmittal.dto";
|
||||||
|
|
||||||
// UI Components
|
// UI Components
|
||||||
@@ -59,6 +61,8 @@ const itemSchema = z.object({
|
|||||||
|
|
||||||
// Main form schema
|
// Main form schema
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
projectId: z.string().min(1, "Project is required"), // ADR-019: UUID
|
||||||
|
recipientOrganizationId: z.string().min(1, "Recipient is required"), // ADR-019: UUID
|
||||||
correspondenceId: z.string().min(1, "Correspondence is required"), // ADR-019: UUID string
|
correspondenceId: z.string().min(1, "Correspondence is required"), // ADR-019: UUID string
|
||||||
subject: z.string().min(1, "Subject is required"),
|
subject: z.string().min(1, "Subject is required"),
|
||||||
purpose: z.enum(["FOR_APPROVAL", "FOR_INFORMATION", "FOR_REVIEW", "OTHER"]),
|
purpose: z.enum(["FOR_APPROVAL", "FOR_INFORMATION", "FOR_REVIEW", "OTHER"]),
|
||||||
@@ -75,6 +79,9 @@ export function TransmittalForm() {
|
|||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
projectId: "",
|
||||||
|
recipientOrganizationId: "",
|
||||||
|
correspondenceId: "",
|
||||||
subject: "",
|
subject: "",
|
||||||
purpose: "FOR_APPROVAL",
|
purpose: "FOR_APPROVAL",
|
||||||
remarks: "",
|
remarks: "",
|
||||||
@@ -89,6 +96,19 @@ export function TransmittalForm() {
|
|||||||
name: "items",
|
name: "items",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ADR-019: Fetch projects and organizations for UUID-based selectors
|
||||||
|
const { data: projectsData, isLoading: isLoadingProjects } = useQuery({
|
||||||
|
queryKey: ["projects-dropdown"],
|
||||||
|
queryFn: () => projectService.getAll(),
|
||||||
|
});
|
||||||
|
const projectsList = projectsData?.data || projectsData || [];
|
||||||
|
|
||||||
|
const { data: orgsData, isLoading: isLoadingOrgs } = useQuery({
|
||||||
|
queryKey: ["organizations-dropdown"],
|
||||||
|
queryFn: () => organizationService.getAll(),
|
||||||
|
});
|
||||||
|
const orgsList = orgsData?.data || orgsData || [];
|
||||||
|
|
||||||
// Fetch correspondences (for header linkage)
|
// Fetch correspondences (for header linkage)
|
||||||
const { data: correspondences } = useQuery({
|
const { data: correspondences } = useQuery({
|
||||||
queryKey: ["correspondences-dropdown"],
|
queryKey: ["correspondences-dropdown"],
|
||||||
@@ -99,7 +119,8 @@ export function TransmittalForm() {
|
|||||||
mutationFn: (data: CreateTransmittalDto) => transmittalService.create(data),
|
mutationFn: (data: CreateTransmittalDto) => transmittalService.create(data),
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
toast.success("Transmittal created successfully");
|
toast.success("Transmittal created successfully");
|
||||||
router.push(`/transmittals/${result.id}`);
|
// ADR-019: Navigate using UUID from correspondence
|
||||||
|
router.push(`/transmittals/${result.correspondence?.uuid || result.uuid}`);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error("Failed to create transmittal");
|
toast.error("Failed to create transmittal");
|
||||||
@@ -107,13 +128,13 @@ export function TransmittalForm() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: FormData) => {
|
const onSubmit = (data: FormData) => {
|
||||||
// Better fix: Add missing recipientOrganizationId mock
|
// ADR-019: All IDs are now UUID strings from the form
|
||||||
const cleanPayload: CreateTransmittalDto = {
|
const cleanPayload: CreateTransmittalDto = {
|
||||||
projectId: 1,
|
projectId: data.projectId,
|
||||||
recipientOrganizationId: 99, // Mock
|
recipientOrganizationId: data.recipientOrganizationId,
|
||||||
correspondenceId: data.correspondenceId,
|
correspondenceId: data.correspondenceId,
|
||||||
subject: data.subject,
|
subject: data.subject,
|
||||||
purpose: data.purpose as any,
|
purpose: data.purpose as string,
|
||||||
remarks: data.remarks,
|
remarks: data.remarks,
|
||||||
items: data.items.map(item => ({
|
items: data.items.map(item => ({
|
||||||
itemType: item.itemType,
|
itemType: item.itemType,
|
||||||
@@ -139,6 +160,59 @@ export function TransmittalForm() {
|
|||||||
<CardTitle>Transmittal Details</CardTitle>
|
<CardTitle>Transmittal Details</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
|
{/* ADR-019: Project & Recipient Organization selectors */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="projectId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Project</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value} disabled={isLoadingProjects}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={isLoadingProjects ? "Loading..." : "Select Project"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{(Array.isArray(projectsList) ? projectsList : []).map((p: { uuid: string; projectName?: string; projectCode?: string }) => (
|
||||||
|
<SelectItem key={p.uuid} value={p.uuid}>
|
||||||
|
{p.projectName || p.projectCode}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="recipientOrganizationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Recipient Organization</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value} disabled={isLoadingOrgs}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={isLoadingOrgs ? "Loading..." : "Select Organization"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{(Array.isArray(orgsList) ? orgsList : []).map((o: { uuid: string; organizationName?: string; orgCode?: string }) => (
|
||||||
|
<SelectItem key={o.uuid} value={o.uuid}>
|
||||||
|
{o.organizationName || o.orgCode}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* Linked Correspondence (Ref No) */}
|
{/* Linked Correspondence (Ref No) */}
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function TransmittalList({ data }: TransmittalListProps) {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original;
|
const item = row.original;
|
||||||
return (
|
return (
|
||||||
<Link href={`/transmittals/${item.id}`}>
|
<Link href={`/transmittals/${item.uuid}`}>
|
||||||
<Button variant="ghost" size="icon" title="View Details">
|
<Button variant="ghost" size="icon" title="View Details">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+13
-13
@@ -11,7 +11,7 @@ export const rfaKeys = {
|
|||||||
lists: () => [...rfaKeys.all, 'list'] as const,
|
lists: () => [...rfaKeys.all, 'list'] as const,
|
||||||
list: (params: SearchRfaDto) => [...rfaKeys.lists(), params] as const,
|
list: (params: SearchRfaDto) => [...rfaKeys.lists(), params] as const,
|
||||||
details: () => [...rfaKeys.all, 'detail'] as const,
|
details: () => [...rfaKeys.all, 'detail'] as const,
|
||||||
detail: (id: number | string) => [...rfaKeys.details(), id] as const,
|
detail: (uuid: string) => [...rfaKeys.details(), uuid] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Queries ---
|
// --- Queries ---
|
||||||
@@ -24,11 +24,11 @@ export function useRFAs(params: SearchRfaDto) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRFA(id: number | string) {
|
export function useRFA(uuid: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: rfaKeys.detail(id),
|
queryKey: rfaKeys.detail(uuid),
|
||||||
queryFn: () => rfaService.getById(id),
|
queryFn: () => rfaService.getByUuid(uuid),
|
||||||
enabled: !!id,
|
enabled: !!uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ export function useUpdateRFA() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number | string; data: UpdateRfaDto }) =>
|
mutationFn: ({ uuid, data }: { uuid: string; data: UpdateRfaDto }) =>
|
||||||
rfaService.update(id, data),
|
rfaService.update(uuid, data),
|
||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { uuid }) => {
|
||||||
toast.success('RFA updated successfully');
|
toast.success('RFA updated successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: rfaKeys.detail(id) });
|
queryClient.invalidateQueries({ queryKey: rfaKeys.detail(uuid) });
|
||||||
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
@@ -74,11 +74,11 @@ export function useProcessRFA() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number | string; data: WorkflowActionDto }) =>
|
mutationFn: ({ uuid, data }: { uuid: string; data: WorkflowActionDto }) =>
|
||||||
rfaService.processWorkflow(id, data),
|
rfaService.processWorkflow(uuid, data),
|
||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { uuid }) => {
|
||||||
toast.success('Workflow status updated successfully');
|
toast.success('Workflow status updated successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: rfaKeys.detail(id) });
|
queryClient.invalidateQueries({ queryKey: rfaKeys.detail(uuid) });
|
||||||
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ export const rfaService = {
|
|||||||
/**
|
/**
|
||||||
* ดึงรายละเอียด RFA และประวัติ Workflow
|
* ดึงรายละเอียด RFA และประวัติ Workflow
|
||||||
*/
|
*/
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
// GET /rfas/:id
|
// GET /rfas/:uuid (ADR-019)
|
||||||
const response = await apiClient.get(`/rfas/${id}`);
|
const response = await apiClient.get(`/rfas/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -44,26 +44,27 @@ export const rfaService = {
|
|||||||
/**
|
/**
|
||||||
* แก้ไข RFA (เฉพาะสถานะ Draft)
|
* แก้ไข RFA (เฉพาะสถานะ Draft)
|
||||||
*/
|
*/
|
||||||
update: async (id: string | number, data: UpdateRfaDto) => {
|
update: async (uuid: string, data: UpdateRfaDto) => {
|
||||||
// PUT /rfas/:id
|
// PUT /rfas/:uuid (ADR-019)
|
||||||
const response = await apiClient.put(`/rfas/${id}`, data);
|
const response = await apiClient.put(`/rfas/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ดำเนินการ Workflow (อนุมัติ / ตีกลับ / ส่งต่อ)
|
* ดำเนินการ Workflow (อนุมัติ / ตีกลับ / ส่งต่อ)
|
||||||
*/
|
*/
|
||||||
processWorkflow: async (id: string | number, actionData: WorkflowActionDto) => {
|
processWorkflow: async (uuid: string, actionData: WorkflowActionDto) => {
|
||||||
// POST /rfas/:id/workflow
|
// POST /rfas/:uuid/workflow (ADR-019)
|
||||||
const response = await apiClient.post(`/rfas/${id}/workflow`, actionData);
|
const response = await apiClient.post(`/rfas/${uuid}/workflow`, actionData);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Optional) ลบ RFA (Soft Delete)
|
* (Optional) ลบ RFA (Soft Delete)
|
||||||
*/
|
*/
|
||||||
delete: async (id: string | number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/rfas/${id}`);
|
// DELETE /rfas/:uuid (ADR-019)
|
||||||
|
const response = await apiClient.delete(`/rfas/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -17,11 +17,11 @@ export const transmittalService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ดึงรายละเอียด Transmittal ตาม ID
|
* ดึงรายละเอียด Transmittal ตาม UUID (ADR-019)
|
||||||
*/
|
*/
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
// GET /transmittals/:id
|
// GET /transmittals/:uuid
|
||||||
const response = await apiClient.get(`/transmittals/${id}`);
|
const response = await apiClient.get(`/transmittals/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -37,18 +37,18 @@ export const transmittalService = {
|
|||||||
/**
|
/**
|
||||||
* แก้ไขข้อมูล Transmittal (เฉพาะ Draft)
|
* แก้ไขข้อมูล Transmittal (เฉพาะ Draft)
|
||||||
*/
|
*/
|
||||||
update: async (id: string | number, data: UpdateTransmittalDto) => {
|
update: async (uuid: string, data: UpdateTransmittalDto) => {
|
||||||
// PUT /transmittals/:id
|
// PUT /transmittals/:uuid (ADR-019)
|
||||||
const response = await apiClient.put(`/transmittals/${id}`, data);
|
const response = await apiClient.put(`/transmittals/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบเอกสาร (Soft Delete)
|
* ลบเอกสาร (Soft Delete)
|
||||||
*/
|
*/
|
||||||
delete: async (id: string | number) => {
|
delete: async (uuid: string) => {
|
||||||
// DELETE /transmittals/:id
|
// DELETE /transmittals/:uuid (ADR-019)
|
||||||
const response = await apiClient.delete(`/transmittals/${id}`);
|
const response = await apiClient.delete(`/transmittals/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lcbp3-frontend",
|
"name": "lcbp3-frontend",
|
||||||
"version": "1.5.1",
|
"version": "1.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export interface RFAItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RFA {
|
export interface RFA {
|
||||||
id: number; // Shared PK with Correspondence
|
uuid: string; // ADR-019: from correspondence.uuid
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
rfaTypeId: number;
|
rfaTypeId: number;
|
||||||
createdBy: number;
|
createdBy: number;
|
||||||
disciplineId?: number;
|
disciplineId?: number;
|
||||||
@@ -35,12 +36,14 @@ export interface RFA {
|
|||||||
};
|
};
|
||||||
// Shared Correspondence Relation
|
// Shared Correspondence Relation
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
project?: {
|
project?: {
|
||||||
|
uuid: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
projectCode: string;
|
projectCode: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ export interface TransmittalItem {
|
|||||||
* Main Transmittal entity
|
* Main Transmittal entity
|
||||||
*/
|
*/
|
||||||
export interface Transmittal {
|
export interface Transmittal {
|
||||||
id: number;
|
uuid: string; // ADR-019: from correspondence.uuid
|
||||||
correspondenceId: number;
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
|
correspondenceId?: number | string;
|
||||||
transmittalNo: string;
|
transmittalNo: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
purpose?: TransmittalPurpose;
|
purpose?: TransmittalPurpose;
|
||||||
@@ -38,7 +39,8 @@ export interface Transmittal {
|
|||||||
// Joined relations from API
|
// Joined relations from API
|
||||||
items?: TransmittalItem[];
|
items?: TransmittalItem[];
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondence_number: string;
|
correspondence_number: string;
|
||||||
project_id: number;
|
project_id: number;
|
||||||
};
|
};
|
||||||
@@ -70,9 +72,9 @@ export interface CreateTransmittalItemDto {
|
|||||||
* DTO for creating a transmittal
|
* DTO for creating a transmittal
|
||||||
*/
|
*/
|
||||||
export interface CreateTransmittalDto {
|
export interface CreateTransmittalDto {
|
||||||
projectId?: number;
|
projectId?: number | string; // ADR-019: Accept UUID
|
||||||
recipientOrganizationId?: number;
|
recipientOrganizationId?: number | string; // ADR-019: Accept UUID
|
||||||
correspondenceId: number;
|
correspondenceId: number | string; // ADR-019: Accept UUID
|
||||||
subject: string;
|
subject: string;
|
||||||
purpose?: TransmittalPurpose;
|
purpose?: TransmittalPurpose;
|
||||||
remarks?: string;
|
remarks?: string;
|
||||||
@@ -85,6 +87,6 @@ export interface CreateTransmittalDto {
|
|||||||
export interface SearchTransmittalDto {
|
export interface SearchTransmittalDto {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
projectId?: number;
|
projectId?: number | string; // ADR-019: Accept UUID
|
||||||
search?: string;
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -928,6 +928,6 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"recommendations": ["jlcodes.antigravity-cockpit"],
|
"recommendations": [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user