feat: improve move-draft-to-current with differential sync
Implement differential sync for organization structure and positions instead of delete-all-and-insert-all approach. Changes: - Add OrgIdMapping and AllOrgMappings interfaces for tracking ID mappings - Implement syncOrgLevel() helper for differential sync per org level - Add syncPositionsForPosMaster() helper for position table sync - Process org levels bottom-up (Child4→Child3→Child2→Child1→Root) - Use ancestorDNA matching with Like operator for descendant sync - Cascade delete positions before deleting org nodes - Batch DELETE/UPDATE/INSERT operations for better performance - Track draft→current ID mappings for position updates - Add comprehensive documentation in docs/move-draft-to-current.md Benefits: - Preserve IDs for unchanged nodes (better tracking) - More efficient (fewer database operations) - Better data integrity with proper FK handling - Sync all descendants under given rootDnaId Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
22fc43fe17
commit
638362df1c
3 changed files with 938 additions and 24 deletions
356
docs/move-draft-to-current.md
Normal file
356
docs/move-draft-to-current.md
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
# Move Draft to Current - Differential Sync Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the implementation of the improved `move-draft-to-current` function in `OrganizationController.ts`. The function synchronizes organization structure and position data from the **Draft Revision** to the **Current Revision** using a differential sync approach (instead of the previous "delete all and insert all" method).
|
||||||
|
|
||||||
|
**API Endpoint:** `POST /api/v1/org/move-draft-to-current/{rootDnaId}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Data Models
|
||||||
|
|
||||||
|
The organization structure consists of 5 hierarchical levels:
|
||||||
|
|
||||||
|
```
|
||||||
|
OrgRoot (Level 0)
|
||||||
|
└── OrgChild1 (Level 1)
|
||||||
|
└── OrgChild2 (Level 2)
|
||||||
|
└── OrgChild3 (Level 3)
|
||||||
|
└── OrgChild4 (Level 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each level has:
|
||||||
|
- Organization nodes with `ancestorDNA` for hierarchical tracking
|
||||||
|
- Foreign key relationships to parent levels
|
||||||
|
- Associated position records (`PosMaster`)
|
||||||
|
|
||||||
|
### Type Definitions
|
||||||
|
|
||||||
|
Located in `src/interfaces/OrgMapping.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface OrgIdMapping {
|
||||||
|
byAncestorDNA: Map<string, string>; // ancestorDNA → current ID
|
||||||
|
byDraftId: Map<string, string>; // draft ID → current ID
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllOrgMappings {
|
||||||
|
orgRoot: OrgIdMapping;
|
||||||
|
orgChild1: OrgIdMapping;
|
||||||
|
orgChild2: OrgIdMapping;
|
||||||
|
orgChild3: OrgIdMapping;
|
||||||
|
orgChild4: OrgIdMapping;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Workflow
|
||||||
|
|
||||||
|
### Phase 0: Preparation
|
||||||
|
|
||||||
|
1. **Get Revision IDs**
|
||||||
|
- Fetch Draft Revision (`orgRevisionIsDraft: true`)
|
||||||
|
- Fetch Current Revision (`orgRevisionIsCurrent: true`)
|
||||||
|
|
||||||
|
2. **Validate rootDnaId**
|
||||||
|
- Check if rootDnaId exists in Draft Revision
|
||||||
|
- Return error if not found
|
||||||
|
|
||||||
|
### Phase 1: Sync Organization Structure (Bottom-Up)
|
||||||
|
|
||||||
|
**Processing Order:** `OrgChild4 → OrgChild3 → OrgChild2 → OrgChild1 → OrgRoot`
|
||||||
|
|
||||||
|
**Why Bottom-Up?** Child nodes have no dependent children (only parent references), allowing safe deletion without FK violations.
|
||||||
|
|
||||||
|
#### For Each Organization Level
|
||||||
|
|
||||||
|
The `syncOrgLevel()` helper performs:
|
||||||
|
|
||||||
|
1. **FETCH** - Get all draft and current nodes under `rootDnaId`
|
||||||
|
```typescript
|
||||||
|
where: { ancestorDNA: Like(`${rootDnaId}%`) } // All descendants
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **DELETE** - Remove current nodes not in draft
|
||||||
|
- Cascade delete positions first (via `cascadeDeletePositions()`)
|
||||||
|
- Delete the organization node
|
||||||
|
|
||||||
|
3. **UPDATE** - Update nodes that exist in both (matched by `ancestorDNA`)
|
||||||
|
- Map parent IDs using `parentMappings`
|
||||||
|
- Preserve original node ID
|
||||||
|
|
||||||
|
4. **INSERT** - Add draft nodes not in current
|
||||||
|
- Create new node with mapped parent IDs
|
||||||
|
- Return new ID for tracking
|
||||||
|
|
||||||
|
5. **RETURN** - Return `OrgIdMapping` for next level
|
||||||
|
|
||||||
|
**Result:** `allMappings` contains draft ID → current ID mappings for all org levels
|
||||||
|
|
||||||
|
### Phase 2: Sync Position Data
|
||||||
|
|
||||||
|
#### Step 2.1: Clear current_holderId
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Clear holders for positions that will have new holders
|
||||||
|
await queryRunner.manager.update(PosMaster,
|
||||||
|
{ current_holderId: In(nextHolderIds) },
|
||||||
|
{ current_holderId: null, isSit: false }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2.2: Fetch Draft and Current Positions
|
||||||
|
|
||||||
|
- Get draft positions using `draftOrgIds` from `allMappings`
|
||||||
|
- Get current positions using `currentOrgIds` from `allMappings`
|
||||||
|
|
||||||
|
#### Step 2.3: Batch DELETE
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Delete current positions not in draft (cascade delete positions first)
|
||||||
|
await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) })
|
||||||
|
await queryRunner.manager.delete(PosMaster, toDeleteIds)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2.4: Process UPDATE or INSERT
|
||||||
|
|
||||||
|
For each draft position:
|
||||||
|
1. Map organization IDs using `resolveOrgId()`
|
||||||
|
2. If exists in current → **UPDATE**
|
||||||
|
3. If not exists → **INSERT**
|
||||||
|
4. Track `draftPosMasterId → currentPosMasterId` mapping
|
||||||
|
|
||||||
|
#### Step 2.5: Sync Position Table
|
||||||
|
|
||||||
|
For each mapped PosMaster:
|
||||||
|
```typescript
|
||||||
|
await syncPositionsForPosMaster(
|
||||||
|
queryRunner,
|
||||||
|
draftPosMasterId,
|
||||||
|
currentPosMasterId,
|
||||||
|
draftRevisionId,
|
||||||
|
currentRevisionId
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
### `resolveOrgId(draftId, mapping)`
|
||||||
|
|
||||||
|
Maps a draft organization ID to its current ID.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private resolveOrgId(
|
||||||
|
draftId: string | null,
|
||||||
|
mapping: OrgIdMapping
|
||||||
|
): string | null {
|
||||||
|
if (!draftId) return null;
|
||||||
|
return mapping.byDraftId.get(draftId) ?? null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `cascadeDeletePositions(queryRunner, node, entityClass)`
|
||||||
|
|
||||||
|
Deletes positions associated with an organization node before deleting the node itself.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private async cascadeDeletePositions(
|
||||||
|
queryRunner: any,
|
||||||
|
node: any,
|
||||||
|
entityClass: any
|
||||||
|
): Promise<void> {
|
||||||
|
const whereClause = { orgRevisionId: node.orgRevisionId };
|
||||||
|
|
||||||
|
// Set FK field based on entity type
|
||||||
|
if (entityClass === OrgRoot) whereClause.orgRootId = node.id;
|
||||||
|
else if (entityClass === OrgChild1) whereClause.orgChild1Id = node.id;
|
||||||
|
// ... etc
|
||||||
|
|
||||||
|
await queryRunner.manager.delete(PosMaster, whereClause);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `syncOrgLevel(...)`
|
||||||
|
|
||||||
|
Generic differential sync for each organization level.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `queryRunner` - Database query runner
|
||||||
|
- `entityClass` - Organization entity class (OrgRoot, OrgChild1, etc.)
|
||||||
|
- `repository` - Repository for the entity
|
||||||
|
- `draftRevisionId` - Draft revision ID
|
||||||
|
- `currentRevisionId` - Current revision ID
|
||||||
|
- `rootDnaId` - Root DNA ID to sync under
|
||||||
|
- `parentMappings` - Mappings from child levels (for FK resolution)
|
||||||
|
|
||||||
|
**Returns:** `OrgIdMapping` for this level
|
||||||
|
|
||||||
|
### `syncPositionsForPosMaster(...)`
|
||||||
|
|
||||||
|
Syncs positions for a PosMaster record.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `queryRunner` - Database query runner
|
||||||
|
- `draftPosMasterId` - Draft PosMaster ID
|
||||||
|
- `currentPosMasterId` - Current PosMaster ID
|
||||||
|
- `draftRevisionId` - Draft revision ID
|
||||||
|
- `currentRevisionId` - Current revision ID
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Fetch draft and current positions
|
||||||
|
2. Delete current positions not in draft (by `orderNo`)
|
||||||
|
3. Update existing positions
|
||||||
|
4. Insert new positions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transaction Management
|
||||||
|
|
||||||
|
All operations are wrapped in a transaction:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const queryRunner = AppDataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ... all sync operations
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "...");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits of Differential Sync
|
||||||
|
|
||||||
|
| Aspect | Old Approach (Delete All + Insert) | New Approach (Differential Sync) |
|
||||||
|
|--------|-----------------------------------|----------------------------------|
|
||||||
|
| **ID Preservation** | All IDs changed | Unchanged nodes keep original IDs |
|
||||||
|
| **Performance** | N deletes + N inserts | Only changed data processed |
|
||||||
|
| **Tracking** | Cannot track what changed | Can track additions/updates/deletes |
|
||||||
|
| **Data Integrity** | Higher risk of data loss | Better integrity with cascade deletes |
|
||||||
|
| **Scalability** | Poor for large datasets | Efficient with batch operations |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema Relationships
|
||||||
|
|
||||||
|
```
|
||||||
|
orgRevision
|
||||||
|
├── orgRoot (FK: orgRevisionId)
|
||||||
|
│ ├── orgChild1 (FK: orgRootId, orgRevisionId)
|
||||||
|
│ │ ├── orgChild2 (FK: orgChild1Id, orgRootId, orgRevisionId)
|
||||||
|
│ │ │ ├── orgChild3 (FK: orgChild2Id, orgChild1Id, orgRootId, orgRevisionId)
|
||||||
|
│ │ │ │ └── orgChild4 (FK: orgChild3Id, orgChild2Id, orgChild1Id, orgRootId, orgRevisionId)
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ └── posMaster (FK: orgRootId, orgChild1Id, orgChild2Id, orgChild3Id, orgChild4Id)
|
||||||
|
│ │ │ └── position (FK: posMasterId)
|
||||||
|
│ │ │
|
||||||
|
│ │ └── posMaster
|
||||||
|
│ │
|
||||||
|
│ └── posMaster
|
||||||
|
│
|
||||||
|
└── (current revision similar structure)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Error Condition | Response |
|
||||||
|
|----------------|----------|
|
||||||
|
| Draft/Current revision not found | 404 NOT_FOUND |
|
||||||
|
| rootDnaId not found in draft | 404 NOT_FOUND |
|
||||||
|
| No positions in draft structure | 404 NOT_FOUND |
|
||||||
|
| Database error during sync | 500 INTERNAL_SERVER_ERROR (rollback) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified/Created
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── interfaces/
|
||||||
|
│ └── OrgMapping.ts [NEW] Type definitions
|
||||||
|
├── controllers/
|
||||||
|
│ └── OrganizationController.ts [MODIFIED] moveDraftToCurrent function + helpers
|
||||||
|
docs/
|
||||||
|
└── move-draft-to-current.md [NEW] This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Considerations
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- Test `syncOrgLevel()` for each org level with various scenarios
|
||||||
|
- Test `resolveOrgId()` mapping function
|
||||||
|
- Test `cascadeDeletePositions()` function
|
||||||
|
- Test `syncPositionsForPosMaster()` function
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
1. **Empty Draft** - Sync with no draft data
|
||||||
|
2. **Full Replacement** - All nodes changed
|
||||||
|
3. **Partial Update** - Some nodes added, some updated, some deleted
|
||||||
|
4. **Position Sync** - Verify position table syncs correctly
|
||||||
|
5. **Foreign Key Constraints** - Verify all FK relationships maintained
|
||||||
|
|
||||||
|
### Manual Testing Flow
|
||||||
|
|
||||||
|
1. Create draft structure with various changes:
|
||||||
|
- Add new department
|
||||||
|
- Modify existing department
|
||||||
|
- Remove existing department
|
||||||
|
- Add/modify/remove positions
|
||||||
|
2. Call `move-draft-to-current` API
|
||||||
|
3. Verify:
|
||||||
|
- New departments appear in current
|
||||||
|
- Modified departments are updated (not recreated)
|
||||||
|
- Removed departments are gone (with positions cascade deleted)
|
||||||
|
- Positions have correct new org IDs
|
||||||
|
- Position table records sync correctly
|
||||||
|
- All foreign key constraints satisfied
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
- **Batch Operations** - Use `In()` clause for multiple IDs
|
||||||
|
- **Map Lookups** - Use `Map` for O(1) lookups instead of array searches
|
||||||
|
- **Bottom-Up Processing** - Minimize FK constraint checks
|
||||||
|
- **Parallel Queries** - Use `Promise.all()` for independent queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
1. **Parallel Processing** - Process independent org branches in parallel
|
||||||
|
2. **Incremental Sync** - Only sync changed subtrees
|
||||||
|
3. **Caching** - Cache org mappings for repeated operations
|
||||||
|
4. **Audit Log** - Track all changes for audit purposes
|
||||||
|
5. **Validation** - Add pre-sync validation to catch errors early
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- TypeORM Documentation: https://typeorm.io/
|
||||||
|
- TSOA Documentation: https://tsoa-community.github.io/
|
||||||
|
- Project Repository: [Internal Git]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-02-09
|
||||||
|
**Author:** Claude Code
|
||||||
|
**Version:** 1.0.0
|
||||||
|
|
@ -24,7 +24,7 @@ import HttpSuccess from "../interfaces/http-success";
|
||||||
import { OrgChild1 } from "../entities/OrgChild1";
|
import { OrgChild1 } from "../entities/OrgChild1";
|
||||||
import HttpError from "../interfaces/http-error";
|
import HttpError from "../interfaces/http-error";
|
||||||
import HttpStatusCode from "../interfaces/http-status";
|
import HttpStatusCode from "../interfaces/http-status";
|
||||||
import { In, IsNull, Not } from "typeorm";
|
import { In, IsNull, Not, Like } from "typeorm";
|
||||||
import { OrgRoot } from "../entities/OrgRoot";
|
import { OrgRoot } from "../entities/OrgRoot";
|
||||||
import { OrgChild2 } from "../entities/OrgChild2";
|
import { OrgChild2 } from "../entities/OrgChild2";
|
||||||
import { OrgChild3 } from "../entities/OrgChild3";
|
import { OrgChild3 } from "../entities/OrgChild3";
|
||||||
|
|
@ -57,6 +57,7 @@ import {
|
||||||
CreatePosMasterHistoryOfficer,
|
CreatePosMasterHistoryOfficer,
|
||||||
} from "../services/PositionService";
|
} from "../services/PositionService";
|
||||||
import { orgStructureCache } from "../utils/OrgStructureCache";
|
import { orgStructureCache } from "../utils/OrgStructureCache";
|
||||||
|
import { OrgIdMapping, AllOrgMappings } from "../interfaces/OrgMapping";
|
||||||
|
|
||||||
@Route("api/v1/org")
|
@Route("api/v1/org")
|
||||||
@Tags("Organization")
|
@Tags("Organization")
|
||||||
|
|
@ -7811,34 +7812,567 @@ export class OrganizationController extends Controller {
|
||||||
* @summary - ย้ายโครงสร้างและตำแหน่งจากแบบร่างไปโครงสร้างปัจจุบัน โดยอ้างอิงตาม rootId
|
* @summary - ย้ายโครงสร้างและตำแหน่งจากแบบร่างไปโครงสร้างปัจจุบัน โดยอ้างอิงตาม rootId
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Post("move-draft-to-current/{rootId}")
|
@Post("move-draft-to-current/{rootDnaId}")
|
||||||
async moveDraftToCurrent(@Request() request: RequestWithUser) {
|
async moveDraftToCurrent(@Request() request: RequestWithUser) {
|
||||||
// part 1 ข้อมูลโครงสร้าง
|
const queryRunner = AppDataSource.createQueryRunner();
|
||||||
const drafRevision = await this.orgRevisionRepository.findOne({
|
await queryRunner.connect();
|
||||||
where: {
|
await queryRunner.startTransaction();
|
||||||
orgRevisionIsDraft: true,
|
|
||||||
orgRevisionIsCurrent: false,
|
|
||||||
},
|
|
||||||
select: ["id"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRevision = await this.orgRevisionRepository.findOne({
|
try {
|
||||||
where: {
|
// permission owner only ??
|
||||||
orgRevisionIsDraft: false,
|
// this code check...
|
||||||
orgRevisionIsCurrent: true,
|
|
||||||
},
|
|
||||||
select: ["id"],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!drafRevision || !currentRevision)
|
// part 1 ข้อมูลโครงสร้าง
|
||||||
return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล");
|
const [drafRevision, currentRevision] = await Promise.all([
|
||||||
|
this.orgRevisionRepository.findOne({
|
||||||
|
where: {
|
||||||
|
orgRevisionIsDraft: true,
|
||||||
|
orgRevisionIsCurrent: false,
|
||||||
|
},
|
||||||
|
select: ["id"],
|
||||||
|
}),
|
||||||
|
this.orgRevisionRepository.findOne({
|
||||||
|
where: {
|
||||||
|
orgRevisionIsDraft: false,
|
||||||
|
orgRevisionIsCurrent: true,
|
||||||
|
},
|
||||||
|
select: ["id"],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const drafRevisionId = drafRevision.id;
|
if (!drafRevision || !currentRevision)
|
||||||
const currentRevisionId = currentRevision.id;
|
return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล");
|
||||||
|
|
||||||
// start transaction multi table orgRoot, orgChild1, orgChild2, orgChild3, orgChild4, posMaster, position
|
const drafRevisionId = drafRevision.id;
|
||||||
|
const currentRevisionId = currentRevision.id;
|
||||||
|
|
||||||
// part 2 ข้อมูลตำแหน่ง
|
// ตรวจสอบว่ามี rootDnaId ในโครงสร้างร่าง และในโครงสร้างปัจจุบันหรือไม่
|
||||||
// 1. ดึงข้อมูลคนออกจากโครงสร้างปัจจุบัน
|
const [orgRootDraft, orgRootCurrent] = await Promise.all([
|
||||||
|
this.orgRootRepository.findOne({
|
||||||
|
where: {
|
||||||
|
ancestorDNA: request.params.rootDnaId,
|
||||||
|
orgRevisionId: drafRevisionId,
|
||||||
|
},
|
||||||
|
select: ["id"],
|
||||||
|
}),
|
||||||
|
this.orgRootRepository.findOne({
|
||||||
|
where: {
|
||||||
|
ancestorDNA: request.params.rootDnaId,
|
||||||
|
orgRevisionId: currentRevisionId,
|
||||||
|
},
|
||||||
|
select: ["id"],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process from bottom (Child4) to top (Root) to handle foreign key constraints
|
||||||
|
// Child4 (leaf nodes - no children depending on them)
|
||||||
|
allMappings.orgChild4 = await this.syncOrgLevel(
|
||||||
|
queryRunner, OrgChild4, this.child4Repository,
|
||||||
|
drafRevisionId, currentRevisionId,
|
||||||
|
request.params.rootDnaId, allMappings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Child3
|
||||||
|
allMappings.orgChild3 = await this.syncOrgLevel(
|
||||||
|
queryRunner, OrgChild3, this.child3Repository,
|
||||||
|
drafRevisionId, currentRevisionId,
|
||||||
|
request.params.rootDnaId, allMappings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Child2
|
||||||
|
allMappings.orgChild2 = await this.syncOrgLevel(
|
||||||
|
queryRunner, OrgChild2, this.child2Repository,
|
||||||
|
drafRevisionId, currentRevisionId,
|
||||||
|
request.params.rootDnaId, allMappings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Child1
|
||||||
|
allMappings.orgChild1 = await this.syncOrgLevel(
|
||||||
|
queryRunner, OrgChild1, this.child1Repository,
|
||||||
|
drafRevisionId, currentRevisionId,
|
||||||
|
request.params.rootDnaId, allMappings
|
||||||
|
);
|
||||||
|
|
||||||
|
// OrgRoot (root level - no parent mapping needed)
|
||||||
|
allMappings.orgRoot = await this.syncOrgLevel(
|
||||||
|
queryRunner, OrgRoot, this.orgRootRepository,
|
||||||
|
drafRevisionId, currentRevisionId,
|
||||||
|
request.params.rootDnaId
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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()],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (nextHolderIds.length > 0) {
|
||||||
|
await queryRunner.manager.update(
|
||||||
|
PosMaster,
|
||||||
|
{
|
||||||
|
orgRevisionId: currentRevisionId,
|
||||||
|
current_holderId: In(nextHolderIds)
|
||||||
|
},
|
||||||
|
{ current_holderId: null, isSit: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const posMasterMapping: Map<string, string> = 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);
|
||||||
|
|
||||||
|
// Track mapping for position sync
|
||||||
|
posMasterMapping.set(draftPos.id, current.id);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.5 Sync positions table for all affected posMasters
|
||||||
|
for (const [draftPosMasterId, currentPosMasterId] of posMasterMapping) {
|
||||||
|
await this.syncPositionsForPosMaster(
|
||||||
|
queryRunner,
|
||||||
|
draftPosMasterId,
|
||||||
|
currentPosMasterId,
|
||||||
|
drafRevisionId,
|
||||||
|
currentRevisionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error moving draft to current:", error);
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการย้ายโครงสร้าง");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: Map draft ID to current ID using the mapping
|
||||||
|
*/
|
||||||
|
private resolveOrgId(
|
||||||
|
draftId: string | null,
|
||||||
|
mapping: OrgIdMapping
|
||||||
|
): string | null {
|
||||||
|
if (!draftId) return null;
|
||||||
|
return mapping.byDraftId.get(draftId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: Cascade delete positions before deleting org node
|
||||||
|
*/
|
||||||
|
private async cascadeDeletePositions(
|
||||||
|
queryRunner: any,
|
||||||
|
node: any,
|
||||||
|
entityClass: any
|
||||||
|
): Promise<void> {
|
||||||
|
const whereClause: any = {
|
||||||
|
orgRevisionId: node.orgRevisionId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine which FK field to use based on entity type
|
||||||
|
if (entityClass === OrgRoot) {
|
||||||
|
whereClause.orgRootId = node.id;
|
||||||
|
} else if (entityClass === OrgChild1) {
|
||||||
|
whereClause.orgChild1Id = node.id;
|
||||||
|
} else if (entityClass === OrgChild2) {
|
||||||
|
whereClause.orgChild2Id = node.id;
|
||||||
|
} else if (entityClass === OrgChild3) {
|
||||||
|
whereClause.orgChild3Id = node.id;
|
||||||
|
} else if (entityClass === OrgChild4) {
|
||||||
|
whereClause.orgChild4Id = node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.manager.delete(PosMaster, whereClause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: Generic differential sync for each org level
|
||||||
|
* Performs DELETE (nodes not in draft), UPDATE (existing nodes), INSERT (new nodes)
|
||||||
|
*/
|
||||||
|
private async syncOrgLevel<T extends OrgRoot | OrgChild1 | OrgChild2 | OrgChild3 | OrgChild4>(
|
||||||
|
queryRunner: any,
|
||||||
|
entityClass: any,
|
||||||
|
repository: any,
|
||||||
|
draftRevisionId: string,
|
||||||
|
currentRevisionId: string,
|
||||||
|
rootDnaId: string,
|
||||||
|
parentMappings?: AllOrgMappings
|
||||||
|
): Promise<OrgIdMapping> {
|
||||||
|
// 1. Fetch draft and current nodes under the given rootDnaId
|
||||||
|
const [draftNodes, currentNodes] = await Promise.all([
|
||||||
|
repository.find({
|
||||||
|
where: {
|
||||||
|
orgRevisionId: draftRevisionId,
|
||||||
|
ancestorDNA: Like(`${rootDnaId}%`)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
repository.find({
|
||||||
|
where: {
|
||||||
|
orgRevisionId: currentRevisionId,
|
||||||
|
ancestorDNA: Like(`${rootDnaId}%`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Build lookup maps for efficient matching by ancestorDNA
|
||||||
|
const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n]));
|
||||||
|
const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n]));
|
||||||
|
|
||||||
|
const mapping: OrgIdMapping = {
|
||||||
|
byAncestorDNA: new Map(),
|
||||||
|
byDraftId: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. DELETE: Current nodes not in draft (cascade delete positions first)
|
||||||
|
const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA));
|
||||||
|
for (const node of toDelete) {
|
||||||
|
// Cascade delete positions first
|
||||||
|
await this.cascadeDeletePositions(queryRunner, node, entityClass);
|
||||||
|
await queryRunner.manager.delete(entityClass, node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. UPDATE: Nodes that exist in both draft and current (matched by ancestorDNA)
|
||||||
|
const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA));
|
||||||
|
for (const draft of toUpdate) {
|
||||||
|
const current: any = currentByDNA.get(draft.ancestorDNA)!;
|
||||||
|
|
||||||
|
// Build update data with mapped parent IDs
|
||||||
|
const updateData: any = {
|
||||||
|
...draft,
|
||||||
|
id: current.id,
|
||||||
|
orgRevisionId: currentRevisionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map parent IDs based on entity level
|
||||||
|
if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) {
|
||||||
|
updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId;
|
||||||
|
} else if (entityClass === OrgChild2) {
|
||||||
|
if (draft.orgRootId && parentMappings) {
|
||||||
|
updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId;
|
||||||
|
}
|
||||||
|
if (draft.orgChild1Id && parentMappings) {
|
||||||
|
updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id;
|
||||||
|
}
|
||||||
|
} else if (entityClass === OrgChild3) {
|
||||||
|
if (draft.orgRootId && parentMappings) {
|
||||||
|
updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId;
|
||||||
|
}
|
||||||
|
if (draft.orgChild1Id && parentMappings) {
|
||||||
|
updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id;
|
||||||
|
}
|
||||||
|
if (draft.orgChild2Id && parentMappings) {
|
||||||
|
updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id;
|
||||||
|
}
|
||||||
|
} else if (entityClass === OrgChild4) {
|
||||||
|
if (draft.orgRootId && parentMappings) {
|
||||||
|
updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId;
|
||||||
|
}
|
||||||
|
if (draft.orgChild1Id && parentMappings) {
|
||||||
|
updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id;
|
||||||
|
}
|
||||||
|
if (draft.orgChild2Id && parentMappings) {
|
||||||
|
updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id;
|
||||||
|
}
|
||||||
|
if (draft.orgChild3Id && parentMappings) {
|
||||||
|
updateData.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.manager.update(entityClass, current.id, updateData);
|
||||||
|
|
||||||
|
mapping.byAncestorDNA.set(draft.ancestorDNA, current.id);
|
||||||
|
mapping.byDraftId.set(draft.id, current.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. INSERT: Draft nodes not in current
|
||||||
|
const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA));
|
||||||
|
for (const draft of toInsert) {
|
||||||
|
const newNode: any = queryRunner.manager.create(entityClass, {
|
||||||
|
...draft,
|
||||||
|
id: undefined,
|
||||||
|
orgRevisionId: currentRevisionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map parent IDs based on entity level
|
||||||
|
if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) {
|
||||||
|
newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId);
|
||||||
|
} else if (entityClass === OrgChild2) {
|
||||||
|
if (draft.orgRootId && parentMappings) {
|
||||||
|
newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId);
|
||||||
|
}
|
||||||
|
if (draft.orgChild1Id && parentMappings) {
|
||||||
|
newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id);
|
||||||
|
}
|
||||||
|
} else if (entityClass === OrgChild3) {
|
||||||
|
if (draft.orgRootId && parentMappings) {
|
||||||
|
newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId);
|
||||||
|
}
|
||||||
|
if (draft.orgChild1Id && parentMappings) {
|
||||||
|
newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id);
|
||||||
|
}
|
||||||
|
if (draft.orgChild2Id && parentMappings) {
|
||||||
|
newNode.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id);
|
||||||
|
}
|
||||||
|
} else if (entityClass === OrgChild4) {
|
||||||
|
if (draft.orgRootId && parentMappings) {
|
||||||
|
newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId);
|
||||||
|
}
|
||||||
|
if (draft.orgChild1Id && parentMappings) {
|
||||||
|
newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id);
|
||||||
|
}
|
||||||
|
if (draft.orgChild2Id && parentMappings) {
|
||||||
|
newNode.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id);
|
||||||
|
}
|
||||||
|
if (draft.orgChild3Id && parentMappings) {
|
||||||
|
newNode.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = await queryRunner.manager.save(newNode);
|
||||||
|
|
||||||
|
mapping.byAncestorDNA.set(draft.ancestorDNA, saved.id);
|
||||||
|
mapping.byDraftId.set(draft.id, saved.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: Sync positions for a PosMaster
|
||||||
|
* Handles DELETE/UPDATE/INSERT for positions associated with a posMaster
|
||||||
|
*/
|
||||||
|
private async syncPositionsForPosMaster(
|
||||||
|
queryRunner: any,
|
||||||
|
draftPosMasterId: string,
|
||||||
|
currentPosMasterId: string,
|
||||||
|
draftRevisionId: string,
|
||||||
|
currentRevisionId: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Fetch draft and current positions for this posMaster
|
||||||
|
const [draftPositions, currentPositions] = await Promise.all([
|
||||||
|
queryRunner.manager.find(Position, {
|
||||||
|
where: {
|
||||||
|
posMasterId: draftPosMasterId
|
||||||
|
},
|
||||||
|
order: { orderNo: 'ASC' }
|
||||||
|
}),
|
||||||
|
queryRunner.manager.find(Position, {
|
||||||
|
where: {
|
||||||
|
posMasterId: currentPosMasterId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If no draft positions, delete all current positions
|
||||||
|
if (draftPositions.length === 0) {
|
||||||
|
if (currentPositions.length > 0) {
|
||||||
|
await queryRunner.manager.delete(
|
||||||
|
Position,
|
||||||
|
currentPositions.map((p: any) => p.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build maps for tracking
|
||||||
|
const currentByOrderNo = new Map(
|
||||||
|
currentPositions.map((p: any) => [p.orderNo, p])
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE: Current positions not in draft (by orderNo)
|
||||||
|
const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo));
|
||||||
|
const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo));
|
||||||
|
|
||||||
|
if (toDelete.length > 0) {
|
||||||
|
await queryRunner.manager.delete(
|
||||||
|
Position,
|
||||||
|
toDelete.map((p: any) => p.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE and INSERT
|
||||||
|
for (const draftPos of draftPositions) {
|
||||||
|
const current: any = currentByOrderNo.get(draftPos.orderNo);
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
// UPDATE existing position
|
||||||
|
await queryRunner.manager.update(Position, current.id, {
|
||||||
|
positionName: draftPos.positionName,
|
||||||
|
positionField: draftPos.positionField,
|
||||||
|
posTypeId: draftPos.posTypeId,
|
||||||
|
posLevelId: draftPos.posLevelId,
|
||||||
|
posExecutiveId: draftPos.posExecutiveId,
|
||||||
|
positionExecutiveField: draftPos.positionExecutiveField,
|
||||||
|
positionArea: draftPos.positionArea,
|
||||||
|
isSpecial: draftPos.isSpecial,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// INSERT new position
|
||||||
|
const newPosition = queryRunner.manager.create(Position, {
|
||||||
|
...draftPos,
|
||||||
|
id: undefined,
|
||||||
|
posMasterId: currentPosMasterId,
|
||||||
|
});
|
||||||
|
await queryRunner.manager.save(newPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/interfaces/OrgMapping.ts
Normal file
24
src/interfaces/OrgMapping.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Type definitions for organization mapping used in move-draft-to-current function
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps draft organization IDs to current organization IDs
|
||||||
|
* - byAncestorDNA: Maps ancestorDNA to current ID for lookup
|
||||||
|
* - byDraftId: Maps draft ID to current ID for position updates
|
||||||
|
*/
|
||||||
|
export interface OrgIdMapping {
|
||||||
|
byAncestorDNA: Map<string, string>;
|
||||||
|
byDraftId: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains mappings for all organization levels
|
||||||
|
*/
|
||||||
|
export interface AllOrgMappings {
|
||||||
|
orgRoot: OrgIdMapping;
|
||||||
|
orgChild1: OrgIdMapping;
|
||||||
|
orgChild2: OrgIdMapping;
|
||||||
|
orgChild3: OrgIdMapping;
|
||||||
|
orgChild4: OrgIdMapping;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue