feat: Display Settings UI + Company Category/Type sync
- Add /admin/display-settings page with Kiosk and Mobile sections - Company category checkbox filter + excluded companies searchable multi-select - New DB tables: company_categories, company_types (migration 064) - Sync COMPANY_CATEGORIES via CompanyCategories entity (id/name/isActive) - Sync COMPANY_TYPES via Companies.companyType picklist - Add to EntityType, ENTITY_DEPENDENCIES, sync-helpers, entity-mapper, entity-sync - New API routes: /api/admin/display-settings (GET/POST), /api/data/company-categories, /api/data/companies-list - Update all 4 routes (kiosk/stats, kiosk/activity, mobile/tickets, mobile/dashboard) to filter by kiosk_settings company_category_ids + excluded_company_ids - Add Display Settings nav link (SlidersHorizontal icon) to Admin menu - Seed kiosk_settings: kiosk_company_category_ids=1, mobile_company_category_ids=1
This commit is contained in:
parent
89dbe6155b
commit
07067bef19
16 changed files with 847 additions and 85 deletions
|
|
@ -82,6 +82,12 @@ export class EntitySyncService {
|
|||
if (entity === EntityType.TICKET_CATEGORIES) {
|
||||
return await this.syncTicketCategories(isIncremental);
|
||||
}
|
||||
if (entity === EntityType.COMPANY_CATEGORIES) {
|
||||
return await this.syncCompanyCategories(isIncremental);
|
||||
}
|
||||
if (entity === EntityType.COMPANY_TYPES) {
|
||||
return await this.syncCompanyTypes(isIncremental);
|
||||
}
|
||||
|
||||
const trackingId = syncId || `${entity}_${Date.now()}`;
|
||||
const entityLogger = this.logger.child({ syncId: trackingId, entityType: entity });
|
||||
|
|
@ -1136,6 +1142,88 @@ export class EntitySyncService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Company Categories (queried from CompanyCategories entity — id, name, isActive)
|
||||
*/
|
||||
async syncCompanyCategories(isIncremental: boolean = false): Promise<EntitySyncStats> {
|
||||
const syncLogger = this.logger.child({ entityType: EntityType.COMPANY_CATEGORIES, syncType: 'entity' });
|
||||
const syncStartTime = syncLogger.start('Company Categories sync');
|
||||
|
||||
try {
|
||||
const apiRecords = await this.autotaskClient.queryEntityPaginated(
|
||||
'CompanyCategories',
|
||||
{ filter: [{ field: 'id', op: 'gt', value: 0 }] },
|
||||
500
|
||||
);
|
||||
|
||||
const records = apiRecords.map((r: any) => ({
|
||||
value: r.id,
|
||||
label: r.name || r.nickname || String(r.id),
|
||||
is_active: r.isActive !== false,
|
||||
sort_order: r.id,
|
||||
synced_at: new Date(),
|
||||
}));
|
||||
|
||||
syncLogger.info('Fetched company categories', { recordCount: records.length });
|
||||
|
||||
const tableName = getTableName(EntityType.COMPANY_CATEGORIES);
|
||||
const existingResult = await postgresClient.query<{ value: number }>(`SELECT value FROM ${tableName}`);
|
||||
const existingValues = new Set(existingResult.rows.map((r: any) => r.value));
|
||||
|
||||
const recordsAdded = records.filter((r: any) => !existingValues.has(r.value)).length;
|
||||
const recordsUpdated = records.filter((r: any) => existingValues.has(r.value)).length;
|
||||
|
||||
await postgresClient.bulkUpsert(tableName, records, ['value']);
|
||||
|
||||
const stats: EntitySyncStats = { recordsAdded, recordsUpdated, recordsDeleted: 0 };
|
||||
syncLogger.complete('Company Categories sync', syncStartTime, stats);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
syncLogger.fail('Company Categories sync', syncStartTime, err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Company Types (Picklist from Companies field)
|
||||
*/
|
||||
async syncCompanyTypes(isIncremental: boolean = false): Promise<EntitySyncStats> {
|
||||
const picklistLogger = this.logger.child({ entityType: EntityType.COMPANY_TYPES, syncType: 'picklist' });
|
||||
const syncStartTime = picklistLogger.start('Picklist sync');
|
||||
|
||||
try {
|
||||
const picklistValues = await this.autotaskClient.getPicklistValues('Companies', 'companyType');
|
||||
|
||||
const records = Object.entries(picklistValues).map(([value, label]) => ({
|
||||
value: parseInt(value),
|
||||
label: label,
|
||||
is_active: true,
|
||||
sort_order: parseInt(value),
|
||||
synced_at: new Date(),
|
||||
}));
|
||||
|
||||
picklistLogger.info('Found picklist values', { recordCount: records.length });
|
||||
|
||||
const tableName = getTableName(EntityType.COMPANY_TYPES);
|
||||
const existingResult = await postgresClient.query<{ value: number }>(`SELECT value FROM ${tableName}`);
|
||||
const existingValues = new Set(existingResult.rows.map((r: any) => r.value));
|
||||
|
||||
const recordsAdded = records.filter((r: any) => !existingValues.has(r.value)).length;
|
||||
const recordsUpdated = records.filter((r: any) => existingValues.has(r.value)).length;
|
||||
|
||||
await postgresClient.bulkUpsert(tableName, records, ['value']);
|
||||
|
||||
const stats: EntitySyncStats = { recordsAdded, recordsUpdated, recordsDeleted: 0 };
|
||||
picklistLogger.complete('Picklist sync', syncStartTime, stats);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
picklistLogger.fail('Picklist sync', syncStartTime, err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Work Types (Picklist from TimeEntry field)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ export enum EntityType {
|
|||
TAG_GROUPS = 'autotask_tag_groups',
|
||||
TAGS = 'autotask_tags',
|
||||
PROJECT_PHASES = 'project_phases',
|
||||
COMPANY_CATEGORIES = 'company_categories',
|
||||
COMPANY_TYPES = 'company_types',
|
||||
}
|
||||
|
||||
// Sync operation types
|
||||
|
|
@ -177,6 +179,8 @@ export const ENTITY_DEPENDENCIES: Record<EntityType, EntityType[]> = {
|
|||
[EntityType.TAG_GROUPS]: [], // No dependencies — standalone lookup
|
||||
[EntityType.TAGS]: [EntityType.TAG_GROUPS], // Depends on tag groups (FK)
|
||||
[EntityType.PROJECT_PHASES]: [EntityType.PROJECTS], // Depends on projects
|
||||
[EntityType.COMPANY_CATEGORIES]: [], // No dependencies — standalone lookup
|
||||
[EntityType.COMPANY_TYPES]: [], // No dependencies — standalone lookup
|
||||
};
|
||||
|
||||
// Autotask API field names (for incremental sync)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ export function mapAutotaskToDatabase(
|
|||
case EntityType.ISSUE_TYPES:
|
||||
case EntityType.SUB_ISSUE_TYPES:
|
||||
case EntityType.WORK_TYPES:
|
||||
case EntityType.COMPANY_CATEGORIES:
|
||||
case EntityType.COMPANY_TYPES:
|
||||
mapped = mapPicklist(data);
|
||||
break;
|
||||
case EntityType.TAG_GROUPS:
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ export function getAutotaskEntityName(entity: EntityType): string {
|
|||
[EntityType.TAG_GROUPS]: 'TagGroups',
|
||||
[EntityType.TAGS]: 'Tags',
|
||||
[EntityType.PROJECT_PHASES]: 'Phases',
|
||||
[EntityType.COMPANY_CATEGORIES]: 'CompanyCategories',
|
||||
[EntityType.COMPANY_TYPES]: 'CompanyTypes',
|
||||
};
|
||||
|
||||
return mapping[entity] || entity;
|
||||
|
|
@ -147,6 +149,7 @@ export function isPicklistEntity(entity: EntityType): boolean {
|
|||
EntityType.QUEUES,
|
||||
EntityType.PRIORITIES,
|
||||
EntityType.TICKET_CATEGORIES,
|
||||
EntityType.COMPANY_TYPES,
|
||||
].includes(entity);
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +183,8 @@ export function getLastModifiedField(entity: EntityType): string {
|
|||
[EntityType.TAG_GROUPS]: 'lastModifiedDate',
|
||||
[EntityType.TAGS]: 'lastModifiedDateTime',
|
||||
[EntityType.PROJECT_PHASES]: 'lastActivityDateTime',
|
||||
[EntityType.COMPANY_CATEGORIES]: 'lastModifiedDate',
|
||||
[EntityType.COMPANY_TYPES]: 'lastModifiedDate',
|
||||
};
|
||||
|
||||
return mapping[entity] || 'lastModifiedDate';
|
||||
|
|
@ -215,6 +220,8 @@ export function getActiveField(entity: EntityType): string | null {
|
|||
[EntityType.TAG_GROUPS]: 'isActive',
|
||||
[EntityType.TAGS]: 'isActive',
|
||||
[EntityType.PROJECT_PHASES]: null, // No active field on phases
|
||||
[EntityType.COMPANY_CATEGORIES]: 'isActive',
|
||||
[EntityType.COMPANY_TYPES]: 'isActive',
|
||||
};
|
||||
|
||||
return mapping[entity] || null;
|
||||
|
|
@ -310,6 +317,8 @@ export function buildDateRangeFilter(
|
|||
[EntityType.PRIORITIES]: null,
|
||||
[EntityType.TICKET_CATEGORIES]: null,
|
||||
[EntityType.PROJECT_PHASES]: null,
|
||||
[EntityType.COMPANY_CATEGORIES]: null,
|
||||
[EntityType.COMPANY_TYPES]: null,
|
||||
};
|
||||
|
||||
const dateField = dateFieldMapping[entity];
|
||||
|
|
@ -518,6 +527,8 @@ export function getEntityDisplayName(entity: EntityType): string {
|
|||
[EntityType.TAG_GROUPS]: 'Tag Groups',
|
||||
[EntityType.TAGS]: 'Tags',
|
||||
[EntityType.PROJECT_PHASES]: 'Project Phases',
|
||||
[EntityType.COMPANY_CATEGORIES]: 'Company Categories',
|
||||
[EntityType.COMPANY_TYPES]: 'Company Types',
|
||||
};
|
||||
|
||||
return mapping[entity] || entity;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue