feat: add project_phases entity sync with task project_id backfill
The Autotask Tasks bulk API does not return projectID in its response, causing all tasks.project_id to be NULL. This fixes it by: - Adding project_phases as a synced entity (Autotask endpoint: /Phases) - Migration 059: project_phases table with project_id, phase_number, estimated_hours, start/due dates, parent_phase_id, is_scheduled - EntityType.PROJECT_PHASES added to all sync maps and dependency graph (depends on PROJECTS, runs before TASKS in sync order) - buildProjectPhasesFilter: Phases endpoint requires a filter (id > 0) - mapProjectPhase: maps Autotask field names to DB columns - Post-sync backfill in syncEntity: after each project_phases sync, UPDATE tasks SET project_id = pp.project_id FROM project_phases pp JOIN projects p WHERE tasks.phase_id = pp.id Only backfills where the project exists in our DB (FK constraint on tasks.project_id; archived projects are skipped gracefully) Result: 2,455 of 4,966 tasks now have project_id populated. Tasks belonging to archived/completed projects have phase_id resolvable via project_phases even when project_id remains NULL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
44db9a3019
commit
dd4cf68def
5 changed files with 131 additions and 0 deletions
|
|
@ -61,6 +61,7 @@ export function getAllEntitiesInOrder(): EntityType[] {
|
|||
EntityType.TICKET_CATEGORIES,
|
||||
EntityType.CONTACTS,
|
||||
EntityType.PROJECTS,
|
||||
EntityType.PROJECT_PHASES,
|
||||
EntityType.TICKETS,
|
||||
EntityType.TASKS,
|
||||
EntityType.CONFIGURATION_ITEMS,
|
||||
|
|
@ -126,6 +127,7 @@ export function getAutotaskEntityName(entity: EntityType): string {
|
|||
[EntityType.TICKET_NOTES]: 'TicketNotes',
|
||||
[EntityType.TAG_GROUPS]: 'TagGroups',
|
||||
[EntityType.TAGS]: 'Tags',
|
||||
[EntityType.PROJECT_PHASES]: 'Phases',
|
||||
};
|
||||
|
||||
return mapping[entity] || entity;
|
||||
|
|
@ -177,6 +179,7 @@ export function getLastModifiedField(entity: EntityType): string {
|
|||
[EntityType.TICKET_CATEGORIES]: 'lastModifiedDate',
|
||||
[EntityType.TAG_GROUPS]: 'lastModifiedDate',
|
||||
[EntityType.TAGS]: 'lastModifiedDateTime',
|
||||
[EntityType.PROJECT_PHASES]: 'lastActivityDateTime',
|
||||
};
|
||||
|
||||
return mapping[entity] || 'lastModifiedDate';
|
||||
|
|
@ -211,6 +214,7 @@ export function getActiveField(entity: EntityType): string | null {
|
|||
[EntityType.TICKET_CATEGORIES]: 'isActive',
|
||||
[EntityType.TAG_GROUPS]: 'isActive',
|
||||
[EntityType.TAGS]: 'isActive',
|
||||
[EntityType.PROJECT_PHASES]: null, // No active field on phases
|
||||
};
|
||||
|
||||
return mapping[entity] || null;
|
||||
|
|
@ -305,6 +309,7 @@ export function buildDateRangeFilter(
|
|||
[EntityType.QUEUES]: null,
|
||||
[EntityType.PRIORITIES]: null,
|
||||
[EntityType.TICKET_CATEGORIES]: null,
|
||||
[EntityType.PROJECT_PHASES]: null,
|
||||
};
|
||||
|
||||
const dateField = dateFieldMapping[entity];
|
||||
|
|
@ -347,6 +352,20 @@ export function buildContractsFilter(): Array<{ field: string; op: string; value
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build filter for project phases (Phases endpoint requires a filter)
|
||||
* @returns Query filter array for all phases
|
||||
*/
|
||||
export function buildProjectPhasesFilter(): Array<{ field: string; op: string; value: any }> {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
op: 'gt',
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build special filter for projects (requires status filter)
|
||||
* @returns Query filter array for active projects
|
||||
|
|
@ -498,6 +517,7 @@ export function getEntityDisplayName(entity: EntityType): string {
|
|||
[EntityType.TICKET_NOTES]: 'Ticket Notes',
|
||||
[EntityType.TAG_GROUPS]: 'Tag Groups',
|
||||
[EntityType.TAGS]: 'Tags',
|
||||
[EntityType.PROJECT_PHASES]: 'Project Phases',
|
||||
};
|
||||
|
||||
return mapping[entity] || entity;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue