From 82ecf2cb81280370c92544d6d8fb17ebbac92677 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 14:06:07 +0700 Subject: [PATCH] fix: save posMasterHistory null --- src/controllers/OrganizationController.ts | 585 +++++++++++----------- 1 file changed, 302 insertions(+), 283 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 51159b91..c7e69071 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7952,312 +7952,331 @@ export class OrganizationController extends Controller { if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง"); // Part 1: Differential sync of organization structure (bottom-up) // Build mapping incrementally as we process each level - const allMappings: AllOrgMappings = { - orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, - }; - // Track sync statistics for organization nodes - const orgSyncStats: Record = - {}; + if (orgRootCurrent) { + const allMappings: AllOrgMappings = { + orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, + }; - // Process from top (Root) to bottom (Child4) to handle foreign key constraints - // OrgRoot (sync first - no parent dependencies) - const orgRootResult = await this.syncOrgLevel( - queryRunner, - OrgRoot, - this.orgRootRepository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgRoot = orgRootResult.mapping; - orgSyncStats.orgRoot = orgRootResult.counts; + // Track sync statistics for organization nodes + const orgSyncStats: Record = + {}; - // Child1 (parent OrgRoot already synced) - const child1Result = await this.syncOrgLevel( - queryRunner, - OrgChild1, - this.child1Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild1 = child1Result.mapping; - orgSyncStats.orgChild1 = child1Result.counts; + // Process from top (Root) to bottom (Child4) to handle foreign key constraints + // OrgRoot (sync first - no parent dependencies) + const orgRootResult = await this.syncOrgLevel( + queryRunner, + OrgRoot, + this.orgRootRepository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgRoot = orgRootResult.mapping; + orgSyncStats.orgRoot = orgRootResult.counts; - // Child2 (parents OrgRoot and Child1 already synced) - const child2Result = await this.syncOrgLevel( - queryRunner, - OrgChild2, - this.child2Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild2 = child2Result.mapping; - orgSyncStats.orgChild2 = child2Result.counts; + // Child1 (parent OrgRoot already synced) + const child1Result = await this.syncOrgLevel( + queryRunner, + OrgChild1, + this.child1Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild1 = child1Result.mapping; + orgSyncStats.orgChild1 = child1Result.counts; - // Child3 (parents OrgRoot, Child1, Child2 already synced) - const child3Result = await this.syncOrgLevel( - queryRunner, - OrgChild3, - this.child3Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild3 = child3Result.mapping; - orgSyncStats.orgChild3 = child3Result.counts; + // Child2 (parents OrgRoot and Child1 already synced) + const child2Result = await this.syncOrgLevel( + queryRunner, + OrgChild2, + this.child2Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild2 = child2Result.mapping; + orgSyncStats.orgChild2 = child2Result.counts; - // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) - const child4Result = await this.syncOrgLevel( - queryRunner, - OrgChild4, - this.child4Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild4 = child4Result.mapping; - orgSyncStats.orgChild4 = child4Result.counts; + // Child3 (parents OrgRoot, Child1, Child2 already synced) + const child3Result = await this.syncOrgLevel( + queryRunner, + OrgChild3, + this.child3Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild3 = child3Result.mapping; + orgSyncStats.orgChild3 = child3Result.counts; - // Part 2: Sync position data using new org IDs from Part 1 - // 2.1 Clear current_holderId for affected positions (keep existing logic) - // Get draft organization IDs under the given rootDnaId to find positions to clear - const draftOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.keys()], - orgChild1: [...allMappings.orgChild1.byDraftId.keys()], - orgChild2: [...allMappings.orgChild2.byDraftId.keys()], - orgChild3: [...allMappings.orgChild3.byDraftId.keys()], - orgChild4: [...allMappings.orgChild4.byDraftId.keys()], - }; + // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) + const child4Result = await this.syncOrgLevel( + queryRunner, + OrgChild4, + this.child4Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild4 = child4Result.mapping; + orgSyncStats.orgChild4 = child4Result.counts; - // Get draft positions that belong to any org under the rootDnaId - const posMasterDraft = await this.posMasterRepository.find({ - where: [ - { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, - { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, - { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, - { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, - { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, - ], - }); + // Part 2: Sync position data using new org IDs from Part 1 + // 2.1 Clear current_holderId for affected positions (keep existing logic) + // Get draft organization IDs under the given rootDnaId to find positions to clear + const draftOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.keys()], + orgChild1: [...allMappings.orgChild1.byDraftId.keys()], + orgChild2: [...allMappings.orgChild2.byDraftId.keys()], + orgChild3: [...allMappings.orgChild3.byDraftId.keys()], + orgChild4: [...allMappings.orgChild4.byDraftId.keys()], + }; - if (posMasterDraft.length <= 0) - return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); - - // Clear current_holderId for positions that will have new holders - const nextHolderIds = posMasterDraft - .filter((x) => x.next_holderId != null) - .map((x) => x.next_holderId) as string[]; - - if (nextHolderIds.length > 0) { - // FIX: Fetch positions first before updating (to avoid race condition) - const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { - where: { - orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds), - }, + // Get draft positions that belong to any org under the rootDnaId + const posMasterDraft = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, + { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, + { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, + { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, + { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, + ], }); - // Save history BEFORE clearing current_holderId - const historyOps = posMastersToUpdate.map((pos) => ({ - posMasterDnaId: pos.ancestorDNA, - profileId: null, - pm: null, - })); - await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); + if (posMasterDraft.length <= 0) + return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); - // Now clear current_holderId - await queryRunner.manager.update( - PosMaster, - { - orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds), - }, - { current_holderId: null, isSit: false }, - ); - } + // Clear current_holderId for positions that will have new holders + const nextHolderIds = posMasterDraft + .filter((x) => x.next_holderId != null) + .map((x) => x.next_holderId) as string[]; - // 2.2 Fetch current positions for comparison - // Get current organization IDs from the mappings - const currentOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.values()], - orgChild1: [...allMappings.orgChild1.byDraftId.values()], - orgChild2: [...allMappings.orgChild2.byDraftId.values()], - orgChild3: [...allMappings.orgChild3.byDraftId.values()], - orgChild4: [...allMappings.orgChild4.byDraftId.values()], - }; - - const posMasterCurrent = await this.posMasterRepository.find({ - where: [ - { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, - { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, - { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, - { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, - { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, - ], - }); - - // Build lookup map - const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); - - // 2.3 Batch DELETE: positions in current but not in draft - const toDelete = posMasterCurrent.filter( - (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), - ); - - if (toDelete.length > 0) { - const toDeleteIds = toDelete.map((p) => p.id); - - // Cascade delete positions first - await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); - - // Then delete posMaster records - await queryRunner.manager.delete(PosMaster, toDeleteIds); - - const deleteHistoryOps = toDelete.map((pos) => ({ - posMasterDnaId: pos.ancestorDNA, - profileId: null, - pm: null, - })); - await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); - } - - // 2.4 Process draft positions (UPDATE or INSERT) - const toUpdate: PosMaster[] = []; - const toInsert: any[] = []; - - // Track draft PosMaster ID to current PosMaster ID mapping for position sync - // Type: Map - const posMasterMapping: Map = new Map(); - - for (const draftPos of posMasterDraft) { - const current = currentByDNA.get(draftPos.ancestorDNA); - - // Map organization IDs using new IDs from Part 1 - const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); - const orgChild1Id = this.resolveOrgId(draftPos.orgChild1Id ?? null, allMappings.orgChild1); - const orgChild2Id = this.resolveOrgId(draftPos.orgChild2Id ?? null, allMappings.orgChild2); - const orgChild3Id = this.resolveOrgId(draftPos.orgChild3Id ?? null, allMappings.orgChild3); - const orgChild4Id = this.resolveOrgId(draftPos.orgChild4Id ?? null, allMappings.orgChild4); - - if (current) { - // UPDATE existing position - Object.assign(current, { - createdAt: draftPos.createdAt, - createdUserId: draftPos.createdUserId, - createdFullName: draftPos.createdFullName, - lastUpdatedAt: new Date(), - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - posMasterNoPrefix: draftPos.posMasterNoPrefix, - posMasterNoSuffix: draftPos.posMasterNoSuffix, - posMasterNo: draftPos.posMasterNo, - posMasterOrder: draftPos.posMasterOrder, - orgRootId, - orgChild1Id, - orgChild2Id, - orgChild3Id, - orgChild4Id, - current_holderId: draftPos.next_holderId, - isSit: draftPos.isSit, - reason: draftPos.reason, - isDirector: draftPos.isDirector, - isStaff: draftPos.isStaff, - positionSign: draftPos.positionSign, - statusReport: "DONE", - isCondition: draftPos.isCondition, - conditionReason: draftPos.conditionReason, - }); - toUpdate.push(current); - - if (draftPos.next_holderId === null) { - await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); - } - - // Track mapping for position sync - posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); - } else { - // INSERT new position - const newPosMaster = queryRunner.manager.create(PosMaster, { - ...draftPos, - id: undefined, - orgRevisionId: currentRevisionId, - orgRootId, - orgChild1Id, - orgChild2Id, - orgChild3Id, - orgChild4Id, - current_holderId: draftPos.next_holderId, - statusReport: "DONE", + if (nextHolderIds.length > 0) { + // FIX: Fetch positions first before updating (to avoid race condition) + const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { + where: { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, }); - toInsert.push(newPosMaster); + // Save history BEFORE clearing current_holderId + const historyOps = posMastersToUpdate + .filter((x) => x.orgRootId != orgRootCurrent?.id) + .map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); + + // Now clear current_holderId + await queryRunner.manager.update( + PosMaster, + { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, + { current_holderId: null, isSit: false }, + ); } - } - // Batch save updates and inserts - if (toUpdate.length > 0) { - await queryRunner.manager.save(toUpdate); - } - if (toInsert.length > 0) { - const saved = await queryRunner.manager.save(toInsert); + // 2.2 Fetch current positions for comparison + // Get current organization IDs from the mappings + const currentOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.values()], + orgChild1: [...allMappings.orgChild1.byDraftId.values()], + orgChild2: [...allMappings.orgChild2.byDraftId.values()], + orgChild3: [...allMappings.orgChild3.byDraftId.values()], + orgChild4: [...allMappings.orgChild4.byDraftId.values()], + }; - // Track mapping for newly inserted posMasters - // saved is an array, map each to its draft ID - if (Array.isArray(saved)) { - for (let i = 0; i < saved.length; i++) { - const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; - if (draftPos && saved[i]) { - posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); + const posMasterCurrent = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, + { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, + { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, + { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, + { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, + ], + }); + + // Build lookup map + const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); + + // 2.3 Batch DELETE: positions in current but not in draft + const toDelete = posMasterCurrent.filter( + (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), + ); + + if (toDelete.length > 0) { + const toDeleteIds = toDelete.map((p) => p.id); + + // Cascade delete positions first + await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); + + // Then delete posMaster records + await queryRunner.manager.delete(PosMaster, toDeleteIds); + + const deleteHistoryOps = toDelete.map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); + } + + // 2.4 Process draft positions (UPDATE or INSERT) + const toUpdate: PosMaster[] = []; + const toInsert: any[] = []; + + // Track draft PosMaster ID to current PosMaster ID mapping for position sync + // Type: Map + const posMasterMapping: Map = new Map(); + + for (const draftPos of posMasterDraft) { + const current = currentByDNA.get(draftPos.ancestorDNA); + + // Map organization IDs using new IDs from Part 1 + const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); + const orgChild1Id = this.resolveOrgId( + draftPos.orgChild1Id ?? null, + allMappings.orgChild1, + ); + const orgChild2Id = this.resolveOrgId( + draftPos.orgChild2Id ?? null, + allMappings.orgChild2, + ); + const orgChild3Id = this.resolveOrgId( + draftPos.orgChild3Id ?? null, + allMappings.orgChild3, + ); + const orgChild4Id = this.resolveOrgId( + draftPos.orgChild4Id ?? null, + allMappings.orgChild4, + ); + + if (current) { + // UPDATE existing position + Object.assign(current, { + createdAt: draftPos.createdAt, + createdUserId: draftPos.createdUserId, + createdFullName: draftPos.createdFullName, + lastUpdatedAt: new Date(), + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + posMasterNoPrefix: draftPos.posMasterNoPrefix, + posMasterNoSuffix: draftPos.posMasterNoSuffix, + posMasterNo: draftPos.posMasterNo, + posMasterOrder: draftPos.posMasterOrder, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + isSit: draftPos.isSit, + reason: draftPos.reason, + isDirector: draftPos.isDirector, + isStaff: draftPos.isStaff, + positionSign: draftPos.positionSign, + statusReport: "DONE", + isCondition: draftPos.isCondition, + conditionReason: draftPos.conditionReason, + }); + toUpdate.push(current); + + if (draftPos.next_holderId === null) { + await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); + } + + // Track mapping for position sync + posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); + } else { + // INSERT new position + const newPosMaster = queryRunner.manager.create(PosMaster, { + ...draftPos, + id: undefined, + orgRevisionId: currentRevisionId, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + statusReport: "DONE", + }); + + toInsert.push(newPosMaster); + } + } + + // Batch save updates and inserts + if (toUpdate.length > 0) { + await queryRunner.manager.save(toUpdate); + } + if (toInsert.length > 0) { + const saved = await queryRunner.manager.save(toInsert); + + // Track mapping for newly inserted posMasters + // saved is an array, map each to its draft ID + if (Array.isArray(saved)) { + for (let i = 0; i < saved.length; i++) { + const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; + if (draftPos && saved[i]) { + posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); + } } } } + + // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) + const positionSyncStats = await this.syncAllPositionsBatch( + queryRunner, + posMasterMapping, + drafRevisionId, + currentRevisionId, + ); + + // Build comprehensive summary + const summary = { + message: "ย้ายโครงสร้างสำเร็จ", + organization: { + orgRoot: orgSyncStats.orgRoot, + orgChild1: orgSyncStats.orgChild1, + orgChild2: orgSyncStats.orgChild2, + orgChild3: orgSyncStats.orgChild3, + orgChild4: orgSyncStats.orgChild4, + }, + positionMaster: { + deleted: toDelete.length, + updated: toUpdate.length, + inserted: toInsert.length, + }, + position: positionSyncStats, + }; + + await queryRunner.commitTransaction(); + return new HttpSuccess(summary); } - // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) - const positionSyncStats = await this.syncAllPositionsBatch( - queryRunner, - posMasterMapping, - drafRevisionId, - currentRevisionId, - ); - - // Build comprehensive summary - const summary = { - message: "ย้ายโครงสร้างสำเร็จ", - organization: { - orgRoot: orgSyncStats.orgRoot, - orgChild1: orgSyncStats.orgChild1, - orgChild2: orgSyncStats.orgChild2, - orgChild3: orgSyncStats.orgChild3, - orgChild4: orgSyncStats.orgChild4, - }, - positionMaster: { - deleted: toDelete.length, - updated: toUpdate.length, - inserted: toInsert.length, - }, - position: positionSyncStats, - }; - - await queryRunner.commitTransaction(); - return new HttpSuccess(summary); + return new HttpSuccess({}); } catch (error) { console.error("Error moving draft to current:", error); await queryRunner.rollbackTransaction();