690403:1632 fix dashboard cir and trans
CI / CD Pipeline / build (push) Successful in 5m0s
CI / CD Pipeline / deploy (push) Successful in 8m34s

This commit is contained in:
2026-04-03 16:32:15 +07:00
parent d4f0d02c62
commit 9c835ec4ac
4 changed files with 218 additions and 58 deletions
@@ -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<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 5,
statusCode: 'DRAFT',
});
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
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>(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<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 5,
statusCode: 'DRAFT',
});
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
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<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 2,
typeCode: 'OLD-TYPE',
});
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
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(
@@ -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<string, unknown> = {};
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);
@@ -56,8 +56,8 @@ export default function TransmittalPage() {
</SelectTrigger>
<SelectContent>
{(Array.isArray(projects) ? projects : []).map(
(p: { uuid: string; projectName?: string; projectCode?: string }) => (
<SelectItem key={p.uuid} value={p.uuid}>
(p: { publicId: string; projectName?: string; projectCode?: string }) => (
<SelectItem key={p.publicId} value={p.publicId}>
{p.projectName || p.projectCode}
</SelectItem>
)
@@ -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 (เอกสารนำส่ง)
-- =====================================================