From 9c835ec4ac5ef869d65ecb62053e2661ecb2ad78 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 3 Apr 2026 16:32:15 +0700 Subject: [PATCH] 690403:1632 fix dashboard cir and trans --- .../correspondence.service.spec.ts | 165 ++++++++++++------ .../correspondence/correspondence.service.ts | 82 ++++++++- .../app/(dashboard)/transmittals/page.tsx | 4 +- .../lcbp3-v1.8.0-schema-02-tables.sql | 25 +++ 4 files changed, 218 insertions(+), 58 deletions(-) diff --git a/backend/src/modules/correspondence/correspondence.service.spec.ts b/backend/src/modules/correspondence/correspondence.service.spec.ts index f9442a7..acb90cb 100644 --- a/backend/src/modules/correspondence/correspondence.service.spec.ts +++ b/backend/src/modules/correspondence/correspondence.service.spec.ts @@ -184,11 +184,22 @@ describe('CorrespondenceService', () => { user_id: 1, primaryOrganizationId: 10, } as unknown as User; + + const mockCorr = { + id: 1, + publicId: 'corr-uuid-1', + correspondenceNumber: 'CORR-001', + projectId: 1, + createdAt: new Date(), + recipients: [], + }; + const mockRevision = { id: 100, correspondenceId: 1, isCurrent: true, statusId: 23, + correspondence: mockCorr, }; jest @@ -209,11 +220,7 @@ describe('CorrespondenceService', () => { ]); jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue({ - id: 1, - publicId: 'corr-uuid-1', - correspondenceNumber: 'CORR-001', - projectId: 1, - createdAt: new Date(), + ...mockCorr, revisions: [], } as unknown as Correspondence); @@ -258,16 +265,6 @@ describe('CorrespondenceService', () => { it('should NOT regenerate number if critical fields unchanged', async () => { const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User; - const mockRevision = { - id: 100, - correspondenceId: 1, - isCurrent: true, - statusId: 5, - }; - - jest - .spyOn(revisionRepo, 'findOne') - .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); const mockCorr = { id: 1, @@ -278,6 +275,18 @@ describe('CorrespondenceService', () => { correspondenceNumber: 'OLD-NUM', recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], }; + const mockRevision = { + id: 100, + correspondenceId: 1, + isCurrent: true, + statusId: 5, + correspondence: mockCorr, + }; + + jest + .spyOn(revisionRepo, 'findOne') + .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); + jest .spyOn(correspondenceRepo, 'findOne') .mockResolvedValue(mockCorr as unknown as Correspondence); @@ -296,16 +305,6 @@ describe('CorrespondenceService', () => { it('should regenerate number if Project ID changes', async () => { const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User; - const mockRevision = { - id: 100, - correspondenceId: 1, - isCurrent: true, - statusId: 5, - }; - jest - .spyOn(revisionRepo, 'findOne') - .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); - const mockCorr = { id: 1, projectId: 1, @@ -315,9 +314,32 @@ describe('CorrespondenceService', () => { correspondenceNumber: 'OLD-NUM', recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], }; + const mockRevision = { + id: 100, + correspondenceId: 1, + isCurrent: true, + statusId: 5, + correspondence: mockCorr, + }; jest - .spyOn(correspondenceRepo, 'findOne') - .mockResolvedValue(mockCorr as unknown as Correspondence); + .spyOn(revisionRepo, 'findOne') + .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); + + const statusRepo = testingModule.get>( + getRepositoryToken(CorrespondenceStatus) + ); + (statusRepo.findOne as jest.Mock).mockResolvedValue({ + id: 5, + statusCode: 'DRAFT', + }); + + const typeRepo = testingModule.get>( + getRepositoryToken(CorrespondenceType) + ); + (typeRepo.findOne as jest.Mock).mockResolvedValue({ + id: 2, + typeCode: 'OLD-TYPE', + }); const updateDto: UpdateCorrespondenceDto = { projectId: 2, @@ -327,6 +349,11 @@ describe('CorrespondenceService', () => { testingModule.get(UuidResolverService); (uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(2); + jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue({ + ...mockCorr, + projectId: 2, + } as unknown as Correspondence); + await service.update(1, updateDto, mockUser); expect( @@ -336,16 +363,6 @@ describe('CorrespondenceService', () => { it('should regenerate number if Document Type changes', async () => { const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User; - const mockRevision = { - id: 100, - correspondenceId: 1, - isCurrent: true, - statusId: 5, - }; - jest - .spyOn(revisionRepo, 'findOne') - .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); - const mockCorr = { id: 1, projectId: 1, @@ -355,13 +372,24 @@ describe('CorrespondenceService', () => { correspondenceNumber: 'OLD-NUM', recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], }; - jest - .spyOn(correspondenceRepo, 'findOne') - .mockResolvedValue(mockCorr as unknown as Correspondence); - - const updateDto: UpdateCorrespondenceDto = { - typeId: 999, + const mockRevision = { + id: 100, + correspondenceId: 1, + isCurrent: true, + statusId: 5, + correspondence: mockCorr, }; + jest + .spyOn(revisionRepo, 'findOne') + .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); + + const statusRepo = testingModule.get>( + getRepositoryToken(CorrespondenceStatus) + ); + (statusRepo.findOne as jest.Mock).mockResolvedValue({ + id: 5, + statusCode: 'DRAFT', + }); const typeRepo = testingModule.get>( getRepositoryToken(CorrespondenceType) @@ -371,6 +399,15 @@ describe('CorrespondenceService', () => { typeCode: 'NEW-TYPE', }); + const updateDto: UpdateCorrespondenceDto = { + typeId: 999, + }; + + jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue({ + ...mockCorr, + correspondenceTypeId: 999, + } as unknown as Correspondence); + await service.update(1, updateDto, mockUser); expect( @@ -380,16 +417,6 @@ describe('CorrespondenceService', () => { it('should regenerate number if Recipient Organization changes', async () => { const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User; - const mockRevision = { - id: 100, - correspondenceId: 1, - isCurrent: true, - statusId: 5, - }; - jest - .spyOn(revisionRepo, 'findOne') - .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); - const mockCorr = { id: 1, projectId: 1, @@ -399,9 +426,32 @@ describe('CorrespondenceService', () => { correspondenceNumber: 'OLD-NUM', recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], }; + const mockRevision = { + id: 100, + correspondenceId: 1, + isCurrent: true, + statusId: 5, + correspondence: mockCorr, + }; jest - .spyOn(correspondenceRepo, 'findOne') - .mockResolvedValue(mockCorr as unknown as Correspondence); + .spyOn(revisionRepo, 'findOne') + .mockResolvedValue(mockRevision as unknown as CorrespondenceRevision); + + const typeRepo = testingModule.get>( + getRepositoryToken(CorrespondenceType) + ); + (typeRepo.findOne as jest.Mock).mockResolvedValue({ + id: 2, + typeCode: 'OLD-TYPE', + }); + + const statusRepo = testingModule.get>( + getRepositoryToken(CorrespondenceStatus) + ); + (statusRepo.findOne as jest.Mock).mockResolvedValue({ + id: 5, + statusCode: 'DRAFT', + }); // Access DataSource manager for mocking mockDataSource.manager.findOne.mockResolvedValue({ @@ -413,6 +463,11 @@ describe('CorrespondenceService', () => { recipients: [{ type: 'TO', organizationId: 88 }], }; + jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue({ + ...mockCorr, + recipients: [{ recipientType: 'TO', recipientOrganizationId: 88 }], + } as unknown as Correspondence); + await service.update(1, updateDto, mockUser); expect( diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 288b018..240e36d 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -696,14 +696,94 @@ export class CorrespondenceService { ) : undefined; - // 3. Update Correspondence Entity if needed + // 3. Check if number regeneration is needed (only for DRAFT status) + const oldCorr = revision.correspondence; + if (!oldCorr) { + throw new InternalServerErrorException( + 'Correspondence relation not loaded for revision' + ); + } + const oldRecipientOrgId = oldCorr.recipients?.find( + (r) => r.recipientType === 'TO' + )?.recipientOrganizationId; + const newRecipientOrgId = updResolvedRecipients?.find( + (r) => r.type === 'TO' + )?.organizationId; + + const needsNumberRegen = + (updResolvedProjectId && updResolvedProjectId !== oldCorr.projectId) || + (updateDto.typeId && updateDto.typeId !== oldCorr.correspondenceTypeId) || + (newRecipientOrgId && newRecipientOrgId !== oldRecipientOrgId); + + let newNumber: string | undefined; + if (needsNumberRegen) { + // Check if current status is DRAFT - only regenerate for drafts + const currentStatus = await this.statusRepo.findOne({ + where: { id: revision.statusId }, + }); + + if (currentStatus?.statusCode === 'DRAFT') { + // Resolve originator for number generation + const originatorId = + updResolvedOriginatorId || + oldCorr.originatorId || + user.primaryOrganizationId; + + // Get type info for number generation + const typeId = updateDto.typeId || oldCorr.correspondenceTypeId; + const type = await this.typeRepo.findOne({ where: { id: typeId } }); + + if (!type) { + throw new NotFoundException('Document Type not found'); + } + + // Get recipient org code for number generation + const recipientOrgId = newRecipientOrgId || oldRecipientOrgId; + let _recipientCode = ''; + if (recipientOrgId) { + const recOrg = await this.dataSource.manager.findOne(Organization, { + where: { id: recipientOrgId }, + }); + if (recOrg) _recipientCode = recOrg.organizationCode; + } + + const projectId = updResolvedProjectId || oldCorr.projectId; + + newNumber = await this.numberingService.updateNumberForDraft( + oldCorr.correspondenceNumber, + { + projectId: oldCorr.projectId, + originatorOrganizationId: + oldCorr.originatorId || user.primaryOrganizationId || 0, + typeId: oldCorr.correspondenceTypeId, + disciplineId: oldCorr.disciplineId, + recipientOrganizationId: oldRecipientOrgId || 0, + userId: user.user_id, + }, + { + projectId, + originatorOrganizationId: + originatorId || user.primaryOrganizationId || 0, + typeId, + disciplineId: updateDto.disciplineId || oldCorr.disciplineId, + recipientOrganizationId: recipientOrgId || 0, + userId: user.user_id, + } + ); + } + } + + // 4. Update Correspondence Entity if needed const correspondenceUpdate: Record = {}; + if (newNumber) correspondenceUpdate.correspondenceNumber = newNumber; if (updateDto.disciplineId) correspondenceUpdate.disciplineId = updateDto.disciplineId; if (updResolvedProjectId) correspondenceUpdate.projectId = updResolvedProjectId; if (updResolvedOriginatorId) correspondenceUpdate.originatorId = updResolvedOriginatorId; + if (updateDto.typeId) + correspondenceUpdate.correspondenceTypeId = updateDto.typeId; if (Object.keys(correspondenceUpdate).length > 0) { await this.correspondenceRepo.update(id, correspondenceUpdate); diff --git a/frontend/app/(dashboard)/transmittals/page.tsx b/frontend/app/(dashboard)/transmittals/page.tsx index 2e5ded7..fa957af 100644 --- a/frontend/app/(dashboard)/transmittals/page.tsx +++ b/frontend/app/(dashboard)/transmittals/page.tsx @@ -56,8 +56,8 @@ export default function TransmittalPage() { {(Array.isArray(projects) ? projects : []).map( - (p: { uuid: string; projectName?: string; projectCode?: string }) => ( - + (p: { publicId: string; projectName?: string; projectCode?: string }) => ( + {p.projectName || p.projectCode} ) diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql index d46b672..a6c1fd3 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql @@ -790,6 +790,31 @@ CREATE TABLE circulations ( UNIQUE INDEX idx_circulations_uuid (uuid) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน'; +-- ตารางเก็บ Routing/Workflow ของใบเวียน (ขั้นตอนการดำเนินงาน) +CREATE TABLE circulation_routings ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของรายการ routing', + circulation_id INT NOT NULL COMMENT 'ID ของใบเวียน', + step_number INT NOT NULL COMMENT 'ลำดับขั้นตอน', + organization_id INT NOT NULL COMMENT 'ID ขององค์กรในขั้นตอนนี้', + assigned_to INT NULL COMMENT 'ID ของผู้ใช้ที่ได้รับมอบหมาย (NULL = ยังไม่มอบหมาย)', + STATUS ENUM( + 'PENDING', + 'IN_PROGRESS', + 'COMPLETED', + 'REJECTED' + ) DEFAULT 'PENDING' COMMENT 'สถานะขั้นตอน', + comments TEXT NULL COMMENT 'ความคิดเห็น/หมายเหตุ', + completed_at DATETIME NULL COMMENT 'วันที่เสร็จสิ้นขั้นตอน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (circulation_id) REFERENCES circulations (id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id), + FOREIGN KEY (assigned_to) REFERENCES users (user_id) ON DELETE + SET NULL, + INDEX idx_circulation_routing_circulation (circulation_id), + INDEX idx_circulation_routing_status (STATUS) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บ Routing/Workflow ของใบเวียน'; + -- ===================================================== -- 7. 📤 Transmittals (เอกสารนำส่ง) -- =====================================================