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:
parent
e8462ef301
commit
6eee14f8af
171 changed files with 32671 additions and 621 deletions
541
lib/utils/entity-mapper.ts
Normal file
541
lib/utils/entity-mapper.ts
Normal file
|
|
@ -0,0 +1,541 @@
|
|||
/**
|
||||
* Entity Mapper
|
||||
* Maps Autotask API responses to PostgreSQL database schema format
|
||||
*/
|
||||
|
||||
import { EntityType } from '../types/sync';
|
||||
|
||||
/**
|
||||
* Map Autotask field names to PostgreSQL column names
|
||||
* Autotask uses camelCase, PostgreSQL uses snake_case
|
||||
*/
|
||||
export function toSnakeCase(str: string): string {
|
||||
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Autotask API response to database format
|
||||
* @param entity Entity type
|
||||
* @param data Autotask API response data
|
||||
* @returns Mapped data for PostgreSQL
|
||||
*/
|
||||
export function mapAutotaskToDatabase(
|
||||
entity: EntityType,
|
||||
data: any
|
||||
): Record<string, any> {
|
||||
// Entity-specific mappings use ORIGINAL data (not snake_cased)
|
||||
let mapped: Record<string, any>;
|
||||
|
||||
switch (entity) {
|
||||
case EntityType.COMPANIES:
|
||||
mapped = mapCompany(data);
|
||||
break;
|
||||
case EntityType.TICKETS:
|
||||
mapped = mapTicket(data);
|
||||
break;
|
||||
case EntityType.TASKS:
|
||||
mapped = mapTask(data);
|
||||
break;
|
||||
case EntityType.PROJECTS:
|
||||
mapped = mapProject(data);
|
||||
break;
|
||||
case EntityType.RESOURCES:
|
||||
mapped = mapResource(data);
|
||||
break;
|
||||
case EntityType.CONFIGURATION_ITEMS:
|
||||
mapped = mapConfigurationItem(data);
|
||||
break;
|
||||
case EntityType.CONTACTS:
|
||||
mapped = mapContact(data);
|
||||
break;
|
||||
case EntityType.CONTRACTS:
|
||||
mapped = mapContract(data);
|
||||
break;
|
||||
case EntityType.BILLING_ITEMS:
|
||||
mapped = mapBillingItem(data);
|
||||
break;
|
||||
case EntityType.TIME_ENTRIES:
|
||||
mapped = mapTimeEntry(data);
|
||||
break;
|
||||
case EntityType.STATUSES:
|
||||
case EntityType.ISSUE_TYPES:
|
||||
case EntityType.SUB_ISSUE_TYPES:
|
||||
case EntityType.WORK_TYPES:
|
||||
mapped = mapPicklist(data);
|
||||
break;
|
||||
default:
|
||||
// Fallback: auto-convert camelCase to snake_case
|
||||
mapped = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const snakeKey = toSnakeCase(key);
|
||||
mapped[snakeKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Add sync timestamp
|
||||
mapped.synced_at = new Date();
|
||||
|
||||
// Ensure is_deleted is false for new/updated records
|
||||
if (mapped.is_deleted === undefined) {
|
||||
mapped.is_deleted = false;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Company entity
|
||||
*/
|
||||
function mapCompany(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
company_name: data.companyName || data.name,
|
||||
company_number: data.companyNumber,
|
||||
phone: data.phone,
|
||||
fax: data.fax,
|
||||
website: data.webSiteURL,
|
||||
address1: data.address1,
|
||||
address2: data.address2,
|
||||
city: data.city,
|
||||
state: data.state,
|
||||
postal_code: data.postalCode,
|
||||
country: data.country,
|
||||
is_active: data.isActive !== undefined ? data.isActive : true,
|
||||
company_type: data.companyType,
|
||||
owner_resource_id: data.ownerResourceID,
|
||||
territory_id: data.territoryID,
|
||||
market_segment_id: data.marketSegmentID,
|
||||
competitor_id: data.competitorID,
|
||||
billing_address1: data.billingAddress1,
|
||||
billing_address2: data.billingAddress2,
|
||||
billing_city: data.billingCity,
|
||||
billing_state: data.billingState,
|
||||
billing_postal_code: data.billingPostalCode,
|
||||
billing_country: data.billingCountry,
|
||||
tax_id: data.taxID,
|
||||
tax_exempt: data.taxExempt || false,
|
||||
tax_region_id: data.taxRegionID,
|
||||
currency_id: data.currencyID,
|
||||
invoice_method: data.invoice_method,
|
||||
invoice_template_id: data.invoice_template_id,
|
||||
quote_template_id: data.quote_template_id,
|
||||
key_account_icon: data.key_account_icon,
|
||||
last_activity_date: data.last_activity_date,
|
||||
last_tracked_modification_date_time: data.last_tracked_modification_date_time || data.last_modified_date,
|
||||
api_vendor_id: data.api_vendor_id,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Ticket entity
|
||||
*/
|
||||
function mapTicket(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
company_id: data.companyID,
|
||||
ticket_number: data.ticketNumber,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
status: data.status,
|
||||
priority: data.priority,
|
||||
queue_id: data.queueID,
|
||||
issue_type: data.issueType,
|
||||
sub_issue_type: data.subIssueType,
|
||||
source: data.source,
|
||||
assigned_resource_id: data.assignedResourceID,
|
||||
assigned_resource_role_id: data.assignedResourceRoleID,
|
||||
contact_id: data.contactID,
|
||||
account_physical_location_id: data.companyLocationID,
|
||||
due_date_time: data.dueDateTime,
|
||||
estimated_hours: data.estimatedHours,
|
||||
completed_date: data.completedDate,
|
||||
create_date: data.createDate,
|
||||
created_by_contact_id: data.createdByContactID,
|
||||
last_activity_date: data.lastActivityDate,
|
||||
last_customer_notification_date_time: data.lastCustomerNotificationDateTime,
|
||||
last_customer_visible_activity_date_time: data.lastCustomerVisibleActivityDateTime,
|
||||
first_response_date_time: data.first_response_date_time,
|
||||
resolution_plan_date_time: data.resolution_plan_date_time,
|
||||
resolved_date_time: data.resolved_date_time,
|
||||
first_response_assigned_resource_id: data.first_response_assigned_resource_id,
|
||||
first_response_initiating_resource_id: data.first_response_initiating_resource_id,
|
||||
project_id: data.project_id,
|
||||
opportunity_id: data.opportunity_id,
|
||||
change_approval_board: data.change_approval_board,
|
||||
change_approval_status: data.change_approval_status,
|
||||
change_approval_type: data.change_approval_type,
|
||||
change_info_field1: data.change_info_field1,
|
||||
change_info_field2: data.change_info_field2,
|
||||
change_info_field3: data.change_info_field3,
|
||||
change_info_field4: data.change_info_field4,
|
||||
change_info_field5: data.change_info_field5,
|
||||
contract_id: data.contract_id,
|
||||
monitor_id: data.monitor_id,
|
||||
monitor_type_id: data.monitor_type_id,
|
||||
ticket_type: data.ticket_type,
|
||||
ticket_category: data.ticket_category,
|
||||
service_level_agreement_id: data.service_level_agreement_id,
|
||||
resolution: data.resolution,
|
||||
purchase_order_number: data.purchase_order_number,
|
||||
ticket_completion_date: data.ticket_completion_date,
|
||||
last_activity_person_type: data.last_activity_person_type,
|
||||
last_activity_resource_id: data.last_activity_resource_id,
|
||||
current_service_thermometer_rating: data.current_service_thermometer_rating,
|
||||
previous_service_thermometer_rating: data.previous_service_thermometer_rating,
|
||||
service_thermometer_temperature: data.service_thermometer_temperature,
|
||||
api_vendor_id: data.api_vendor_id,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Task entity
|
||||
*/
|
||||
function mapTask(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
status: data.status,
|
||||
priority: data.priority,
|
||||
assigned_resource_id: data.assignedResourceID,
|
||||
assigned_resource_role_id: data.assignedResourceRoleID,
|
||||
department_id: data.departmentID,
|
||||
estimated_hours: data.estimatedHours,
|
||||
remaining_hours: data.remainingHours,
|
||||
hours_to_be_scheduled: data.hoursToBeScheduled,
|
||||
start_date_time: data.startDateTime,
|
||||
end_date_time: data.endDateTime,
|
||||
completed_date_time: data.completedDateTime,
|
||||
create_date_time: data.createDateTime,
|
||||
creator_resource_id: data.creatorResourceID,
|
||||
completed_by_resource_id: data.completedByResourceID,
|
||||
last_activity_date_time: data.lastActivityDateTime,
|
||||
project_id: data.projectID,
|
||||
ticket_id: data.ticketID,
|
||||
phase_id: data.phaseID,
|
||||
allocation_code_id: data.allocationCodeID,
|
||||
task_type: data.taskType,
|
||||
task_is_billable: data.taskIsBillable !== undefined ? data.taskIsBillable : true,
|
||||
task_number: data.taskNumber,
|
||||
purchase_order_number: data.purchaseOrderNumber,
|
||||
can_client_portal_user_complete_task: data.canClientPortalUserCompleteTask || false,
|
||||
creator_type: data.creatorType,
|
||||
task_category_id: data.taskCategoryID,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Project entity
|
||||
*/
|
||||
function mapProject(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
company_id: data.companyID,
|
||||
project_name: data.projectName,
|
||||
project_number: data.projectNumber,
|
||||
description: data.description,
|
||||
start_date_time: data.start_date_time,
|
||||
end_date_time: data.end_date_time,
|
||||
estimated_time: data.estimated_time,
|
||||
actual_hours: data.actual_hours,
|
||||
estimated_sale_cost: data.estimated_sale_cost,
|
||||
labor_estimated_costs: data.labor_estimated_costs,
|
||||
labor_estimated_revenue: data.labor_estimated_revenue,
|
||||
project_cost_estimated_margin_percentage: data.project_cost_estimated_margin_percentage,
|
||||
status: data.status,
|
||||
type: data.type,
|
||||
project_lead_resource_id: data.project_lead_resource_id,
|
||||
account_executive_resource_id: data.account_executive_resource_id,
|
||||
owner_resource_id: data.owner_resource_id,
|
||||
creator_resource_id: data.creator_resource_id,
|
||||
completed_percentage: data.completed_percentage,
|
||||
completed_date_time: data.completed_date_time,
|
||||
duration: data.duration,
|
||||
original_estimated_revenue: data.original_estimated_revenue,
|
||||
estimated_time_cost: data.estimated_time_cost,
|
||||
purchase_order_number: data.purchase_order_number,
|
||||
business_division_subdivision_id: data.business_division_subdivision_id,
|
||||
line_of_business_id: data.line_of_business_id,
|
||||
department: data.department,
|
||||
last_activity_date_time: data.last_activity_date_time,
|
||||
last_activity_person_type: data.last_activity_person_type,
|
||||
last_activity_resource_id: data.last_activity_resource_id,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Resource entity
|
||||
*/
|
||||
function mapResource(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
email: data.email || data.email_address,
|
||||
user_name: data.user_name || data.username,
|
||||
title: data.title,
|
||||
office_phone: data.office_phone,
|
||||
mobile_phone: data.mobile_phone,
|
||||
office_extension: data.office_extension,
|
||||
is_active: data.is_active !== undefined ? data.is_active : true,
|
||||
location_id: data.location_id,
|
||||
resource_type: data.resource_type,
|
||||
pay_roll_identifier: data.pay_roll_identifier,
|
||||
hire_date: data.hire_date,
|
||||
travel_availability_pct: data.travel_availability_pct,
|
||||
survey_resource_rating: data.survey_resource_rating,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Configuration Item entity
|
||||
*/
|
||||
function mapConfigurationItem(data: any): Record<string, any> {
|
||||
// Configuration items have many fields, map all of them
|
||||
const mapped: Record<string, any> = {
|
||||
id: data.id,
|
||||
company_id: data.companyID,
|
||||
product_id: data.productID,
|
||||
reference_title: data.referenceTitle,
|
||||
reference_number: data.referenceNumber,
|
||||
serial_number: data.serialNumber,
|
||||
install_date: data.installDate,
|
||||
warranty_expiration_date: data.warrantyExpirationDate,
|
||||
is_active: data.isActive !== undefined ? data.isActive : true,
|
||||
contact_id: data.contactID,
|
||||
configuration_item_type: data.type,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
|
||||
// Add all other fields dynamically
|
||||
const fieldsToInclude = [
|
||||
'daily_cost', 'hourly_cost', 'monthly_cost', 'per_use_cost', 'setup_fee',
|
||||
'location_id', 'vendor_id', 'installed_by_id', 'installed_by_contact_id',
|
||||
'parent_configuration_item_id', 'notes', 'create_date', 'created_by_person_id',
|
||||
'last_modified_time', 'last_activity_person_type', 'impersonator_creator_resource_id',
|
||||
'configuration_item_category_id', 'configuration_item_type', 'device_type',
|
||||
'rmm_device_uid', 'api_vendor_id',
|
||||
];
|
||||
|
||||
// Add Datto RMM fields
|
||||
const dattoFields = Object.keys(data).filter(key => key.startsWith('datto_') || key.startsWith('rmm_'));
|
||||
fieldsToInclude.push(...dattoFields);
|
||||
|
||||
fieldsToInclude.forEach(field => {
|
||||
if (data[field] !== undefined) {
|
||||
mapped[field] = data[field];
|
||||
}
|
||||
});
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Contact entity
|
||||
*/
|
||||
function mapContact(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
company_id: data.companyID,
|
||||
first_name: data.firstName,
|
||||
last_name: data.lastName,
|
||||
title: data.title,
|
||||
email_address: data.emailAddress || data.email_address || data.email,
|
||||
email_address2: data.emailAddress2 || data.email_address2,
|
||||
email_address3: data.emailAddress3 || data.email_address3,
|
||||
phone: data.phone,
|
||||
extension: data.extension,
|
||||
alternate_phone: data.alternate_phone,
|
||||
mobile_phone: data.mobile_phone,
|
||||
fax: data.faxNumber || data.fax,
|
||||
address_line: data.addressLine || data.address_line,
|
||||
address_line1: data.addressLine1 || data.address_line1 || data.address_line_1,
|
||||
city: data.city,
|
||||
state: data.state,
|
||||
zip_code: data.zipCode || data.zip_code || data.postal_code,
|
||||
country: data.country,
|
||||
is_active: data.is_active !== undefined ? data.is_active : true,
|
||||
name_prefix: data.name_prefix,
|
||||
name_suffix: data.name_suffix,
|
||||
facebook_url: data.facebook_url,
|
||||
twitter_url: data.twitter_url,
|
||||
linked_in_url: data.linked_in_url,
|
||||
primary_contact: data.primary_contact || false,
|
||||
account_physical_location_id: data.account_physical_location_id,
|
||||
solicitation_opt_out: data.solicitation_opt_out || false,
|
||||
room_number: data.room_number,
|
||||
last_activity_date: data.last_activity_date,
|
||||
last_modified_date: data.last_modified_date,
|
||||
api_vendor_id: data.api_vendor_id,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Contract entity
|
||||
*/
|
||||
function mapContract(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
company_id: data.companyID,
|
||||
contract_name: data.contractName,
|
||||
contract_number: data.contractNumber,
|
||||
description: data.description,
|
||||
start_date: data.start_date,
|
||||
end_date: data.end_date,
|
||||
time_reporting_requires_start_and_stop_times: data.time_reporting_requires_start_and_stop_times,
|
||||
service_level_agreement_id: data.service_level_agreement_id,
|
||||
contract_type: data.contract_type,
|
||||
contract_category: data.contract_category,
|
||||
status: data.status,
|
||||
business_division_subdivision_id: data.business_division_subdivision_id,
|
||||
contact_id: data.contact_id,
|
||||
contact_name: data.contact_name,
|
||||
billing_preference: data.billing_preference,
|
||||
purchase_order_number: data.purchase_order_number,
|
||||
setup_fee: data.setup_fee,
|
||||
setup_fee_allocation_code_id: data.setup_fee_allocation_code_id,
|
||||
estimated_cost: data.estimated_cost,
|
||||
estimated_hours: data.estimated_hours,
|
||||
estimated_revenue: data.estimated_revenue,
|
||||
over_budget_dollar_amount: data.over_budget_dollar_amount,
|
||||
over_budget_hours: data.over_budget_hours,
|
||||
contract_period_type: data.contract_period_type,
|
||||
opportunity_id: data.opportunity_id,
|
||||
renewed_contract_id: data.renewed_contract_id,
|
||||
is_default_contract: data.is_default_contract || false,
|
||||
internal_currency_setup_fee: data.internal_currency_setup_fee,
|
||||
internal_currency_over_budget_dollar_amount: data.internal_currency_over_budget_dollar_amount,
|
||||
internal_currency_estimated_cost: data.internal_currency_estimated_cost,
|
||||
internal_currency_estimated_revenue: data.internal_currency_estimated_revenue,
|
||||
exclusion_contract_id: data.exclusion_contract_id,
|
||||
internal_currency_monthly_revenue: data.internal_currency_monthly_revenue,
|
||||
internal_currency_quarterly_revenue: data.internal_currency_quarterly_revenue,
|
||||
internal_currency_semi_annual_revenue: data.internal_currency_semi_annual_revenue,
|
||||
internal_currency_yearly_revenue: data.internal_currency_yearly_revenue,
|
||||
internal_currency_one_time_revenue: data.internal_currency_one_time_revenue,
|
||||
compliance: data.compliance,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Billing Item entity
|
||||
*/
|
||||
function mapBillingItem(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
company_id: data.companyID,
|
||||
product_id: data.productID,
|
||||
description: data.description,
|
||||
quantity: data.quantity,
|
||||
rate: data.rate,
|
||||
total_amount: data.total_amount,
|
||||
line_discount_dollars: data.line_discount_dollars,
|
||||
line_discount_percent: data.line_discount_percent,
|
||||
tax_category_id: data.tax_category_id,
|
||||
internal_currency_line_discount_dollars: data.internal_currency_line_discount_dollars,
|
||||
allocation_code_id: data.allocation_code_id,
|
||||
invoice_id: data.invoice_id,
|
||||
vendor_id: data.vendor_id,
|
||||
expense_item: data.expense_item || false,
|
||||
task_id: data.task_id,
|
||||
ticket_id: data.ticket_id,
|
||||
project_id: data.project_id,
|
||||
our_cost: data.our_cost,
|
||||
list_price: data.list_price,
|
||||
unit_cost: data.unit_cost,
|
||||
unit_price: data.unit_price,
|
||||
extended_price: data.extended_price,
|
||||
tax_dollars: data.tax_dollars,
|
||||
internal_currency_unit_price: data.internal_currency_unit_price,
|
||||
internal_currency_total_amount: data.internal_currency_total_amount,
|
||||
synced_at: data.synced_at,
|
||||
is_deleted: data.is_deleted || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Time Entry entity
|
||||
*/
|
||||
function mapTimeEntry(data: any): Record<string, any> {
|
||||
return {
|
||||
id: data.id,
|
||||
resource_id: data.resourceID,
|
||||
ticket_id: data.ticketID,
|
||||
task_id: data.taskID,
|
||||
project_id: data.projectID,
|
||||
company_id: data.companyID,
|
||||
entry_date: data.dateWorked, // Autotask API returns dateWorked
|
||||
hours_worked: data.hoursWorked, // Autotask API returns hoursWorked
|
||||
notes: data.summaryNotes, // Autotask uses summaryNotes
|
||||
internal_notes: data.internalNotes,
|
||||
title: data.title,
|
||||
type: data.type,
|
||||
start_date_time: data.startDateTime,
|
||||
end_date_time: data.endDateTime,
|
||||
billable: data.billable, // Autotask returns billable
|
||||
billing_rate: data.billingRate,
|
||||
billing_rate_currency_id: data.billingRateCurrencyID,
|
||||
cost_rate: data.costRate,
|
||||
cost_rate_currency_id: data.costRateCurrencyID,
|
||||
cost: data.cost,
|
||||
cost_currency_id: data.costCurrencyID,
|
||||
revenue: data.revenue,
|
||||
revenue_currency_id: data.revenueCurrencyID,
|
||||
margin: data.margin,
|
||||
margin_currency_id: data.marginCurrencyID,
|
||||
approved: data.approved,
|
||||
approved_by_resource_id: data.approvedByResourceID,
|
||||
approved_date_time: data.approvedDateTime,
|
||||
non_billable: data.nonBillable,
|
||||
contract_service_id: data.contractServiceID,
|
||||
contract_service_bundle_id: data.contractServiceBundleID,
|
||||
role_id: data.roleID,
|
||||
department_id: data.departmentID,
|
||||
location_id: data.locationID,
|
||||
allocation_code_id: data.allocationCodeID,
|
||||
imp_project_schedule_id: data.impProjectScheduleID,
|
||||
imp_project_schedule_task_id: data.impProjectScheduleTaskID,
|
||||
api_vendor_id: data.apiVendorID,
|
||||
synced_at: new Date(),
|
||||
is_deleted: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Picklist entity (statuses, issue types, etc.)
|
||||
*/
|
||||
function mapPicklist(data: any): Record<string, any> {
|
||||
return {
|
||||
value: data.value,
|
||||
label: data.label || data.name,
|
||||
is_active: data.isActive !== undefined ? data.isActive : true,
|
||||
is_system: data.isSystem || false,
|
||||
sort_order: data.sortOrder,
|
||||
parent_value: data.parentValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch map multiple entities
|
||||
*/
|
||||
export function mapAutotaskBatch(
|
||||
entity: EntityType,
|
||||
items: any[]
|
||||
): Record<string, any>[] {
|
||||
return items.map(item => mapAutotaskToDatabase(entity, item));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue