Add comprehensive admin features and multi-system integration

- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
This commit is contained in:
root 2025-11-19 14:18:16 -05:00
parent e8462ef301
commit 6eee14f8af
171 changed files with 32671 additions and 621 deletions

View file

@ -211,3 +211,14 @@ export enum AddigyDeviceType {
iPad = 'ipad',
AppleTV = 'appletv',
}
// Mapping types for Addigy organizations to Autotask companies
export interface AddigyOrgMapping {
id: number;
addigyOrgId: string;
addigyOrgName: string;
autotaskCompanyId: number;
autotaskCompanyName: string;
createdAt: string;
updatedAt: string;
}

276
lib/types/analytics.ts Normal file
View file

@ -0,0 +1,276 @@
/**
* Analytics Types
* TypeScript definitions for analytics, scoring, and insights
*/
// Score interfaces
export interface ActivityScore {
score: number; // 0 to 1
factors: string[];
breakdown: {
completeness: number;
consistency: number;
duration: number;
categorization: number;
};
}
export interface ContentScore {
score: number; // 0 to 1
factors: string[];
breakdown: {
notesQuality: number;
titleClarity: number;
internalNotes: number;
technicalDetail: number;
};
}
export interface TimelinessScore {
score: number; // 0 to 1
factors: string[];
breakdown: {
entryDelay: number;
businessHours: number;
regularity: number;
approvalTimeliness: number;
};
}
// Insight interfaces
export interface AnalyticsInsight {
type: 'success' | 'warning' | 'error' | 'info';
category: 'activity' | 'content' | 'timeliness' | 'overall' | 'billing' | 'performance';
title: string;
description: string;
recommendation: string;
severity?: 'low' | 'medium' | 'high';
actionable?: boolean;
}
// Analysis interfaces
export interface TimeEntryAnalysis {
timeEntryId: number;
activityScore: ActivityScore;
contentScore: ContentScore;
timelinessScore: TimelinessScore;
overallScore: number; // 0 to 1
insights: AnalyticsInsight[];
analyzedAt: Date;
}
export interface AggregateAnalysis {
totalEntries: number;
totalHours: number;
averageHoursPerEntry: number;
dateRange: {
earliest: Date;
latest: Date;
};
scores: {
activity: number;
content: number;
timeliness: number;
overall: number;
};
insights: AnalyticsInsight[];
patterns: {
dayOfWeek: number[]; // 7 values, Sunday = 0
hourly: number[]; // 24 values
};
trends: {
weekly: Array<{
week: Date;
hours: number;
entries: number;
}>;
};
analyzedAt: Date;
}
// Timeline interfaces
export interface TimelineEvent {
id: string;
type: 'time_entry' | 'key_moment' | 'milestone';
timestamp: Date;
title: string;
description?: string;
duration?: number; // in hours
metadata?: Record<string, any>;
score?: number; // 0 to 1
isHumanActivity: boolean;
importance: 'low' | 'medium' | 'high' | 'critical';
}
export interface TimelineView {
events: TimelineEvent[];
dateRange: {
start: Date;
end: Date;
};
timeRange: 'hour' | 'day' | 'week' | 'month';
filters: {
resourceIds?: number[];
ticketIds?: number[];
projectIds?: number[];
activityTypes?: string[];
minScore?: number;
};
summary: {
totalEvents: number;
humanActivities: number;
systemActivities: number;
totalHours: number;
averageScore: number;
};
}
// LLM Analysis interfaces
export interface LLMAnalysisRequest {
timeEntries: Array<{
id: number;
notes?: string;
title?: string;
hours_worked: number;
entry_date: string;
resource_name?: string;
ticket_title?: string;
}>;
analysisType: 'productivity' | 'quality' | 'patterns' | 'anomalies' | 'comprehensive';
context?: {
timeRange: string;
resourceIds?: number[];
projectIds?: number[];
};
}
export interface LLMAnalysisResponse {
insights: string[];
patterns: Array<{
type: string;
description: string;
frequency: number;
impact: 'low' | 'medium' | 'high';
}>;
recommendations: Array<{
category: string;
priority: 'low' | 'medium' | 'high';
action: string;
expectedImpact: string;
}>;
summary: {
overallQuality: number; // 0 to 1
productivityLevel: number; // 0 to 1
keyFindings: string[];
};
processingTime: number; // milliseconds
tokensUsed: number;
}
// Scoring algorithm interfaces
export interface ScoringWeights {
activity: number;
content: number;
timeliness: number;
}
export interface ScoringConfiguration {
weights: ScoringWeights;
thresholds: {
excellent: number;
good: number;
average: number;
poor: number;
};
factors: {
activity: {
completeness: number;
consistency: number;
duration: number;
categorization: number;
};
content: {
notesQuality: number;
titleClarity: number;
internalNotes: number;
technicalDetail: number;
};
timeliness: {
entryDelay: number;
businessHours: number;
regularity: number;
approvalTimeliness: number;
};
};
}
// Analytics query interfaces
export interface AnalyticsQuery {
timeRange: {
start: Date;
end: Date;
};
filters: {
resourceIds?: number[];
ticketIds?: number[];
taskIds?: number[];
projectIds?: number[];
companyIds?: number[];
minHours?: number;
maxHours?: number;
billable?: boolean;
approved?: boolean;
activityTypes?: string[];
};
groupBy?: 'resource' | 'ticket' | 'project' | 'company' | 'day' | 'week' | 'month';
includeScores?: boolean;
includeInsights?: boolean;
includePatterns?: boolean;
includeTrends?: boolean;
}
export interface AnalyticsQueryResult {
data: Array<{
group: string;
totalEntries: number;
totalHours: number;
averageHoursPerEntry: number;
scores?: {
activity: number;
content: number;
timeliness: number;
overall: number;
};
insights?: AnalyticsInsight[];
}>;
summary: AggregateAnalysis;
query: AnalyticsQuery;
processedAt: Date;
}
// Export and reporting interfaces
export interface AnalyticsExport {
format: 'csv' | 'excel' | 'pdf' | 'json';
data: {
timeEntries: any[];
analyses: TimeEntryAnalysis[];
summary: AggregateAnalysis;
insights: AnalyticsInsight[];
};
metadata: {
exportedAt: Date;
timeRange: string;
filters: string;
recordCount: number;
};
}
// Performance metrics
export interface AnalyticsPerformanceMetrics {
processingTime: number; // milliseconds
recordsProcessed: number;
recordsPerSecond: number;
memoryUsage: number; // MB
cacheHitRate: number; // percentage
errors: string[];
}

View file

@ -183,6 +183,8 @@ export interface ConfigurationItem {
setupFee?: number;
sourceProductID?: number;
type?: number;
configurationItemType?: number; // API field name (camelCase)
configuration_item_type?: number; // Database field name (snake_case)
vendorID?: number;
vendorName?: string;
warrantyExpirationDate?: string;
@ -207,6 +209,7 @@ export interface PicklistValue {
sortOrder?: number;
isActive?: boolean;
isSystem?: boolean;
parentValue?: number | string;
}
export interface EntityField {
@ -221,6 +224,53 @@ export interface EntityField {
picklistValues?: PicklistValue[];
}
export interface AutotaskTimeEntry {
id: number;
resourceID: number;
ticketID?: number;
taskID?: number;
projectID?: number;
companyID?: number;
dateWorked: string; // ISO date string - Autotask uses dateWorked
hoursWorked: number; // Hours worked on this entry
summaryNotes?: string; // Autotask uses summaryNotes not notes
internalNotes?: string;
title?: string;
type?: number;
startDateTime?: string; // ISO datetime string
endDateTime?: string; // ISO datetime string
billable?: boolean;
billingRate?: number;
billingRateCurrencyID?: number;
costRate?: number;
costRateCurrencyID?: number;
cost?: number;
costCurrencyID?: number;
revenue?: number;
revenueCurrencyID?: number;
margin?: number;
marginCurrencyID?: number;
approved?: boolean;
approvedByResourceID?: number;
approvedDateTime?: string; // ISO datetime string
nonBillable?: boolean;
contractServiceID?: number;
contractServiceBundleID?: number;
roleID?: number;
departmentID?: number;
locationID?: number;
allocationCodeID?: number;
impProjectScheduleID?: number;
impProjectScheduleTaskID?: number;
apiVendorID?: number;
createDate: string; // ISO datetime string
lastModifiedDate?: string; // ISO datetime string
userDefinedFields?: Array<{
name: string;
value: any;
}>;
}
export interface ApiResponse<T> {
item?: T;
items?: T[];

111
lib/types/auvik.ts Normal file
View file

@ -0,0 +1,111 @@
// Auvik API Type Definitions
export interface AuvikNetworkInterface {
interfaceName: string;
status: string;
speed?: number;
macAddress?: string;
ipAddress?: string;
vlan?: string;
}
export interface AuvikDevice {
id: string;
deviceName: string;
serialNumber?: string;
macAddresses?: string[];
ipAddresses: string[];
deviceType: string;
manufacturer?: string;
model?: string;
makeModel?: string;
vendorName?: string;
firmwareVersion?: string;
softwareVersion?: string;
onlineStatus: 'online' | 'offline' | 'unknown';
lastSeenTime?: string;
uptime?: number;
tenantId: string;
tenantName?: string;
description?: string;
networkInterfaces?: AuvikNetworkInterface[];
}
export interface AuvikTenant {
id: string;
domainPrefix: string;
tenantType: 'multiClient' | 'client';
parentId?: string;
}
export interface AuvikDeviceResponse {
data: Array<{
type: string;
id: string;
attributes: {
ipAddresses: string[];
deviceName: string;
deviceType: string;
makeModel?: string;
vendorName?: string;
softwareVersion?: string;
serialNumber?: string;
description?: string;
firmwareVersion?: string;
lastModified?: string;
lastSeenTime?: string;
onlineStatus: string;
};
relationships?: {
tenant?: {
data: {
type: string;
id: string;
attributes?: {
domainPrefix: string;
};
};
};
};
}>;
links?: {
next?: string;
first?: string;
last?: string;
};
}
export interface AuvikTenantResponse {
data: Array<{
type: string;
id: string;
attributes: {
domainPrefix: string;
tenantType: string;
};
relationships?: {
parent?: {
data: {
type: string;
id: string;
};
};
};
}>;
}
export interface AuvikClientConfig {
apiUrl: string;
apiUser: string;
apiKey: string;
}
export interface AuvikTenantMapping {
id: number;
auvikTenantId: string;
auvikTenantName: string;
autotaskCompanyId: number;
autotaskCompanyName: string;
createdAt: string;
updatedAt: string;
}

513
lib/types/database.ts Normal file
View file

@ -0,0 +1,513 @@
/**
* Database Types and Interfaces
* TypeScript definitions matching PostgreSQL table schemas
*/
// Base audit fields present in all tables
export interface AuditFields {
created_at: Date;
updated_at: Date;
synced_at: Date;
is_deleted: boolean;
deleted_at?: Date | null;
}
// Company entity
export interface Company extends AuditFields {
id: number;
company_name?: string | null;
company_number?: string | null;
phone?: string | null;
fax?: string | null;
website?: string | null;
address1?: string | null;
address2?: string | null;
city?: string | null;
state?: string | null;
postal_code?: string | null;
country?: string | null;
is_active?: boolean;
company_type?: number | null;
owner_resource_id?: number | null;
territory_id?: number | null;
market_segment_id?: number | null;
competitor_id?: number | null;
billing_address1?: string | null;
billing_address2?: string | null;
billing_city?: string | null;
billing_state?: string | null;
billing_postal_code?: string | null;
billing_country?: string | null;
tax_id?: string | null;
tax_exempt?: boolean;
tax_region_id?: number | null;
currency_id?: number | null;
invoice_method?: number | null;
invoice_template_id?: number | null;
quote_template_id?: number | null;
key_account_icon?: number | null;
last_activity_date?: Date | null;
last_tracked_modification_date_time?: Date | null;
api_vendor_id?: number | null;
}
// Resource (User) entity
export interface Resource extends AuditFields {
id: number;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
user_name?: string | null;
title?: string | null;
office_phone?: string | null;
mobile_phone?: string | null;
office_extension?: string | null;
is_active?: boolean;
location_id?: number | null;
resource_type?: number | null;
pay_roll_identifier?: string | null;
hire_date?: Date | null;
travel_availability_pct?: number | null;
survey_resource_rating?: number | null;
}
// Contact entity
export interface Contact extends AuditFields {
id: number;
company_id: number;
first_name?: string | null;
last_name?: string | null;
title?: string | null;
email_address?: string | null;
email_address2?: string | null;
email_address3?: string | null;
phone?: string | null;
extension?: string | null;
alternate_phone?: string | null;
mobile_phone?: string | null;
fax?: string | null;
address_line?: string | null;
address_line1?: string | null;
city?: string | null;
state?: string | null;
zip_code?: string | null;
country?: string | null;
is_active?: boolean;
name_prefix?: string | null;
name_suffix?: string | null;
facebook_url?: string | null;
twitter_url?: string | null;
linked_in_url?: string | null;
primary_contact?: boolean;
account_physical_location_id?: number | null;
solicitation_opt_out?: boolean;
room_number?: string | null;
last_activity_date?: Date | null;
last_modified_date?: Date | null;
api_vendor_id?: number | null;
}
// Project entity
export interface Project extends AuditFields {
id: number;
company_id: number;
project_name?: string | null;
project_number?: string | null;
description?: string | null;
start_date_time?: Date | null;
end_date_time?: Date | null;
estimated_time?: number | null;
actual_hours?: number | null;
estimated_sale_cost?: number | null;
labor_estimated_costs?: number | null;
labor_estimated_revenue?: number | null;
project_cost_estimated_margin_percentage?: number | null;
status?: number | null;
type?: number | null;
project_lead_resource_id?: number | null;
account_executive_resource_id?: number | null;
owner_resource_id?: number | null;
creator_resource_id?: number | null;
completed_percentage?: number | null;
completed_date_time?: Date | null;
duration?: number | null;
original_estimated_revenue?: number | null;
estimated_time_cost?: number | null;
purchase_order_number?: string | null;
business_division_subdivision_id?: number | null;
line_of_business_id?: number | null;
department?: number | null;
last_activity_date_time?: Date | null;
last_activity_person_type?: number | null;
last_activity_resource_id?: number | null;
}
// Ticket entity
export interface Ticket extends AuditFields {
id: number;
company_id: number;
ticket_number?: string | null;
title?: string | null;
description?: string | null;
status?: number | null;
priority?: number | null;
queue_id?: number | null;
issue_type?: number | null;
sub_issue_type?: number | null;
source?: number | null;
assigned_resource_id?: number | null;
assigned_resource_role_id?: number | null;
contact_id?: number | null;
account_physical_location_id?: number | null;
due_date_time?: Date | null;
estimated_hours?: number | null;
completed_date?: Date | null;
create_date?: Date | null;
created_by_contact_id?: number | null;
last_activity_date?: Date | null;
last_customer_notification_date_time?: Date | null;
last_customer_visible_activity_date_time?: Date | null;
first_response_date_time?: Date | null;
resolution_plan_date_time?: Date | null;
resolved_date_time?: Date | null;
first_response_assigned_resource_id?: number | null;
first_response_initiating_resource_id?: number | null;
project_id?: number | null;
opportunity_id?: number | null;
change_approval_board?: number | null;
change_approval_status?: number | null;
change_approval_type?: number | null;
change_info_field1?: string | null;
change_info_field2?: string | null;
change_info_field3?: string | null;
change_info_field4?: string | null;
change_info_field5?: string | null;
contract_id?: number | null;
monitor_id?: number | null;
monitor_type_id?: number | null;
ticket_type?: number | null;
ticket_category?: number | null;
service_level_agreement_id?: number | null;
resolution?: string | null;
purchase_order_number?: string | null;
ticket_completion_date?: Date | null;
last_activity_person_type?: number | null;
last_activity_resource_id?: number | null;
current_service_thermometer_rating?: number | null;
previous_service_thermometer_rating?: number | null;
service_thermometer_temperature?: number | null;
api_vendor_id?: number | null;
}
// Task entity
export interface Task extends AuditFields {
id: number;
title?: string | null;
description?: string | null;
status?: number | null;
priority?: number | null;
assigned_resource_id?: number | null;
assigned_resource_role_id?: number | null;
department_id?: number | null;
estimated_hours?: number | null;
remaining_hours?: number | null;
hours_to_be_scheduled?: number | null;
start_date_time?: Date | null;
end_date_time?: Date | null;
completed_date_time?: Date | null;
create_date_time?: Date | null;
creator_resource_id?: number | null;
completed_by_resource_id?: number | null;
last_activity_date_time?: Date | null;
project_id?: number | null;
ticket_id?: number | null;
phase_id?: number | null;
allocation_code_id?: number | null;
task_type?: number | null;
task_is_billable?: boolean;
task_number?: string | null;
purchase_order_number?: string | null;
can_client_portal_user_complete_task?: boolean;
creator_type?: number | null;
task_category_id?: number | null;
}
// Configuration Item entity
export interface ConfigurationItem extends AuditFields {
id: number;
company_id: number;
product_id?: number | null;
reference_title?: string | null;
reference_number?: string | null;
serial_number?: string | null;
install_date?: Date | null;
warranty_expiration_date?: Date | null;
is_active?: boolean;
daily_cost?: number | null;
hourly_cost?: number | null;
monthly_cost?: number | null;
per_use_cost?: number | null;
setup_fee?: number | null;
contact_id?: number | null;
location_id?: number | null;
vendor_id?: number | null;
installed_by_id?: number | null;
installed_by_contact_id?: number | null;
parent_configuration_item_id?: number | null;
notes?: string | null;
create_date?: Date | null;
created_by_person_id?: number | null;
last_modified_time?: Date | null;
last_activity_person_type?: number | null;
impersonator_creator_resource_id?: number | null;
configuration_item_category_id?: number | null;
configuration_item_type?: number | null;
datto_availability?: number | null;
datto_device_memory_megabytes?: number | null;
datto_drives_errors?: boolean | null;
datto_hostname?: string | null;
datto_internal_ip?: string | null;
datto_kernel_version_id?: number | null;
datto_last_check_in_date_time?: Date | null;
datto_nic_speed_kilobits_per_second?: number | null;
datto_number_of_agents?: number | null;
datto_number_of_drives?: number | null;
datto_number_of_logical_volumes?: number | null;
datto_number_of_volumes?: number | null;
datto_off_site_storage_used_bytes?: number | null;
datto_os_version_id?: number | null;
datto_percentage_used?: number | null;
datto_protected_kilobytes?: number | null;
datto_remote_ip?: string | null;
datto_serial_number?: string | null;
datto_uptime_seconds?: number | null;
datto_used_kilobytes?: number | null;
datto_z_pool_percentage?: number | null;
device_networking_id?: number | null;
last_backup_date?: Date | null;
last_backup_status?: number | null;
os_version_id?: number | null;
service_id?: number | null;
service_bundle_id?: number | null;
snmp_location?: string | null;
snmp_name?: string | null;
snmp_contact?: string | null;
api_vendor_id?: number | null;
device_type?: string | null;
rmm_device_uid?: string | null;
rmm_device_audit_architecture_id?: number | null;
rmm_device_audit_display_adaptor_id?: number | null;
rmm_device_audit_domain_id?: number | null;
rmm_device_audit_external_ip_address?: string | null;
rmm_device_audit_hostname?: string | null;
rmm_device_audit_ip_address?: string | null;
rmm_device_audit_mac_address?: string | null;
rmm_device_audit_manufacturer_id?: number | null;
rmm_device_audit_missing_patch_count?: number | null;
rmm_device_audit_mobile_network_operator_id?: number | null;
rmm_device_audit_mobile_number?: string | null;
rmm_device_audit_model_id?: number | null;
rmm_device_audit_motherboard_id?: number | null;
rmm_device_audit_operating_system_id?: number | null;
rmm_device_audit_processor_id?: number | null;
rmm_device_audit_service_pack_id?: number | null;
rmm_device_audit_snmp_contact?: string | null;
rmm_device_audit_snmp_location?: string | null;
rmm_device_audit_snmp_name?: string | null;
rmm_device_audit_software_status_id?: number | null;
rmm_device_audit_storage_bytes?: number | null;
rmm_open_alert_count?: number | null;
rmm_device_audit_description?: string | null;
rmm_device_audit_device_type_id?: number | null;
rmm_device_audit_last_user?: string | null;
rmm_device_audit_memory_bytes?: number | null;
source_cost_id?: number | null;
source_cost_type?: number | null;
}
// Contract entity
export interface Contract extends AuditFields {
id: number;
company_id: number;
contract_name?: string | null;
contract_number?: string | null;
description?: string | null;
start_date?: Date | null;
end_date?: Date | null;
time_reporting_requires_start_and_stop_times?: number | null;
service_level_agreement_id?: number | null;
contract_type?: number | null;
contract_category?: number | null;
status?: number | null;
business_division_subdivision_id?: number | null;
contact_id?: number | null;
contact_name?: string | null;
billing_preference?: number | null;
purchase_order_number?: string | null;
setup_fee?: number | null;
setup_fee_allocation_code_id?: number | null;
estimated_cost?: number | null;
estimated_hours?: number | null;
estimated_revenue?: number | null;
over_budget_dollar_amount?: number | null;
over_budget_hours?: number | null;
contract_period_type?: string | null;
opportunity_id?: number | null;
renewed_contract_id?: number | null;
is_default_contract?: boolean;
internal_currency_setup_fee?: number | null;
internal_currency_over_budget_dollar_amount?: number | null;
internal_currency_estimated_cost?: number | null;
internal_currency_estimated_revenue?: number | null;
exclusion_contract_id?: number | null;
internal_currency_monthly_revenue?: number | null;
internal_currency_quarterly_revenue?: number | null;
internal_currency_semi_annual_revenue?: number | null;
internal_currency_yearly_revenue?: number | null;
internal_currency_one_time_revenue?: number | null;
compliance?: boolean | null;
}
// Billing Item entity
export interface BillingItem extends AuditFields {
id: number;
company_id?: number | null;
product_id?: number | null;
description?: string | null;
quantity?: number | null;
rate?: number | null;
total_amount?: number | null;
line_discount_dollars?: number | null;
line_discount_percent?: number | null;
tax_category_id?: number | null;
internal_currency_line_discount_dollars?: number | null;
allocation_code_id?: number | null;
invoice_id?: number | null;
vendor_id?: number | null;
expense_item?: boolean;
task_id?: number | null;
ticket_id?: number | null;
project_id?: number | null;
our_cost?: number | null;
list_price?: number | null;
unit_cost?: number | null;
unit_price?: number | null;
extended_price?: number | null;
tax_dollars?: number | null;
internal_currency_unit_price?: number | null;
internal_currency_total_amount?: number | null;
}
// Picklist base interface
export interface PicklistValue extends AuditFields {
value: number;
label: string;
is_active?: boolean;
is_system?: boolean;
sort_order?: number | null;
parent_value?: number | null;
}
// Status picklist
export interface Status extends PicklistValue {}
// Issue Type picklist
export interface IssueType extends PicklistValue {}
// Sub-Issue Type picklist
export interface SubIssueType extends PicklistValue {}
// Work Type picklist
export interface WorkType extends PicklistValue {}
// Sync History (matches sync_history table)
export interface SyncHistoryRecord {
id: number;
entity_type: string;
sync_type: 'full' | 'incremental' | 'entity-specific';
status: 'started' | 'in_progress' | 'completed' | 'failed';
started_at: Date;
completed_at?: Date | null;
records_added: number;
records_updated: number;
records_deleted: number;
error_message?: string | null;
triggered_by?: string | null;
}
// Time Entry entity
export interface TimeEntry extends AuditFields {
id: number;
resource_id: number;
ticket_id?: number | null;
task_id?: number | null;
project_id?: number | null;
company_id?: number | null;
entry_date: Date;
hours_worked: number;
notes?: string | null;
internal_notes?: string | null;
title?: string | null;
type?: number | null;
start_date_time?: Date | null;
end_date_time?: Date | null;
billable?: boolean;
billing_rate?: number | null;
billing_rate_currency_id?: number | null;
cost_rate?: number | null;
cost_rate_currency_id?: number | null;
cost?: number | null;
cost_currency_id?: number | null;
revenue?: number | null;
revenue_currency_id?: number | null;
margin?: number | null;
margin_currency_id?: number | null;
approved?: boolean;
approved_by_resource_id?: number | null;
approved_date_time?: Date | null;
non_billable?: boolean;
contract_service_id?: number | null;
contract_service_bundle_id?: number | null;
role_id?: number | null;
department_id?: number | null;
location_id?: number | null;
allocation_code_id?: number | null;
imp_project_schedule_id?: number | null;
imp_project_schedule_task_id?: number | null;
api_vendor_id?: number | null;
}
// Union type for all entities
export type Entity =
| Company
| Resource
| Contact
| Project
| Ticket
| Task
| ConfigurationItem
| Contract
| BillingItem
| Status
| IssueType
| SubIssueType
| WorkType
| TimeEntry;
// Table name type
export type TableName =
| 'companies'
| 'resources'
| 'contacts'
| 'projects'
| 'tickets'
| 'tasks'
| 'configuration_items'
| 'contracts'
| 'billing_items'
| 'statuses'
| 'issue_types'
| 'sub_issue_types'
| 'work_types'
| 'time_entries'
| 'sync_history';

266
lib/types/errors.ts Normal file
View file

@ -0,0 +1,266 @@
/**
* Custom Error Types for Sync Operations
*/
/**
* Base sync error class
*/
export class SyncError extends Error {
public readonly code: string;
public readonly context?: Record<string, any>;
public readonly isRetryable: boolean;
constructor(
message: string,
code: string,
context?: Record<string, any>,
isRetryable: boolean = false
) {
super(message);
this.name = 'SyncError';
this.code = code;
this.context = context;
this.isRetryable = isRetryable;
// Maintains proper stack trace for where our error was thrown
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
/**
* Network-related errors (connection failures, timeouts)
*/
export class NetworkError extends SyncError {
constructor(message: string, context?: Record<string, any>) {
super(message, 'NETWORK_ERROR', context, true);
this.name = 'NetworkError';
}
}
/**
* Authentication/Authorization errors
*/
export class AuthError extends SyncError {
constructor(message: string, context?: Record<string, any>) {
super(message, 'AUTH_ERROR', context, false);
this.name = 'AuthError';
}
}
/**
* Rate limit errors
*/
export class RateLimitError extends SyncError {
public readonly retryAfter?: number;
constructor(message: string, retryAfter?: number, context?: Record<string, any>) {
super(message, 'RATE_LIMIT_ERROR', context, true);
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
/**
* API-related errors
*/
export class ApiError extends SyncError {
public readonly statusCode?: number;
constructor(message: string, statusCode?: number, context?: Record<string, any>) {
super(message, 'API_ERROR', context, statusCode ? statusCode >= 500 : false);
this.name = 'ApiError';
this.statusCode = statusCode;
}
}
/**
* Database-related errors
*/
export class DatabaseError extends SyncError {
constructor(message: string, context?: Record<string, any>, isRetryable: boolean = false) {
super(message, 'DATABASE_ERROR', context, isRetryable);
this.name = 'DatabaseError';
}
}
/**
* Database constraint violation errors
*/
export class ConstraintError extends SyncError {
constructor(message: string, context?: Record<string, any>) {
super(message, 'DATABASE_CONSTRAINT_ERROR', context, false);
this.name = 'ConstraintError';
}
}
/**
* Data validation errors
*/
export class ValidationError extends SyncError {
public readonly validationErrors: Array<{ field: string; message: string }>;
constructor(
message: string,
validationErrors: Array<{ field: string; message: string }>,
context?: Record<string, any>
) {
super(message, 'VALIDATION_ERROR', context, false);
this.name = 'ValidationError';
this.validationErrors = validationErrors;
}
}
/**
* Data mapping errors
*/
export class MappingError extends SyncError {
constructor(message: string, context?: Record<string, any>) {
super(message, 'MAPPING_ERROR', context, false);
this.name = 'MappingError';
}
}
/**
* Configuration errors
*/
export class ConfigError extends SyncError {
constructor(message: string, context?: Record<string, any>) {
super(message, 'CONFIG_ERROR', context, false);
this.name = 'ConfigError';
}
}
/**
* Timeout errors
*/
export class TimeoutError extends SyncError {
constructor(message: string, context?: Record<string, any>) {
super(message, 'TIMEOUT_ERROR', context, true);
this.name = 'TimeoutError';
}
}
/**
* Helper function to categorize generic errors
*/
export function categorizeError(error: any): SyncError {
if (error instanceof SyncError) {
return error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
const errorString = errorMessage.toLowerCase();
// Network errors
if (
errorString.includes('econnrefused') ||
errorString.includes('etimedout') ||
errorString.includes('enotfound') ||
errorString.includes('network')
) {
return new NetworkError(errorMessage, { originalError: error });
}
// Auth errors
if (
errorString.includes('401') ||
errorString.includes('403') ||
errorString.includes('unauthorized') ||
errorString.includes('forbidden')
) {
return new AuthError(errorMessage, { originalError: error });
}
// Rate limit errors
if (errorString.includes('429') || errorString.includes('rate limit')) {
return new RateLimitError(errorMessage, undefined, { originalError: error });
}
// Database constraint errors
if (
errorString.includes('constraint') ||
errorString.includes('duplicate') ||
errorString.includes('unique violation')
) {
return new ConstraintError(errorMessage, { originalError: error });
}
// Database errors
if (
errorString.includes('query') ||
errorString.includes('sql') ||
errorString.includes('database') ||
errorString.includes('postgres')
) {
return new DatabaseError(errorMessage, { originalError: error });
}
// Validation errors
if (errorString.includes('validation') || errorString.includes('invalid')) {
return new ValidationError(errorMessage, [], { originalError: error });
}
// Mapping errors
if (errorString.includes('mapping') || errorString.includes('transform')) {
return new MappingError(errorMessage, { originalError: error });
}
// Timeout errors
if (errorString.includes('timeout') || errorString.includes('timed out')) {
return new TimeoutError(errorMessage, { originalError: error });
}
// API errors (check for HTTP status codes)
const statusMatch = errorString.match(/\b([45]\d{2})\b/);
if (statusMatch) {
const statusCode = parseInt(statusMatch[1]);
return new ApiError(errorMessage, statusCode, { originalError: error });
}
// Default to generic sync error
return new SyncError(errorMessage, 'UNKNOWN_ERROR', { originalError: error });
}
/**
* Check if error is retryable
*/
export function isRetryableError(error: any): boolean {
if (error instanceof SyncError) {
return error.isRetryable;
}
const categorized = categorizeError(error);
return categorized.isRetryable;
}
/**
* Format error for logging
*/
export function formatErrorForLog(error: any): {
message: string;
code: string;
stack?: string;
context?: Record<string, any>;
isRetryable: boolean;
} {
if (error instanceof SyncError) {
return {
message: error.message,
code: error.code,
stack: error.stack,
context: error.context,
isRetryable: error.isRetryable,
};
}
const categorized = categorizeError(error);
return {
message: categorized.message,
code: categorized.code,
stack: categorized.stack,
context: categorized.context,
isRetryable: categorized.isRetryable,
};
}

194
lib/types/sync.ts Normal file
View file

@ -0,0 +1,194 @@
/**
* Sync Types and Interfaces
* TypeScript definitions for sync operations
*/
// Entity types that can be synced from Autotask
export enum EntityType {
COMPANIES = 'companies',
TICKETS = 'tickets',
TASKS = 'tasks',
PROJECTS = 'projects',
RESOURCES = 'resources',
STATUSES = 'statuses',
ISSUE_TYPES = 'issue_types',
SUB_ISSUE_TYPES = 'sub_issue_types',
WORK_TYPES = 'work_types',
BILLING_ITEMS = 'billing_items',
CONFIGURATION_ITEMS = 'configuration_items',
CONTACTS = 'contacts',
CONTRACTS = 'contracts',
TIME_ENTRIES = 'time_entries',
}
// Sync operation types
export enum SyncType {
FULL = 'full',
INCREMENTAL = 'incremental',
ENTITY_SPECIFIC = 'entity-specific',
}
// Sync status
export enum SyncStatus {
STARTED = 'started',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
FAILED = 'failed',
}
// Sync configuration
export interface SyncConfig {
entities: EntityType[];
syncType: SyncType;
triggeredBy?: string;
batchSize?: number;
rateLimit?: number; // requests per second
yearsBack?: number; // Number of years to look back for time-based entities (default: 2)
}
// Sync progress information
export interface SyncProgress {
syncId: string;
entityType: EntityType;
status: SyncStatus;
currentPage?: number;
totalPages?: number;
recordsProcessed: number;
recordsAdded: number;
recordsUpdated: number;
recordsDeleted: number;
startedAt: Date;
estimatedCompletion?: Date;
error?: string;
// Chunking support
currentChunk?: number;
totalChunks?: number;
chunkDescription?: string; // e.g., "Jan 2024"
}
// Chunk progress for detailed tracking
export interface ChunkProgress {
chunkIndex: number;
totalChunks: number;
startDate: Date;
endDate: Date;
description: string; // e.g., "January 2024"
status: 'pending' | 'in_progress' | 'completed' | 'failed';
recordsProcessed: number;
error?: string;
}
// Sync history record (matches database table)
export interface SyncHistory {
id: number;
entity_type: string;
sync_type: SyncType;
status: SyncStatus;
started_at: Date;
completed_at?: Date;
records_added: number;
records_updated: number;
records_deleted: number;
error_message?: string;
triggered_by?: string;
}
// Alias for database compatibility
export interface SyncHistoryRecord {
id: number;
entity_type: string;
sync_type: SyncType;
status: SyncStatus;
started_at: Date;
completed_at?: Date;
records_added: number;
records_updated: number;
records_deleted: number;
error_message?: string;
triggered_by?: string;
}
// Sync result for a single entity
export interface EntitySyncResult {
entityType: EntityType;
success: boolean;
recordsAdded: number;
recordsUpdated: number;
recordsDeleted: number;
duration: number; // milliseconds
error?: string;
}
// Overall sync result
export interface SyncResult {
syncId: string;
syncType: SyncType;
status: SyncStatus;
entities: EntitySyncResult[];
totalRecordsAdded: number;
totalRecordsUpdated: number;
totalRecordsDeleted: number;
startedAt: Date;
completedAt?: Date;
duration?: number; // milliseconds
errors: string[];
}
// Sync state (stored in Redis for real-time updates)
export interface SyncState {
syncId: string;
status: SyncStatus;
progress: SyncProgress[];
startedAt: Date;
lastUpdated: Date;
}
// Entity dependency map (for determining sync order)
export const ENTITY_DEPENDENCIES: Record<EntityType, EntityType[]> = {
[EntityType.COMPANIES]: [], // No dependencies
[EntityType.RESOURCES]: [], // No dependencies
[EntityType.STATUSES]: [], // No dependencies
[EntityType.ISSUE_TYPES]: [], // No dependencies
[EntityType.SUB_ISSUE_TYPES]: [], // No dependencies
[EntityType.WORK_TYPES]: [], // No dependencies
[EntityType.CONTACTS]: [EntityType.COMPANIES], // Depends on companies
[EntityType.PROJECTS]: [EntityType.COMPANIES, EntityType.RESOURCES], // Depends on companies and resources
[EntityType.TICKETS]: [EntityType.COMPANIES, EntityType.RESOURCES, EntityType.CONTACTS], // Depends on companies, resources, contacts
[EntityType.TASKS]: [EntityType.RESOURCES, EntityType.PROJECTS, EntityType.TICKETS], // Depends on resources, projects, tickets
[EntityType.CONFIGURATION_ITEMS]: [EntityType.COMPANIES, EntityType.CONTACTS], // Depends on companies and contacts
[EntityType.CONTRACTS]: [EntityType.COMPANIES, EntityType.CONTACTS], // Depends on companies and contacts
[EntityType.BILLING_ITEMS]: [EntityType.COMPANIES, EntityType.TASKS, EntityType.TICKETS, EntityType.PROJECTS], // Depends on multiple entities
[EntityType.TIME_ENTRIES]: [EntityType.COMPANIES, EntityType.RESOURCES, EntityType.CONTACTS, EntityType.PROJECTS, EntityType.TASKS, EntityType.TICKETS], // Depends on many entities
};
// Autotask API field names (for incremental sync)
export interface AutotaskQueryFilter {
field: string;
op: 'eq' | 'noteq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'beginsWith' | 'endsWith';
value: any;
}
// Autotask query options
export interface AutotaskQueryOptions {
filters?: AutotaskQueryFilter[];
pageSize?: number;
includeFields?: string[];
maxRecords?: number;
}
// Last sync information per entity
export interface LastSyncInfo {
entityType: EntityType;
lastSyncTime?: Date;
lastSyncStatus: SyncStatus;
recordCount: number;
}
// Sync notification
export interface SyncNotification {
syncId: string;
type: 'success' | 'failure' | 'warning';
message: string;
entityType?: EntityType;
timestamp: Date;
}