From e5769269a875a2142cf0c8c529af3adff1814e62 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 18 Mar 2026 14:01:32 +0700 Subject: [PATCH] 260318:1401 Fix UUID #05 --- backend/fix_switch.js | 133 ------------------ backend/package.json | 2 +- .../correspondence.controller.spec.ts | 2 +- .../correspondence.controller.ts | 10 +- .../correspondence/correspondence.service.ts | 7 +- .../correspondence/dto/add-reference.dto.ts | 10 +- .../dto/create-correspondence.dto.ts | 2 +- .../correspondence-revision.entity.ts | 2 +- .../services/document-numbering.service.ts | 9 +- .../migration/migration.controller.spec.ts | 3 +- backend/src/modules/rfa/dto/search-rfa.dto.ts | 20 +-- .../rfa/entities/rfa-revision.entity.ts | 2 +- backend/src/modules/rfa/rfa.controller.ts | 46 +++--- backend/src/modules/rfa/rfa.module.ts | 2 + backend/src/modules/rfa/rfa.service.ts | 107 ++++++++++---- backend/src/modules/search/search.service.ts | 9 +- .../transmittal/dto/search-transmittal.dto.ts | 12 +- .../transmittal/transmittal.controller.ts | 38 +++-- .../modules/transmittal/transmittal.module.ts | 2 + .../transmittal/transmittal.service.ts | 42 ++++-- .../admin/doc-control/numbering/page.tsx | 8 +- .../rfas/{[id] => [uuid]}/page.tsx | 6 +- .../transmittals/{[id] => [uuid]}/page.tsx | 10 +- .../app/(dashboard)/transmittals/page.tsx | 40 +++++- .../components/numbering/template-tester.tsx | 6 +- frontend/components/rfas/detail.tsx | 2 +- frontend/components/rfas/form.tsx | 50 +++++-- frontend/components/rfas/list.tsx | 4 +- .../transmittal/transmittal-form.tsx | 84 ++++++++++- .../transmittal/transmittal-list.tsx | 2 +- frontend/hooks/use-rfa.ts | 26 ++-- frontend/lib/services/rfa.service.ts | 33 ++--- frontend/lib/services/transmittal.service.ts | 30 ++-- frontend/package.json | 2 +- frontend/types/rfa.ts | 7 +- frontend/types/transmittal.ts | 16 ++- lcbp3.code-workspace | 2 +- 37 files changed, 460 insertions(+), 328 deletions(-) delete mode 100644 backend/fix_switch.js rename frontend/app/(dashboard)/rfas/{[id] => [uuid]}/page.tsx (84%) rename frontend/app/(dashboard)/transmittals/{[id] => [uuid]}/page.tsx (96%) diff --git a/backend/fix_switch.js b/backend/fix_switch.js deleted file mode 100644 index b3938d0..0000000 --- a/backend/fix_switch.js +++ /dev/null @@ -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."); -} diff --git a/backend/package.json b/backend/package.json index 6424194..5f6c498 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "1.5.1", + "version": "1.8.0", "description": "

\r \"Nest\r

", "author": "", "private": true, diff --git a/backend/src/modules/correspondence/correspondence.controller.spec.ts b/backend/src/modules/correspondence/correspondence.controller.spec.ts index ace17f1..00265f2 100644 --- a/backend/src/modules/correspondence/correspondence.controller.spec.ts +++ b/backend/src/modules/correspondence/correspondence.controller.spec.ts @@ -100,7 +100,7 @@ describe('CorrespondenceController', () => { const mockReq = { user: { user_id: 1 } }; const result = await controller.submit( - 1, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', { note: 'Test note' }, mockReq as Parameters[2] ); diff --git a/backend/src/modules/correspondence/correspondence.controller.ts b/backend/src/modules/correspondence/correspondence.controller.ts index a7e3214..96c614a 100644 --- a/backend/src/modules/correspondence/correspondence.controller.ts +++ b/backend/src/modules/correspondence/correspondence.controller.ts @@ -6,7 +6,6 @@ import { UseGuards, Request, Param, - ParseIntPipe, Query, Delete, Put, @@ -43,7 +42,7 @@ export class CorrespondenceController { private readonly workflowService: CorrespondenceWorkflowService ) {} - @Post(':id/workflow/action') + @Post(':uuid/workflow/action') @ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' }) @ApiResponse({ status: 201, description: 'Action processed successfully.' }) @RequirePermission('workflow.action_review') @@ -188,15 +187,16 @@ export class CorrespondenceController { return this.correspondenceService.addReference(corr.id, dto); } - @Delete(':uuid/references/:targetId') + @Delete(':uuid/references/:targetUuid') @ApiOperation({ summary: 'Remove reference' }) @ApiResponse({ status: 200, description: 'Reference removed successfully.' }) @RequirePermission('document.edit') async removeReference( @Param('uuid', ParseUuidPipe) uuid: string, - @Param('targetId', ParseIntPipe) targetId: number + @Param('targetUuid', ParseUuidPipe) targetUuid: string ) { 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); } } diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index d3c088c..68010eb 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -423,8 +423,9 @@ export class CorrespondenceService { async addReference(id: number, dto: AddReferenceDto) { const source = await this.correspondenceRepo.findOne({ where: { id } }); + // ADR-019: Resolve target UUID → internal INT id const target = await this.correspondenceRepo.findOne({ - where: { id: dto.targetId }, + where: { uuid: dto.targetUuid }, }); if (!source || !target) { @@ -438,7 +439,7 @@ export class CorrespondenceService { const exists = await this.referenceRepo.findOne({ where: { sourceId: id, - targetId: dto.targetId, + targetId: target.id, }, }); @@ -448,7 +449,7 @@ export class CorrespondenceService { const ref = this.referenceRepo.create({ sourceId: id, - targetId: dto.targetId, + targetId: target.id, }); return this.referenceRepo.save(ref); diff --git a/backend/src/modules/correspondence/dto/add-reference.dto.ts b/backend/src/modules/correspondence/dto/add-reference.dto.ts index 90ac118..43c4021 100644 --- a/backend/src/modules/correspondence/dto/add-reference.dto.ts +++ b/backend/src/modules/correspondence/dto/add-reference.dto.ts @@ -1,12 +1,12 @@ -import { IsInt, IsNotEmpty } from 'class-validator'; +import { IsUUID, IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AddReferenceDto { @ApiProperty({ - description: 'Target Correspondence ID to reference', - example: 20, + description: 'Target Correspondence UUID to reference (ADR-019)', + example: '019505a1-7c3e-7000-8000-abc123def456', }) - @IsInt() + @IsUUID('all') @IsNotEmpty() - targetId!: number; + targetUuid!: string; } diff --git a/backend/src/modules/correspondence/dto/create-correspondence.dto.ts b/backend/src/modules/correspondence/dto/create-correspondence.dto.ts index a57c314..aec8c62 100644 --- a/backend/src/modules/correspondence/dto/create-correspondence.dto.ts +++ b/backend/src/modules/correspondence/dto/create-correspondence.dto.ts @@ -76,7 +76,7 @@ export class CreateCorrespondenceDto { }) @IsObject() @IsOptional() - details?: Record; // ข้อมูล JSON (เช่น RFI question) + details?: Record; // ข้อมูล JSON (เช่น RFI question) @ApiPropertyOptional({ description: 'Is internal document?', default: false }) @IsBoolean() diff --git a/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts b/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts index dec129d..fc7bd58 100644 --- a/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts +++ b/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts @@ -53,7 +53,7 @@ export class CorrespondenceRevision extends UuidBaseEntity { remarks?: string; @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 }) schemaVersion!: number; diff --git a/backend/src/modules/document-numbering/services/document-numbering.service.ts b/backend/src/modules/document-numbering/services/document-numbering.service.ts index 92e1f6e..e03ef01 100644 --- a/backend/src/modules/document-numbering/services/document-numbering.service.ts +++ b/backend/src/modules/document-numbering/services/document-numbering.service.ts @@ -267,12 +267,17 @@ export class DocumentNumberingService { // --- Admin / Legacy --- async getTemplates() { - return this.formatRepo.find(); + return this.formatRepo.find({ + relations: ['project', 'correspondenceType'], + }); } async getTemplatesByProject(projectId: number | string) { 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) { diff --git a/backend/src/modules/migration/migration.controller.spec.ts b/backend/src/modules/migration/migration.controller.spec.ts index 44ba627..4567d90 100644 --- a/backend/src/modules/migration/migration.controller.spec.ts +++ b/backend/src/modules/migration/migration.controller.spec.ts @@ -33,11 +33,12 @@ describe('MigrationController', () => { it('should call importCorrespondence on service', async () => { const dto: ImportCorrespondenceDto = { document_number: 'DOC-001', - title: 'Legacy Record', + subject: 'Legacy Record', category: 'Correspondence', source_file_path: '/staging_ai/test.pdf', migrated_by: 'SYSTEM_IMPORT', batch_id: 'batch1', + project_id: 1, }; const idempotencyKey = 'key123'; diff --git a/backend/src/modules/rfa/dto/search-rfa.dto.ts b/backend/src/modules/rfa/dto/search-rfa.dto.ts index 8d9d782..260f570 100644 --- a/backend/src/modules/rfa/dto/search-rfa.dto.ts +++ b/backend/src/modules/rfa/dto/search-rfa.dto.ts @@ -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'; export class SearchRfaDto { - @IsInt() - @Type(() => Number) - @IsNotEmpty() - projectId!: number; // บังคับระบุ Project + @IsUUID('all') + projectUuid!: string; // ADR-019: Public UUID of the project + + /** @internal Resolved INT ID — set by controller, do NOT expose in API */ + projectId?: number; @IsOptional() @IsInt() @@ -13,9 +14,12 @@ export class SearchRfaDto { rfaTypeId?: number; // กรองตามประเภท RFA @IsOptional() - @IsInt() - @Type(() => Number) - statusId?: number; // กรองตามสถานะ (เช่น Draft, For Approve) + @IsString() + statusCode?: string; // กรองตามสถานะโดยใช้ status code เช่น 'DFT', 'FAP' + + @IsOptional() + @IsString() + revisionStatus?: string; // 'CURRENT' | 'OLD' | 'ALL' — default 'CURRENT' @IsOptional() @IsString() diff --git a/backend/src/modules/rfa/entities/rfa-revision.entity.ts b/backend/src/modules/rfa/entities/rfa-revision.entity.ts index 18f4d84..2ee940b 100644 --- a/backend/src/modules/rfa/entities/rfa-revision.entity.ts +++ b/backend/src/modules/rfa/entities/rfa-revision.entity.ts @@ -35,7 +35,7 @@ export class RfaRevision { // --- JSON & Schema Section --- @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) @Column({ name: 'schema_version', default: 1 }) diff --git a/backend/src/modules/rfa/rfa.controller.ts b/backend/src/modules/rfa/rfa.controller.ts index 09bf681..bc9a854 100644 --- a/backend/src/modules/rfa/rfa.controller.ts +++ b/backend/src/modules/rfa/rfa.controller.ts @@ -4,7 +4,6 @@ import { Controller, Get, Param, - ParseIntPipe, Post, Query, UseGuards, @@ -22,6 +21,7 @@ import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto'; import { User } from '../user/entities/user.entity'; import { CreateRfaDto } from './dto/create-rfa.dto'; import { SubmitRfaDto } from './dto/submit-rfa.dto'; +import { SearchRfaDto } from './dto/search-rfa.dto'; import { RfaService } from './rfa.service'; 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 { JwtAuthGuard } from '../../common/guards/jwt-auth.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)') @ApiBearerAuth() @UseGuards(JwtAuthGuard, RbacGuard) @Controller('rfas') export class RfaController { - constructor(private readonly rfaService: RfaService) {} + constructor( + private readonly rfaService: RfaService, + private readonly projectService: ProjectService + ) {} @Post() @ApiOperation({ summary: 'Create new RFA (Draft)' }) @@ -47,24 +52,26 @@ export class RfaController { return this.rfaService.create(createDto, user); } - @Post(':id/submit') + @Post(':uuid/submit') @ApiOperation({ summary: 'Submit RFA to Workflow' }) - @ApiParam({ name: 'id', description: 'RFA ID' }) + @ApiParam({ name: 'uuid', description: 'RFA UUID (from correspondences.uuid)' }) @ApiBody({ type: SubmitRfaDto }) @ApiResponse({ status: 200, description: 'RFA submitted successfully' }) @RequirePermission('rfa.create') @Audit('rfa.submit', 'rfa') - submit( - @Param('id', ParseIntPipe) id: number, + async submit( + @Param('uuid', ParseUuidPipe) uuid: string, @Body() submitDto: SubmitRfaDto, @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)' }) - @ApiParam({ name: 'id', description: 'RFA ID' }) + @ApiParam({ name: 'uuid', description: 'RFA UUID (from correspondences.uuid)' }) @ApiBody({ type: WorkflowActionDto }) @ApiResponse({ status: 200, @@ -72,28 +79,33 @@ export class RfaController { }) @RequirePermission('workflow.action_review') @Audit('rfa.action', 'rfa') - processAction( - @Param('id', ParseIntPipe) id: number, + async processAction( + @Param('uuid', ParseUuidPipe) uuid: string, @Body() actionDto: WorkflowActionDto, @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() @ApiOperation({ summary: 'List all RFAs with pagination' }) @ApiResponse({ status: 200, description: 'List of RFAs' }) @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); } - @Get(':id') + @Get(':uuid') @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' }) @RequirePermission('document.view') - findOne(@Param('id', ParseIntPipe) id: number) { - return this.rfaService.findOne(id); + findOne(@Param('uuid', ParseUuidPipe) uuid: string) { + return this.rfaService.findOneByUuid(uuid); } } diff --git a/backend/src/modules/rfa/rfa.module.ts b/backend/src/modules/rfa/rfa.module.ts index caa987c..13e9229 100644 --- a/backend/src/modules/rfa/rfa.module.ts +++ b/backend/src/modules/rfa/rfa.module.ts @@ -29,6 +29,7 @@ import { RfaService } from './rfa.service'; // External Modules import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; import { NotificationModule } from '../notification/notification.module'; +import { ProjectModule } from '../project/project.module'; import { SearchModule } from '../search/search.module'; import { UserModule } from '../user/user.module'; import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'; @@ -56,6 +57,7 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module' ]), DocumentNumberingModule, UserModule, + ProjectModule, SearchModule, WorkflowEngineModule, NotificationModule, diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index 87ffc06..b5d41a9 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -32,6 +32,17 @@ import { Rfa } from './entities/rfa.entity'; // DTOs import { WorkflowActionDto } from '../correspondence/dto/workflow-action.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 import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface'; @@ -272,6 +283,7 @@ export class RfaService { this.searchService .indexDocument({ id: savedCorr.id, + uuid: savedCorr.uuid, // ADR-019: index UUID for search type: 'rfa', docNumber: docNumber.number, title: createDto.subject, @@ -298,13 +310,19 @@ export class RfaService { // ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ... - async findAll(query: any) { - const { page = 1, limit = 20, projectId, status, search } = query; + async findAll(query: SearchRfaDto) { + const { + page = 1, + limit = 20, + projectId, + search, + revisionStatus = 'CURRENT', + statusCode, + } = query; const skip = (page - 1) * limit; // Fix: Start query from Rfa entity instead of Correspondence, // because Correspondence has no 'rfas' relation. - // [Force Rebuild] const queryBuilder = this.rfaRepo .createQueryBuilder('rfa') .leftJoinAndSelect('rfa.correspondence', 'corr') @@ -318,11 +336,9 @@ export class RfaService { .leftJoinAndSelect('sdRev.attachments', 'attachments'); // Filter by Revision Status (from query param 'revisionStatus') - const revStatus = query.revisionStatus || 'CURRENT'; - - if (revStatus === 'CURRENT') { + if (revisionStatus === 'CURRENT') { queryBuilder.where('corrRev.isCurrent = :isCurrent', { isCurrent: true }); - } else if (revStatus === 'OLD') { + } else if (revisionStatus === 'OLD') { queryBuilder.where('corrRev.isCurrent = :isCurrent', { isCurrent: false, }); @@ -333,8 +349,8 @@ export class RfaService { queryBuilder.andWhere('corr.projectId = :projectId', { projectId }); } - if (status) { - queryBuilder.andWhere('status.statusCode = :status', { status }); + if (statusCode) { + queryBuilder.andWhere('status.statusCode = :statusCode', { statusCode }); } if (search) { @@ -355,15 +371,18 @@ export class RfaService { ); // Map `revisions` property back to the expected payload for the frontend - const mappedItems = items.map((rfa) => { - const mappedRfa = { ...rfa } as any; - mappedRfa.revisions = - rfa.correspondence?.revisions?.map((cr) => ({ + const mappedItems: RfaMapped[] = items.map((rfa) => { + const revisions = + (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; + return { + ...rfa, + uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level + revisions: revisions.map((cr) => ({ ...cr, - ...(cr.rfaRevision || {}), - id: cr.rfaRevision?.id || cr.id, - })) || []; - return mappedRfa; + ...(cr.rfaRevision ?? {}), + id: cr.rfaRevision?.id ?? cr.id, + })) as CorrRevWithRfa[], + }; }); 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) { const rfa = await this.rfaRepo.findOne({ where: { id }, @@ -405,22 +450,26 @@ export class RfaService { } // Map to structure expected by frontend DTO - const mappedRfa = { ...rfa } as any; - mappedRfa.revisions = - rfa.correspondence?.revisions?.map((cr) => ({ + const revisions = + (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; + const mappedRfa: RfaMapped = { + ...rfa, + uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level + revisions: revisions.map((cr) => ({ ...cr, - ...(cr.rfaRevision || {}), - id: cr.rfaRevision?.id || cr.id, - })) || []; + ...(cr.rfaRevision ?? {}), + id: cr.rfaRevision?.id ?? cr.id, + })) as CorrRevWithRfa[], + }; return mappedRfa; } async submit(rfaId: number, templateId: number, user: User) { const rfa = await this.findOne(rfaId, true); - const currentCorrRev = rfa.correspondence?.revisions?.find( - (r: any) => r.isCurrent - ); + const corrRevisions = + (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; + const currentCorrRev = corrRevisions.find((r) => r.isCurrent); if (!currentCorrRev || !currentCorrRev.rfaRevision) throw new NotFoundException('Current revision not found'); @@ -512,9 +561,9 @@ export class RfaService { async processAction(rfaId: number, dto: WorkflowActionDto, user: User) { // Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB const rfa = await this.findOne(rfaId, true); - const currentCorrRev = rfa.correspondence?.revisions?.find( - (r: any) => r.isCurrent - ); + const corrRevisions = + (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; + const currentCorrRev = corrRevisions.find((r) => r.isCurrent); if (!currentCorrRev || !currentCorrRev.rfaRevision) throw new NotFoundException('Current revision not found'); diff --git a/backend/src/modules/search/search.service.ts b/backend/src/modules/search/search.service.ts index 162ec93..343bbcb 100644 --- a/backend/src/modules/search/search.service.ts +++ b/backend/src/modules/search/search.service.ts @@ -10,7 +10,7 @@ export class SearchService implements OnModuleInit { constructor( private readonly esService: ElasticsearchService, - private readonly configService: ConfigService, + private readonly configService: ConfigService ) {} async onModuleInit() { @@ -34,6 +34,7 @@ export class SearchService implements OnModuleInit { mappings: { properties: { id: { type: 'integer' }, + uuid: { type: 'keyword' }, // ADR-019: public identifier type: { type: 'keyword' }, // correspondence, rfa, drawing docNumber: { type: 'text' }, title: { type: 'text', analyzer: 'standard' }, @@ -60,12 +61,12 @@ export class SearchService implements OnModuleInit { try { return await this.esService.index({ 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' ในบางเวอร์ชัน }); } catch (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) { this.logger.error( - `Failed to remove document: ${(error as Error).message}`, + `Failed to remove document: ${(error as Error).message}` ); } } diff --git a/backend/src/modules/transmittal/dto/search-transmittal.dto.ts b/backend/src/modules/transmittal/dto/search-transmittal.dto.ts index 1973de8..aea7448 100644 --- a/backend/src/modules/transmittal/dto/search-transmittal.dto.ts +++ b/backend/src/modules/transmittal/dto/search-transmittal.dto.ts @@ -3,16 +3,18 @@ import { IsOptional, IsString, IsEnum, - IsNotEmpty, + IsUUID, } from 'class-validator'; import { Type } from 'class-transformer'; import { TransmittalPurpose } from './create-transmittal.dto'; export class SearchTransmittalDto { - @IsInt() - @Type(() => Number) - @IsNotEmpty() - projectId!: number; // บังคับระบุ Project + @IsUUID('all') + @IsOptional() + projectUuid?: string; // ADR-019: Public UUID of the project + + /** @internal Resolved INT ID — set by controller, do NOT expose in API */ + projectId?: number; @IsEnum(TransmittalPurpose) @IsOptional() diff --git a/backend/src/modules/transmittal/transmittal.controller.ts b/backend/src/modules/transmittal/transmittal.controller.ts index 03969ec..24c1566 100644 --- a/backend/src/modules/transmittal/transmittal.controller.ts +++ b/backend/src/modules/transmittal/transmittal.controller.ts @@ -5,39 +5,59 @@ import { Body, Param, UseGuards, - ParseIntPipe, Query, } from '@nestjs/common'; import { TransmittalService } from './transmittal.service'; import { CreateTransmittalDto } from './dto/create-transmittal.dto'; +import { SearchTransmittalDto } from './dto/search-transmittal.dto'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { User } from '../user/entities/user.entity'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; -import { 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') @ApiBearerAuth() -@UseGuards(JwtAuthGuard) +@UseGuards(JwtAuthGuard, RbacGuard) @Controller('transmittals') export class TransmittalController { - constructor(private readonly transmittalService: TransmittalService) {} + constructor( + private readonly transmittalService: TransmittalService, + private readonly projectService: ProjectService + ) {} @Post() @ApiOperation({ summary: 'Create a new Transmittal' }) + @RequirePermission('document.create') create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) { return this.transmittalService.create(createDto, user); } @Get() @ApiOperation({ summary: 'Search Transmittals' }) - findAll(@Query() searchDto: any) { - // Using any for simplicity as I can't import SearchTransmittalDto easily without checking its export + @RequirePermission('document.view') + 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); } - @Get(':id') + @Get(':uuid') @ApiOperation({ summary: 'Get Transmittal details' }) - findOne(@Param('id', ParseIntPipe) id: number) { - return this.transmittalService.findOne(id); + @ApiParam({ name: 'uuid', description: 'Transmittal UUID (from correspondences.uuid)' }) + @RequirePermission('document.view') + findOne(@Param('uuid', ParseUuidPipe) uuid: string) { + return this.transmittalService.findOneByUuid(uuid); } } diff --git a/backend/src/modules/transmittal/transmittal.module.ts b/backend/src/modules/transmittal/transmittal.module.ts index 16e1eae..3013e3d 100644 --- a/backend/src/modules/transmittal/transmittal.module.ts +++ b/backend/src/modules/transmittal/transmittal.module.ts @@ -8,6 +8,7 @@ import { CorrespondenceStatus } from '../correspondence/entities/correspondence- import { TransmittalService } from './transmittal.service'; import { TransmittalController } from './transmittal.controller'; import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; +import { ProjectModule } from '../project/project.module'; import { UserModule } from '../user/user.module'; import { SearchModule } from '../search/search.module'; @@ -21,6 +22,7 @@ import { SearchModule } from '../search/search.module'; CorrespondenceStatus, ]), DocumentNumberingModule, + ProjectModule, UserModule, SearchModule, ], diff --git a/backend/src/modules/transmittal/transmittal.service.ts b/backend/src/modules/transmittal/transmittal.service.ts index e3f629c..100bf22 100644 --- a/backend/src/modules/transmittal/transmittal.service.ts +++ b/backend/src/modules/transmittal/transmittal.service.ts @@ -9,7 +9,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { Transmittal } from './entities/transmittal.entity'; import { TransmittalItem } from './entities/transmittal-item.entity'; -import { CreateTransmittalDto } from './dto/create-transmittal.dto'; +import { + CreateTransmittalDto, + TransmittalItemDto, +} from './dto/create-transmittal.dto'; +import { SearchTransmittalDto } from './dto/search-transmittal.dto'; import { User } from '../user/entities/user.entity'; import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service'; import { Correspondence } from '../correspondence/entities/correspondence.entity'; @@ -125,14 +129,7 @@ export class TransmittalService { // 6. Create Items if (createDto.items && createDto.items.length > 0) { - // Filter only items that are effectively correspondences (or mapped as such) - // 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) => + const items = createDto.items.map((item: TransmittalItemDto) => queryRunner.manager.create(TransmittalItem, { transmittalId: savedCorr.id, 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) { const transmittal = await this.transmittalRepo.findOne({ where: { correspondenceId: id }, @@ -170,9 +182,9 @@ export class TransmittalService { return transmittal; } - async findAll(query: any) { + async findAll(query: SearchTransmittalDto) { const { page = 1, limit = 20, projectId, search } = query; - const skip = (page - 1) * limit; + const skip = ((page ?? 1) - 1) * (limit ?? 20); const queryBuilder = this.transmittalRepo .createQueryBuilder('transmittal') @@ -205,8 +217,14 @@ export class TransmittalService { .take(limit) .getManyAndCount(); + // ADR-019: Map correspondence.uuid to top level for frontend convenience + const mappedItems = items.map((t) => ({ + ...t, + uuid: t.correspondence?.uuid, + })); + return { - data: items, + data: mappedItems, meta: { total, page, diff --git a/frontend/app/(admin)/admin/doc-control/numbering/page.tsx b/frontend/app/(admin)/admin/doc-control/numbering/page.tsx index a1fb090..2ebac88 100644 --- a/frontend/app/(admin)/admin/doc-control/numbering/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/numbering/page.tsx @@ -31,7 +31,7 @@ export default function NumberingPage() { useEffect(() => { if (projects.length > 0 && !selectedProjectId) { const first = projects[0] as any; - setSelectedProjectId(String(first.id || first.uuid)); + setSelectedProjectId(String(first.id ?? first.uuid)); } }, [projects, selectedProjectId]); @@ -41,7 +41,7 @@ export default function NumberingPage() { const [isTesting, setIsTesting] = useState(false); const [testTemplate, setTestTemplate] = useState(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'; // Master Data @@ -109,7 +109,7 @@ export default function NumberingPage() { {(projects as any[]).map((project) => ( - + {project.projectCode} - {project.projectName} ))} @@ -137,7 +137,7 @@ export default function NumberingPage() {
{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) => (
diff --git a/frontend/app/(dashboard)/rfas/[id]/page.tsx b/frontend/app/(dashboard)/rfas/[uuid]/page.tsx similarity index 84% rename from frontend/app/(dashboard)/rfas/[id]/page.tsx rename to frontend/app/(dashboard)/rfas/[uuid]/page.tsx index 37fb1c8..b876ea2 100644 --- a/frontend/app/(dashboard)/rfas/[id]/page.tsx +++ b/frontend/app/(dashboard)/rfas/[uuid]/page.tsx @@ -6,11 +6,11 @@ import { useRFA } from "@/hooks/use-rfa"; import { Loader2 } from "lucide-react"; 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) { return ( diff --git a/frontend/app/(dashboard)/transmittals/[id]/page.tsx b/frontend/app/(dashboard)/transmittals/[uuid]/page.tsx similarity index 96% rename from frontend/app/(dashboard)/transmittals/[id]/page.tsx rename to frontend/app/(dashboard)/transmittals/[uuid]/page.tsx index 9b6f29d..91ae960 100644 --- a/frontend/app/(dashboard)/transmittals/[id]/page.tsx +++ b/frontend/app/(dashboard)/transmittals/[uuid]/page.tsx @@ -22,12 +22,12 @@ import { toast } from "sonner"; export default function TransmittalDetailPage() { const params = useParams(); - const id = params.id as string; + const uuid = params.uuid as string; const { data: transmittal, isLoading, error } = useQuery({ - queryKey: ["transmittal", id], - queryFn: () => transmittalService.getById(id), - enabled: !!id, + queryKey: ["transmittal", uuid], + queryFn: () => transmittalService.getByUuid(uuid), + enabled: !!uuid, }); const handlePrint = () => { @@ -100,7 +100,7 @@ export default function TransmittalDetailPage() {

Generated From

{transmittal.correspondence ? ( {transmittal.correspondence.correspondence_number} diff --git a/frontend/app/(dashboard)/transmittals/page.tsx b/frontend/app/(dashboard)/transmittals/page.tsx index dfbe9ff..6105ad8 100644 --- a/frontend/app/(dashboard)/transmittals/page.tsx +++ b/frontend/app/(dashboard)/transmittals/page.tsx @@ -1,22 +1,41 @@ "use client"; +import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { TransmittalList } from "@/components/transmittal/transmittal-list"; import { transmittalService } from "@/lib/services/transmittal.service"; +import { projectService } from "@/lib/services/project.service"; import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Plus, RefreshCw } from "lucide-react"; import Link from "next/link"; import { TransmittalListResponse } from "@/types/transmittal"; export default function TransmittalPage() { + // ADR-019: Dynamic project selection via UUID + const [selectedProjectUuid, setSelectedProjectUuid] = useState(""); + + const { data: projectsData } = useQuery({ + queryKey: ["projects-for-transmittals"], + queryFn: () => projectService.getAll(), + }); + const projects = projectsData?.data || projectsData || []; + const { data, isLoading, error, refetch, } = useQuery({ - queryKey: ["transmittals"], - queryFn: () => transmittalService.getAll({ projectId: 1 }), + queryKey: ["transmittals", selectedProjectUuid], + queryFn: () => transmittalService.getAll({ projectId: selectedProjectUuid }), + enabled: !!selectedProjectUuid, }); return ( @@ -47,6 +66,23 @@ export default function TransmittalPage() {
+ {/* ADR-019: Project filter */} +
+ Project: + +
+ {error && (
Failed to load transmittals. diff --git a/frontend/components/numbering/template-tester.tsx b/frontend/components/numbering/template-tester.tsx index 0e4900a..80e9d00 100644 --- a/frontend/components/numbering/template-tester.tsx +++ b/frontend/components/numbering/template-tester.tsx @@ -53,7 +53,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP const [loading, setLoading] = useState(false); // 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: correspondenceTypes } = useCorrespondenceTypes(); const { data: contracts } = useContracts(projectId); @@ -117,7 +117,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP {(organizations as Organization[])?.map((org) => ( - + {org.organizationCode} - {org.organizationName} ))} @@ -137,7 +137,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP {(organizations as Organization[])?.map((org) => ( - + {org.organizationCode} - {org.organizationName} ))} diff --git a/frontend/components/rfas/detail.tsx b/frontend/components/rfas/detail.tsx index ce6421d..8dce2a7 100644 --- a/frontend/components/rfas/detail.tsx +++ b/frontend/components/rfas/detail.tsx @@ -30,7 +30,7 @@ export function RFADetail({ data }: RFADetailProps) { processMutation.mutate( { - id: data.rfaId, + uuid: data.uuid, data: { action: apiAction, comments: comments, diff --git a/frontend/components/rfas/form.tsx b/frontend/components/rfas/form.tsx index d56dcb7..b710775 100644 --- a/frontend/components/rfas/form.tsx +++ b/frontend/components/rfas/form.tsx @@ -19,6 +19,7 @@ import { import { useRouter } from "next/navigation"; import { useCreateRFA } from "@/hooks/use-rfa"; import { useDisciplines, useContracts } from "@/hooks/use-master-data"; +import { useProjects } from "@/hooks/use-projects"; import { CreateRFADto } from "@/types/rfa"; import { useState, useEffect } from "react"; import { correspondenceService } from "@/lib/services/correspondence.service"; @@ -30,6 +31,7 @@ const rfaItemSchema = z.object({ unit: z.string().min(1, "Unit is required"), }); const rfaSchema = z.object({ + projectId: z.string().min(1, "Project is required"), // ADR-019: UUID contractId: z.string().min(1, "Contract is required"), disciplineId: z.number().min(1, "Discipline is required"), rfaTypeId: z.number().min(1, "Type is required"), @@ -49,9 +51,9 @@ export function RFAForm() { const router = useRouter(); const createMutation = useCreateRFA(); - // Dynamic Contract Loading (Default Project Context: 1) - const currentProjectId = 1; - const { data: contracts, isLoading: isLoadingContracts } = useContracts(currentProjectId); + // ADR-019: Dynamic project selection + const { data: projectsData, isLoading: isLoadingProjects } = useProjects(); + const projects = projectsData?.data || projectsData || []; const { register, @@ -63,6 +65,7 @@ export function RFAForm() { } = useForm({ resolver: zodResolver(rfaSchema), defaultValues: { + projectId: "", contractId: "", disciplineId: 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 { data: disciplines, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId); @@ -97,7 +103,7 @@ export function RFAForm() { const fetchPreview = async () => { try { const res = await correspondenceService.previewNumber({ - projectId: currentProjectId, + projectId: selectedProjectId, typeId: rfaTypeId, // RfaTypeId acts as TypeId disciplineId, // RFA uses 'TO' organization as recipient @@ -112,7 +118,7 @@ export function RFAForm() { const timer = setTimeout(fetchPreview, 500); return () => clearTimeout(timer); - }, [rfaTypeId, disciplineId, toOrganizationId, currentProjectId]); + }, [rfaTypeId, disciplineId, toOrganizationId, selectedProjectId]); const { fields, append, remove } = useFieldArray({ control, @@ -122,7 +128,7 @@ export function RFAForm() { const onSubmit = (data: RFAFormData) => { const payload: CreateRFADto = { ...data, - projectId: currentProjectId, + // ADR-019: projectId is already a UUID string from the form }; createMutation.mutate(payload as any, { onSuccess: () => { @@ -178,19 +184,45 @@ export function RFAForm() {
+ {/* ADR-019: Project selector */} +
+ + + {errors.projectId && ( +

{errors.projectId.message}

+ )} +
+
+ + + + + + + {(Array.isArray(projectsList) ? projectsList : []).map((p: { uuid: string; projectName?: string; projectCode?: string }) => ( + + {p.projectName || p.projectCode} + + ))} + + + + + )} + /> + + ( + + Recipient Organization + + + + )} + /> +
+
{/* Linked Correspondence (Ref No) */} { const item = row.original; return ( - + diff --git a/frontend/hooks/use-rfa.ts b/frontend/hooks/use-rfa.ts index 74fd703..5244d87 100644 --- a/frontend/hooks/use-rfa.ts +++ b/frontend/hooks/use-rfa.ts @@ -11,7 +11,7 @@ export const rfaKeys = { lists: () => [...rfaKeys.all, 'list'] as const, list: (params: SearchRfaDto) => [...rfaKeys.lists(), params] 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 --- @@ -24,11 +24,11 @@ export function useRFAs(params: SearchRfaDto) { }); } -export function useRFA(id: number | string) { +export function useRFA(uuid: string) { return useQuery({ - queryKey: rfaKeys.detail(id), - queryFn: () => rfaService.getById(id), - enabled: !!id, + queryKey: rfaKeys.detail(uuid), + queryFn: () => rfaService.getByUuid(uuid), + enabled: !!uuid, }); } @@ -55,11 +55,11 @@ export function useUpdateRFA() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, data }: { id: number | string; data: UpdateRfaDto }) => - rfaService.update(id, data), - onSuccess: (_, { id }) => { + mutationFn: ({ uuid, data }: { uuid: string; data: UpdateRfaDto }) => + rfaService.update(uuid, data), + onSuccess: (_, { uuid }) => { toast.success('RFA updated successfully'); - queryClient.invalidateQueries({ queryKey: rfaKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: rfaKeys.detail(uuid) }); queryClient.invalidateQueries({ queryKey: rfaKeys.lists() }); }, onError: (error: unknown) => { @@ -74,11 +74,11 @@ export function useProcessRFA() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, data }: { id: number | string; data: WorkflowActionDto }) => - rfaService.processWorkflow(id, data), - onSuccess: (_, { id }) => { + mutationFn: ({ uuid, data }: { uuid: string; data: WorkflowActionDto }) => + rfaService.processWorkflow(uuid, data), + onSuccess: (_, { uuid }) => { toast.success('Workflow status updated successfully'); - queryClient.invalidateQueries({ queryKey: rfaKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: rfaKeys.detail(uuid) }); queryClient.invalidateQueries({ queryKey: rfaKeys.lists() }); }, onError: (error: unknown) => { diff --git a/frontend/lib/services/rfa.service.ts b/frontend/lib/services/rfa.service.ts index 6be56fd..d075ed5 100644 --- a/frontend/lib/services/rfa.service.ts +++ b/frontend/lib/services/rfa.service.ts @@ -1,9 +1,9 @@ // File: lib/services/rfa.service.ts import apiClient from "@/lib/api/client"; -import { - CreateRfaDto, - UpdateRfaDto, - SearchRfaDto +import { + CreateRfaDto, + UpdateRfaDto, + SearchRfaDto } from "@/types/dto/rfa/rfa.dto"; // DTO สำหรับการอนุมัติ (อาจจะย้ายไปไว้ใน folder dto/rfa/ ก็ได้ในอนาคต) @@ -26,9 +26,9 @@ export const rfaService = { /** * ดึงรายละเอียด RFA และประวัติ Workflow */ - getById: async (id: string | number) => { - // GET /rfas/:id - const response = await apiClient.get(`/rfas/${id}`); + getByUuid: async (uuid: string) => { + // GET /rfas/:uuid (ADR-019) + const response = await apiClient.get(`/rfas/${uuid}`); return response.data; }, @@ -44,26 +44,27 @@ export const rfaService = { /** * แก้ไข RFA (เฉพาะสถานะ Draft) */ - update: async (id: string | number, data: UpdateRfaDto) => { - // PUT /rfas/:id - const response = await apiClient.put(`/rfas/${id}`, data); + update: async (uuid: string, data: UpdateRfaDto) => { + // PUT /rfas/:uuid (ADR-019) + const response = await apiClient.put(`/rfas/${uuid}`, data); return response.data; }, /** * ดำเนินการ Workflow (อนุมัติ / ตีกลับ / ส่งต่อ) */ - processWorkflow: async (id: string | number, actionData: WorkflowActionDto) => { - // POST /rfas/:id/workflow - const response = await apiClient.post(`/rfas/${id}/workflow`, actionData); + processWorkflow: async (uuid: string, actionData: WorkflowActionDto) => { + // POST /rfas/:uuid/workflow (ADR-019) + const response = await apiClient.post(`/rfas/${uuid}/workflow`, actionData); return response.data; }, /** * (Optional) ลบ RFA (Soft Delete) */ - delete: async (id: string | number) => { - const response = await apiClient.delete(`/rfas/${id}`); + delete: async (uuid: string) => { + // DELETE /rfas/:uuid (ADR-019) + const response = await apiClient.delete(`/rfas/${uuid}`); return response.data; } -}; \ No newline at end of file +}; diff --git a/frontend/lib/services/transmittal.service.ts b/frontend/lib/services/transmittal.service.ts index 87c6396..32a5e11 100644 --- a/frontend/lib/services/transmittal.service.ts +++ b/frontend/lib/services/transmittal.service.ts @@ -1,9 +1,9 @@ // File: lib/services/transmittal.service.ts import apiClient from "@/lib/api/client"; -import { - CreateTransmittalDto, - UpdateTransmittalDto, - SearchTransmittalDto +import { + CreateTransmittalDto, + UpdateTransmittalDto, + SearchTransmittalDto } from "@/types/dto/transmittal/transmittal.dto"; export const transmittalService = { @@ -17,11 +17,11 @@ export const transmittalService = { }, /** - * ดึงรายละเอียด Transmittal ตาม ID + * ดึงรายละเอียด Transmittal ตาม UUID (ADR-019) */ - getById: async (id: string | number) => { - // GET /transmittals/:id - const response = await apiClient.get(`/transmittals/${id}`); + getByUuid: async (uuid: string) => { + // GET /transmittals/:uuid + const response = await apiClient.get(`/transmittals/${uuid}`); return response.data; }, @@ -37,18 +37,18 @@ export const transmittalService = { /** * แก้ไขข้อมูล Transmittal (เฉพาะ Draft) */ - update: async (id: string | number, data: UpdateTransmittalDto) => { - // PUT /transmittals/:id - const response = await apiClient.put(`/transmittals/${id}`, data); + update: async (uuid: string, data: UpdateTransmittalDto) => { + // PUT /transmittals/:uuid (ADR-019) + const response = await apiClient.put(`/transmittals/${uuid}`, data); return response.data; }, /** * ลบเอกสาร (Soft Delete) */ - delete: async (id: string | number) => { - // DELETE /transmittals/:id - const response = await apiClient.delete(`/transmittals/${id}`); + delete: async (uuid: string) => { + // DELETE /transmittals/:uuid (ADR-019) + const response = await apiClient.delete(`/transmittals/${uuid}`); return response.data; } -}; \ No newline at end of file +}; diff --git a/frontend/package.json b/frontend/package.json index 5fdab8b..71c367a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "lcbp3-frontend", - "version": "1.5.1", + "version": "1.8.0", "private": true, "scripts": { "dev": "next dev", diff --git a/frontend/types/rfa.ts b/frontend/types/rfa.ts index b04aab0..af37349 100644 --- a/frontend/types/rfa.ts +++ b/frontend/types/rfa.ts @@ -8,7 +8,8 @@ export interface RFAItem { } 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; createdBy: number; disciplineId?: number; @@ -35,12 +36,14 @@ export interface RFA { }; // Shared Correspondence Relation correspondence?: { - id: number; + uuid: string; + id?: number; // Excluded from API responses (ADR-019) correspondenceNumber: string; projectId: number; originatorId?: number; createdAt?: string; project?: { + uuid: string; projectName: string; projectCode: string; }; diff --git a/frontend/types/transmittal.ts b/frontend/types/transmittal.ts index dcb2ba2..c1bdd36 100644 --- a/frontend/types/transmittal.ts +++ b/frontend/types/transmittal.ts @@ -28,8 +28,9 @@ export interface TransmittalItem { * Main Transmittal entity */ export interface Transmittal { - id: number; - correspondenceId: number; + uuid: string; // ADR-019: from correspondence.uuid + id?: number; // Excluded from API responses (ADR-019) + correspondenceId?: number | string; transmittalNo: string; subject: string; purpose?: TransmittalPurpose; @@ -38,7 +39,8 @@ export interface Transmittal { // Joined relations from API items?: TransmittalItem[]; correspondence?: { - id: number; + uuid: string; + id?: number; // Excluded from API responses (ADR-019) correspondence_number: string; project_id: number; }; @@ -70,9 +72,9 @@ export interface CreateTransmittalItemDto { * DTO for creating a transmittal */ export interface CreateTransmittalDto { - projectId?: number; - recipientOrganizationId?: number; - correspondenceId: number; + projectId?: number | string; // ADR-019: Accept UUID + recipientOrganizationId?: number | string; // ADR-019: Accept UUID + correspondenceId: number | string; // ADR-019: Accept UUID subject: string; purpose?: TransmittalPurpose; remarks?: string; @@ -85,6 +87,6 @@ export interface CreateTransmittalDto { export interface SearchTransmittalDto { page?: number; limit?: number; - projectId?: number; + projectId?: number | string; // ADR-019: Accept UUID search?: string; } diff --git a/lcbp3.code-workspace b/lcbp3.code-workspace index 2bdb0c5..f63085d 100644 --- a/lcbp3.code-workspace +++ b/lcbp3.code-workspace @@ -928,6 +928,6 @@ ], }, "extensions": { - "recommendations": ["jlcodes.antigravity-cockpit"], + "recommendations": [], }, }