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:
lorentz 2026-04-06 09:03:19 -04:00
parent 89dbe6155b
commit 07067bef19
16 changed files with 847 additions and 85 deletions

View file

@ -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)
*/