From 401e626a6429fc45d84dcede756b69d8a6c55e75 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 28 Feb 2022 03:48:17 -0500 Subject: [PATCH] :sparkles: Add Onfleet Node & Trigger (#2845) * feat: added Onfleet nodes Added Onfleet nodes for working with different endpoints like: organizations, administrators, workers, hubs, teams, destinations, recipients, containers and webhooks. * style: fixed typos, arrays uniformity, unnecesary files * refactor: changed add to create in comments and labels * feat: added name field to onfleet trigger node * feat: added team endpoints to onfleet node Added team auto-dispatch and driver time estimate endpoints to Onfleet node * style: remove dots in descriptions and fixed some typos * feat: added fixes according to comments made on the n8n PR added new fixed collections, refactored the code according to comments made on the n8n pr * fix: fixed recipient and destination cretion * docs: added docstrings for format some functions added docstrings for new functions addded for formatting the destination and recipient objects * style: formatting the code according to n8n nodelinter * fix: typos and better descriptions * [INT-510] n8n: Address additional problems from n8n code review (#5) * Fixed some error creating a worker, moving some fields under additional fields collection * Fixed returned values for delete operations, making some changes for style code * Added operational error since required property is not working for dateTime fields * :zap: Improvements to #2593 * :zap: Improvements * :bug: Fix issue with wrong interface * :zap: Improvements * :zap: Improvements * :zap: Minor improvement Co-authored-by: Santiago Botero Ruiz Co-authored-by: ilsemaj Co-authored-by: Santiago Botero Ruiz <39206812+YokySantiago@users.noreply.github.com> Co-authored-by: Jan Oberhauser --- .../credentials/OnfleetApi.credentials.ts | 18 + .../nodes/Onfleet/GenericFunctions.ts | 123 ++ .../nodes-base/nodes/Onfleet/Onfleet.node.ts | 230 +++ packages/nodes-base/nodes/Onfleet/Onfleet.svg | 14 + packages/nodes-base/nodes/Onfleet/Onfleet.ts | 1343 +++++++++++++++++ .../nodes/Onfleet/OnfleetTrigger.node.ts | 164 ++ .../nodes/Onfleet/WebhookMapping.ts | 91 ++ .../descriptions/AdministratorDescription.ts | 209 +++ .../descriptions/ContainerDescription.ts | 219 +++ .../descriptions/DestinationDescription.ts | 402 +++++ .../Onfleet/descriptions/HubDescription.ts | 199 +++ .../descriptions/OnfleetWebhookDescription.ts | 43 + .../descriptions/OrganizationDescription.ts | 53 + .../descriptions/RecipientDescription.ts | 329 ++++ .../Onfleet/descriptions/TaskDescription.ts | 453 ++++++ .../Onfleet/descriptions/TeamDescription.ts | 600 ++++++++ .../descriptions/WebhookDescription.ts | 161 ++ .../Onfleet/descriptions/WorkerDescription.ts | 754 +++++++++ .../nodes-base/nodes/Onfleet/interfaces.ts | 184 +++ packages/nodes-base/package.json | 3 + packages/workflow/src/NodeErrors.ts | 1 + 21 files changed, 5593 insertions(+) create mode 100644 packages/nodes-base/credentials/OnfleetApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Onfleet/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Onfleet/Onfleet.node.ts create mode 100644 packages/nodes-base/nodes/Onfleet/Onfleet.svg create mode 100644 packages/nodes-base/nodes/Onfleet/Onfleet.ts create mode 100644 packages/nodes-base/nodes/Onfleet/OnfleetTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Onfleet/WebhookMapping.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/AdministratorDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/ContainerDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/DestinationDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/HubDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/OnfleetWebhookDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/OrganizationDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/RecipientDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/TaskDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/TeamDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/WebhookDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/descriptions/WorkerDescription.ts create mode 100644 packages/nodes-base/nodes/Onfleet/interfaces.ts diff --git a/packages/nodes-base/credentials/OnfleetApi.credentials.ts b/packages/nodes-base/credentials/OnfleetApi.credentials.ts new file mode 100644 index 0000000000..69e5ae3b72 --- /dev/null +++ b/packages/nodes-base/credentials/OnfleetApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class OnfleetApi implements ICredentialType { + name = 'onfleetApi'; + displayName = 'Onfleet API'; + documentationUrl = 'onfleet'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Onfleet/GenericFunctions.ts b/packages/nodes-base/nodes/Onfleet/GenericFunctions.ts new file mode 100644 index 0000000000..0c3020388d --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/GenericFunctions.ts @@ -0,0 +1,123 @@ +import { + ICredentialDataDecryptedObject, + IDataObject, + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INodePropertyOptions, + IWebhookFunctions, + JsonObject, + NodeApiError +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +import * as moment from 'moment-timezone'; + +export async function onfleetApiRequest( + this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: any = {}, // tslint:disable-line:no-any + qs?: any, // tslint:disable-line:no-any + uri?: string): Promise { // tslint:disable-line:no-any + + const credentials = await this.getCredentials('onfleetApi') as ICredentialDataDecryptedObject; + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'n8n-onfleet', + }, + auth: { + user: credentials.apiKey as string, + pass: '', + }, + method, + body, + qs, + uri: uri || `https://onfleet.com/api/v2/${resource}`, + json: true, + }; + try { + //@ts-ignore + return await this.helpers.request(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} + +export async function onfleetApiRequestAllItems( + this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + // tslint:disable-next-line: no-any + body: any = {}, + query: IDataObject = {}, +): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + do { + responseData = await onfleetApiRequest.call(this, method, endpoint, body, query); + query.lastId = responseData['lastId']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['lastId'] !== undefined + ); + + return returnData; +} +export const resourceLoaders = { + async getTeams(this: ILoadOptionsFunctions): Promise { + try { + const teams = await onfleetApiRequest.call(this, 'GET', 'teams') as IDataObject[]; + return teams.map(({ name = '', id: value = '' }) => ({ name, value })) as INodePropertyOptions[]; + } catch (error) { + return []; + } + }, + + async getWorkers(this: ILoadOptionsFunctions): Promise { + try { + const workers = await onfleetApiRequest.call(this, 'GET', 'workers') as IDataObject[]; + return workers.map(({ name = '', id: value = '' }) => ({ name, value })) as INodePropertyOptions[]; + } catch (error) { + return []; + } + }, + + async getAdmins(this: ILoadOptionsFunctions): Promise { + try { + const admins = await onfleetApiRequest.call(this, 'GET', 'admins') as IDataObject[]; + return admins.map(({ name = '', id: value = '' }) => ({ name, value })) as INodePropertyOptions[]; + } catch (error) { + return []; + } + }, + + async getHubs(this: ILoadOptionsFunctions): Promise { + try { + const hubs = await onfleetApiRequest.call(this, 'GET', 'hubs') as IDataObject[]; + return hubs.map(({ name = '', id: value = '' }) => ({ name, value })) as INodePropertyOptions[]; + } catch (error) { + return []; + } + }, + + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData = [] as INodePropertyOptions[]; + for (const timezone of moment.tz.names()) { + returnData.push({ + name: timezone, + value: timezone, + }); + } + return returnData; + }, +}; diff --git a/packages/nodes-base/nodes/Onfleet/Onfleet.node.ts b/packages/nodes-base/nodes/Onfleet/Onfleet.node.ts new file mode 100644 index 0000000000..9413630005 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/Onfleet.node.ts @@ -0,0 +1,230 @@ +import { + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + INodeCredentialTestResult, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + taskFields, + taskOperations, +} from './descriptions/TaskDescription'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + destinationFields, + destinationOperations, +} from './descriptions/DestinationDescription'; + +import { + resourceLoaders, +} from './GenericFunctions'; + +import { + recipientFields, + recipientOperations, +} from './descriptions/RecipientDescription'; + +import { + organizationFields, + organizationOperations, +} from './descriptions/OrganizationDescription'; + +import { + adminFields, + adminOperations, +} from './descriptions/AdministratorDescription'; + +import { + hubFields, + hubOperations, +} from './descriptions/HubDescription'; + +import { + workerFields, + workerOperations, +} from './descriptions/WorkerDescription'; + +// import { +// webhookFields, +// webhookOperations, +// } from './descriptions/WebhookDescription'; + +import { + containerFields, + containerOperations, +} from './descriptions/ContainerDescription'; + +import { + teamFields, + teamOperations, +} from './descriptions/TeamDescription'; + +import { + OptionsWithUri, +} from 'request'; + +import { Onfleet as OnfleetMethods } from './Onfleet'; +export class Onfleet implements INodeType { + description: INodeTypeDescription = { + displayName: 'Onfleet', + name: 'onfleet', + icon: 'file:Onfleet.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Onfleet API', + defaults: { + color: '#AA81F3', + name: 'Onfleet', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'onfleetApi', + required: true, + testedBy: 'onfleetApiTest', + }, + ], + properties: [ + // List of option resources + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Admin', + value: 'admin', + }, + { + name: 'Container', + value: 'container', + }, + { + name: 'Destination', + value: 'destination', + }, + { + name: 'Hub', + value: 'hub', + }, + { + name: 'Organization', + value: 'organization', + }, + { + name: 'Recipient', + value: 'recipient', + }, + { + name: 'Task', + value: 'task', + }, + { + name: 'Team', + value: 'team', + }, + // { + // name: 'Webhook', + // value: 'webhook', + // }, + { + name: 'Worker', + value: 'worker', + }, + ], + default: 'task', + description: 'The resource to perform operations on', + }, + // Operations & fields + ...adminOperations, + ...adminFields, + ...containerOperations, + ...containerFields, + ...destinationOperations, + ...destinationFields, + ...hubOperations, + ...hubFields, + ...organizationOperations, + ...organizationFields, + ...recipientOperations, + ...recipientFields, + ...taskOperations, + ...taskFields, + ...teamOperations, + ...teamFields, + // ...webhookOperations, + // ...webhookFields, + ...workerOperations, + ...workerFields, + ], + }; + + methods = { + credentialTest: { + async onfleetApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { + const credentials = credential.data as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'n8n-onfleet', + }, + auth: { + user: credentials.apiKey as string, + pass: '', + }, + method: 'GET', + uri: 'https://onfleet.com/api/v2/auth/test', + json: true, + }; + + try { + await this.helpers.request(options); + return { + status: 'OK', + message: 'Authentication successful', + }; + } catch (error) { + return { + status: 'Error', + message: `Auth settings are not valid: ${error}`, + }; + } + }, + }, + loadOptions: resourceLoaders, + }; + + async execute(this: IExecuteFunctions): Promise { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const items = this.getInputData(); + + const operations: { [key: string]: Function } = { + task: OnfleetMethods.executeTaskOperations, + destination: OnfleetMethods.executeDestinationOperations, + organization: OnfleetMethods.executeOrganizationOperations, + admin: OnfleetMethods.executeAdministratorOperations, + recipient: OnfleetMethods.executeRecipientOperations, + hub: OnfleetMethods.executeHubOperations, + worker: OnfleetMethods.executeWorkerOperations, + webhook: OnfleetMethods.executeWebhookOperations, + container: OnfleetMethods.executeContainerOperations, + team: OnfleetMethods.executeTeamOperations, + }; + + const responseData = await operations[resource].call(this, `${resource}s`, operation, items); + + // Map data to n8n data + return [this.helpers.returnJsonArray(responseData)]; + } +} diff --git a/packages/nodes-base/nodes/Onfleet/Onfleet.svg b/packages/nodes-base/nodes/Onfleet/Onfleet.svg new file mode 100644 index 0000000000..a8e2b261c1 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/Onfleet.svg @@ -0,0 +1,14 @@ + + + Group 2 + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Onfleet/Onfleet.ts b/packages/nodes-base/nodes/Onfleet/Onfleet.ts new file mode 100644 index 0000000000..70d4818fe6 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/Onfleet.ts @@ -0,0 +1,1343 @@ +import { + IDataObject, + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INodeExecutionData, + IWebhookFunctions, + NodeOperationError, +} from 'n8n-workflow'; + +import { + OnfleetAdmins, + OnfleetCloneOverrideTaskOptions, + OnfleetCloneTask, + OnfleetCloneTaskOptions, + OnfleetDestination, + OnfleetHubs, + OnfleetListTaskFilters, + OnfleetRecipient, + OnfleetTask, + OnfleetTaskComplete, + OnfleetTaskUpdate, + OnfleetTeamAutoDispatch, + OnfleetTeams, + OnfleetWebhook, + OnfleetWorker, + OnfleetWorkerEstimates, + OnfleetWorkerFilter, + OnfleetWorkerSchedule, + OnfleetWorkerScheduleEntry +} from './interfaces'; + +import { + onfleetApiRequest, + onfleetApiRequestAllItems, +} from './GenericFunctions'; + +import * as moment from 'moment-timezone'; + +const formatAddress = ( + unparsed: boolean, + address: string | undefined, + addressNumber: string | undefined, + addressStreet: string | undefined, + addressCity: string | undefined, + addressCountry: string | undefined, + additionalFields: IDataObject, +): OnfleetDestination => { + let destination: OnfleetDestination; + if (unparsed) { + destination = { address: { unparsed: address } }; + } else { + destination = { + address: { + number: addressNumber, + street: addressStreet, + city: addressCity, + country: addressCountry, + }, + }; + } + + // Adding destination extra fields + if (additionalFields.addressName) { + destination.address.name = additionalFields.addressName as string; + } + if (additionalFields.addressApartment) { + destination.address.apartment = additionalFields.addressApartment as string; + } + if (additionalFields.addressState) { + destination.address.state = additionalFields.addressState as string; + } + if (additionalFields.addressPostalCode) { + destination.address.postalCode = additionalFields.addressPostalCode as string; + } + if (additionalFields.addressNotes) { + destination.notes = additionalFields.addressNotes as string; + } + return destination; +}; + +export class Onfleet { + + /** + * Returns a valid formatted destination object + * @param unparsed Whether the address is parsed or not + * @param address Destination address + * @param addressNumber Destination number + * @param addressStreet Destination street + * @param addressCity Destination city + * @param addressCountry Destination country + * @param additionalFields Destination additional fields + * @returns + */ + + /** + * Gets the properties of a destination according to the operation chose + * @param item Current execution data + * @param operation Current destination operation + * @param shared Whether the collection is in other resource or not + * @returns {OnfleetDestination} Destination information + */ + static getDestinationFields( + this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + item: number, operation: string, shared: { parent: string } | boolean = false, + ): OnfleetDestination | OnfleetDestination[] | null { + if (['create', 'update'].includes(operation)) { + /* -------------------------------------------------------------------------- */ + /* Get fields for create and update a destination */ + /* -------------------------------------------------------------------------- */ + if (shared !== false) { + let destination; + if (typeof shared === 'boolean' && shared) { + const { destinationProperties = {} } = this.getNodeParameter('destination', item) as IDataObject; + destination = destinationProperties; + } else if (typeof shared !== 'boolean') { + const { destination: destinationCollection = {} } = this.getNodeParameter(shared.parent, item) as IDataObject; + destination = (destinationCollection as IDataObject).destinationProperties; + } + + if (!destination || Object.keys((destination) as IDataObject).length === 0) { + return []; + } + + const { + unparsed, address, addressNumber, addressStreet, addressCity, addressCountry, ...additionalFields + } = destination as IDataObject; + return formatAddress( + unparsed as boolean, + address as string, + addressNumber as string, + addressStreet as string, + addressCity as string, + addressCountry as string, + additionalFields as IDataObject, + ) as OnfleetDestination; + } else { + let unparsed, address, addressNumber, addressStreet, addressCity, addressCountry, additionalFields; + unparsed = this.getNodeParameter('unparsed', item) as boolean; + if (unparsed) { + address = this.getNodeParameter('address', item) as string; + } else { + addressNumber = this.getNodeParameter('addressNumber', item) as string; + addressStreet = this.getNodeParameter('addressStreet', item) as string; + addressCity = this.getNodeParameter('addressCity', item) as string; + addressCountry = this.getNodeParameter('addressCountry', item) as string; + } + additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + + return formatAddress( + unparsed, address, addressNumber, addressStreet, addressCity, addressCountry, additionalFields, + ) as OnfleetDestination; + } + + } + return null; + } + + /** + * Gets the properties of an administrator according to the operation chose + * @param item Current execution data + * @param operation Current administrator operation + * @returns {OnfleetAdmins} Administrator information + */ + static getAdminFields(this: IExecuteFunctions, item: number, operation: string): OnfleetAdmins | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields for create admin */ + /* -------------------------------------------------------------------------- */ + const name = this.getNodeParameter('name', item) as string; + const email = this.getNodeParameter('email', item) as string; + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + + const adminData: OnfleetAdmins = { name, email }; + // Adding additional fields + Object.assign(adminData, additionalFields); + + return adminData; + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Get fields for update admin */ + /* -------------------------------------------------------------------------- */ + const updateFields = this.getNodeParameter('updateFields', item) as IDataObject; + const adminData: OnfleetAdmins = {}; + if (!Object.keys(updateFields).length) { + throw new NodeOperationError(this.getNode(), 'Select at least one field to be updated'); + } + // Adding additional fields + Object.assign(adminData, updateFields); + return adminData; + } + return null; + } + + /** + * Gets the properties of a hub according to the operation chose + * @param item Current execution data + * @param operation Current hub operation + * @returns {OnfleetHubs|null} Hub information + */ + static getHubFields(this: IExecuteFunctions, item: number, operation: string): OnfleetHubs | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields for create hub */ + /* -------------------------------------------------------------------------- */ + const destination = Onfleet.getDestinationFields.call(this, item, operation, true) as OnfleetDestination; + const name = this.getNodeParameter('name', item) as string; + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + + const hubData: OnfleetHubs = { name, ...destination }; + + // Adding additional fields + Object.assign(hubData, additionalFields); + + + return hubData; + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Get fields for update hub */ + /* -------------------------------------------------------------------------- */ + const destination = Onfleet.getDestinationFields.call(this, item, operation, { parent: 'updateFields' }) as OnfleetDestination; + const hubData: OnfleetHubs = { ...destination }; + + // Adding additional fields + const updateFields = this.getNodeParameter('updateFields', item) as IDataObject; + + if (!Object.keys(updateFields).length) { + throw new NodeOperationError(this.getNode(), 'Select at least one field to be updated'); + } + + Object.assign(hubData, updateFields); + return hubData; + } + return null; + } + + /** + * Gets the properties of a worker according to the operation chose + * @param item Current execution data + * @param operation Current worker operation + * @returns {OnfleetWorker|OnfleetWorkerFilter|OnfleetWorkerSchedule|null} Worker information + */ + static getWorkerFields(this: IExecuteFunctions, item: number, operation: string): OnfleetWorker | OnfleetWorkerFilter | OnfleetWorkerSchedule | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields for create worker */ + /* -------------------------------------------------------------------------- */ + const name = this.getNodeParameter('name', item) as string; + const phone = this.getNodeParameter('phone', item) as string; + const teams = this.getNodeParameter('teams', item) as string[]; + const workerData: OnfleetWorker = { name, phone, teams }; + + // Adding additional fields + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + + if (additionalFields.vehicle) { + const { vehicleProperties } = additionalFields.vehicle as IDataObject; + Object.assign(workerData, { vehicle: vehicleProperties }); + delete additionalFields.vehicle; + } + + Object.assign(workerData, additionalFields); + + return workerData; + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Get fields for update worker */ + /* -------------------------------------------------------------------------- */ + + const workerData: OnfleetWorker = {}; + + // Adding additional fields + const updateFields = this.getNodeParameter('updateFields', item) as IDataObject; + + if (!Object.keys(updateFields).length) { + throw new NodeOperationError(this.getNode(), 'Select at least one field to be updated'); + } + + Object.assign(workerData, updateFields); + return workerData; + } else if (operation === 'get') { + const options = this.getNodeParameter('options', item, {}) as IDataObject; + const workerFilter: OnfleetWorkerFilter = {}; + if (options.filter) { + options.filter = (options.filter as string[]).join(','); + } + if (typeof options.analytics === 'boolean') { + options.analytics = options.analytics ? 'true' : 'false'; + } + Object.assign(workerFilter, options); + return workerFilter; + + } else if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get fields for get and getAll workers */ + /* -------------------------------------------------------------------------- */ + + const options = this.getNodeParameter('options', item, {}) as IDataObject; + const filters = this.getNodeParameter('filters', item, {}) as IDataObject; + const workerFilter: OnfleetWorkerFilter = {}; + + if (filters.states) { + filters.states = (filters.states as number[]).join(','); + } + if (filters.teams) { + filters.teams = (filters.teams as string[]).join(','); + } + if (filters.phones) { + filters.phones = (filters.phones as string[]).join(','); + } + if (options.filter) { + options.filter = (options.filter as string[]).join(','); + } + + Object.assign(workerFilter, options); + Object.assign(workerFilter, filters); + return workerFilter; + } else if (operation === 'setSchedule') { + /* -------------------------------------------------------------------------- */ + /* Set a worker schedule */ + /* -------------------------------------------------------------------------- */ + const { scheduleProperties } = this.getNodeParameter('schedule', item) as IDataObject; + const entries = (scheduleProperties as IDataObject[] || []).map(entry => { + const { timezone, date, shifts } = entry as IDataObject; + const { shiftsProperties } = shifts as IDataObject; + return { + timezone: timezone as string, + date: moment(date as Date).format('YYYY-MM-DD'), + shifts: (shiftsProperties as IDataObject[]).map(({ start, end }) => [ + new Date(start as Date).getTime(), + new Date(end as Date).getTime(), + ]), + } as OnfleetWorkerScheduleEntry; + }) as OnfleetWorkerScheduleEntry[]; + return { entries } as OnfleetWorkerSchedule; + } + return null; + } + + /** + * Gets the properties of a webhooks according to the operation chose + * @param item Current execution data + * @param operation Current webhooks operation + * @returns {OnfleetWebhook} Webhooks information + */ + static getWebhookFields(this: IExecuteFunctions, item: number, operation: string): OnfleetWebhook | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields for create webhook */ + /* -------------------------------------------------------------------------- */ + const url = this.getNodeParameter('url', item) as string; + const name = this.getNodeParameter('name', item) as string; + const trigger = this.getNodeParameter('trigger', item) as number; + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + + const webhookData: OnfleetWebhook = { url, name, trigger }; + // Adding additional fields + Object.assign(webhookData, additionalFields); + + return webhookData; + } + return null; + } + + /** + * Returns a valid formatted recipient object + * @param name Recipient name + * @param phone Recipient phone + * @param additionalFields Recipient additional fields + * @returns + */ + static formatRecipient( + name: string, phone: string, additionalFields: IDataObject, + options: IDataObject = {}, + ): OnfleetRecipient { + const recipient: OnfleetRecipient = { name, phone }; + + // Adding recipient extra fields + if (additionalFields.recipientNotes) { + recipient.notes = additionalFields.recipientNotes as string; + } + if (additionalFields.recipientSkipSMSNotifications) { + recipient.skipSMSNotifications = additionalFields.recipientSkipSMSNotifications as boolean; + } + if ('recipientSkipPhoneNumberValidation' in options) { + recipient.skipPhoneNumberValidation = options.recipientSkipPhoneNumberValidation as boolean || false; + } + + return recipient; + } + + /** + * Gets the properties of a recipient according to the operation chose + * @param item Current execution data + * @param operation Current recipient operation + * @param shared Whether the collection is in other resource or not + * @returns {OnfleetRecipient} Recipient information + */ + static getRecipientFields( + this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + item: number, operation: string, shared = false, + ): OnfleetRecipient | OnfleetRecipient[] | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields to create recipient */ + /* -------------------------------------------------------------------------- */ + if (shared) { + const { recipient: recipientData = {} } = this.getNodeParameter('additionalFields', item, {}) as IDataObject; + const options = this.getNodeParameter('options', item, {}) as IDataObject; + const { recipientProperties: recipient = {} } = recipientData as IDataObject; + if (!recipient || Object.keys(recipient).length === 0) { + return null; + } + const { recipientName: name, recipientPhone: phone, ...additionalFields } = recipient as IDataObject; + return Onfleet.formatRecipient( + name as string, + phone as string, + additionalFields as IDataObject, + options, + ); + } else { + const name = this.getNodeParameter('recipientName', item) as string; + const phone = this.getNodeParameter('recipientPhone', item) as string; + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + const options = this.getNodeParameter('options', item) as IDataObject; + return Onfleet.formatRecipient(name, phone, additionalFields, options) as OnfleetRecipient; + } + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Get fields to update recipient */ + /* -------------------------------------------------------------------------- */ + const { + recipientName: name = '', recipientPhone: phone = '', ...additionalFields + } = this.getNodeParameter('updateFields', item) as IDataObject; + + const recipientData: OnfleetRecipient = {}; + + // Adding additional fields + if (name) { + recipientData.name = name as string; + } + if (phone) { + recipientData.phone = phone as string; + } + Object.assign(recipientData, additionalFields); + return recipientData; + } + return null; + } + + /** + * Gets the properties of a task according to the operation chose + * @param item Current execution data + * @param operation Current task operation + * @returns {OnfleetListTaskFilters | OnfleetTask } Task information + */ + static getTaskFields(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + item: number, operation: string): + OnfleetListTaskFilters | OnfleetTask | OnfleetCloneTask | OnfleetTaskComplete | OnfleetTaskUpdate | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields to create a task */ + /* -------------------------------------------------------------------------- */ + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + const destination = Onfleet.getDestinationFields.call(this, item, operation, true) as OnfleetDestination; + + // Adding recipients information + const recipient = Onfleet.getRecipientFields.call(this, item, operation, true) as OnfleetRecipient; + + const taskData: OnfleetTask = { destination, recipients: [recipient] }; + const { completeAfter = null, completeBefore = null, ...extraFields } = additionalFields; + if (completeAfter) taskData.completeAfter = new Date(completeAfter as Date).getTime(); + if (completeBefore) taskData.completeBefore = new Date(completeBefore as Date).getTime(); + + Object.assign(taskData, extraFields); + return taskData; + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Get fields to update task */ + /* -------------------------------------------------------------------------- */ + const updateFields = this.getNodeParameter('updateFields', item) as IDataObject; + const taskData: OnfleetTaskUpdate = {}; + + if (!Object.keys(updateFields).length) { + throw new NodeOperationError(this.getNode(), 'Select at least one field to be updated'); + } + + const { completeAfter = null, completeBefore = null, ...extraFields } = updateFields; + if (completeAfter) taskData.completeAfter = new Date(completeAfter as Date).getTime(); + if (completeBefore) taskData.completeBefore = new Date(completeBefore as Date).getTime(); + Object.assign(taskData, extraFields); + return taskData; + } else if (operation === 'clone') { + /* -------------------------------------------------------------------------- */ + /* Get fields to clone task */ + /* -------------------------------------------------------------------------- */ + const overrideFields = this.getNodeParameter('overrideFields', item) as IDataObject; + + const options: OnfleetCloneTaskOptions = {}; + if (overrideFields.includeMetadata) options.includeMetadata = overrideFields.includeMetadata as boolean; + if (overrideFields.includeBarcodes) options.includeBarcodes = overrideFields.includeBarcodes as boolean; + if (overrideFields.includeDependencies) options.includeDependencies = overrideFields.includeDependencies as boolean; + + // Adding overrides data + const { + notes, pickupTask, serviceTime, completeAfter, completeBefore, + } = overrideFields as IDataObject; + const overridesData = {} as OnfleetCloneOverrideTaskOptions; + + if (notes) overridesData.notes = notes as string; + if (typeof pickupTask !== 'undefined') overridesData.pickupTask = pickupTask as boolean; + if (serviceTime) overridesData.serviceTime = serviceTime as number; + if (completeAfter) overridesData.completeAfter = new Date(completeAfter as Date).getTime(); + if (completeBefore) overridesData.completeBefore = new Date(completeBefore as Date).getTime(); + + if (overridesData && Object.keys(overridesData).length > 0) { + options.overrides = overridesData; + } + + return { options } as OnfleetCloneTask; + } else if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get fields to list tasks */ + /* -------------------------------------------------------------------------- */ + const filters = this.getNodeParameter('filters', item) as IDataObject; + const listTaskData: OnfleetListTaskFilters = {}; + + const allStates = '0,1,2,3'; + + const twoWeeksInMilisecods = () => (604800 * 1000); + + // Adding extra fields to search tasks + if (filters.from) { + listTaskData.from = new Date(filters.from as Date).getTime(); + } else { + listTaskData.from = new Date().getTime() - twoWeeksInMilisecods(); + } + if (filters.to) { + listTaskData.to = new Date(filters.to as Date).getTime(); + } + if (filters.state) { + listTaskData.state = (filters.state as number[]).join(','); + if (listTaskData.state.includes('all')) { + listTaskData.state = allStates; + } + } + + return listTaskData; + } else if (operation === 'complete') { + /* -------------------------------------------------------------------------- */ + /* Get fields to complete a task */ + /* -------------------------------------------------------------------------- */ + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + const success = this.getNodeParameter('success', item) as boolean; + const taskData: OnfleetTaskComplete = { completionDetails: { success } }; + if (additionalFields.notes) taskData.completionDetails.notes = additionalFields.notes as string; + return taskData; + } + return null; + } + + /** + * Gets the properties of a team according to the operation chose + * @param item Current execution data + * @param operation Current team operation + * @returns {OnfleetTeams} Team information + */ + static getTeamFields(this: IExecuteFunctions, item: number, operation: string): OnfleetTeams | OnfleetWorkerEstimates | OnfleetTeamAutoDispatch | null { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Get fields to create a team */ + /* -------------------------------------------------------------------------- */ + const name = this.getNodeParameter('name', item) as string; + const workers = this.getNodeParameter('workers', item) as string[]; + const managers = this.getNodeParameter('managers', item) as string[]; + const additionalFields = this.getNodeParameter('additionalFields', item) as IDataObject; + + const teamData: OnfleetTeams = { name, workers, managers }; + // Adding additional fields + Object.assign(teamData, additionalFields); + + return teamData; + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Get fields to update a team */ + /* -------------------------------------------------------------------------- */ + const teamData: OnfleetTeams = {}; + // Adding additional fields + const updateFields = this.getNodeParameter('updateFields', item) as IDataObject; + + if (!Object.keys(updateFields).length) { + throw new NodeOperationError(this.getNode(), 'Select at least one field to be updated'); + } + + Object.assign(teamData, updateFields); + return teamData; + } else if (operation === 'getTimeEstimates') { + /* -------------------------------------------------------------------------- */ + /* Get driver time estimates for tasks that haven't been created yet */ + /* -------------------------------------------------------------------------- */ + const { dropOff = {}, pickUp = {}, ...additionalFields } = this.getNodeParameter('filters', item) as IDataObject; + const { dropOffProperties = {} } = dropOff as IDataObject; + const { pickUpProperties = {} } = pickUp as IDataObject; + const hasPickUp = pickUp && Object.keys(pickUpProperties as IDataObject).length > 0; + const hasDropOff = dropOffProperties && Object.keys(dropOffProperties as IDataObject).length > 0; + + if (!hasPickUp && !hasDropOff) { + throw new NodeOperationError( + this.getNode(), 'At least 1 of Drop-Off location or Pick-Up location must be selected', + ); + } + + const workerTimeEstimates = {} as OnfleetWorkerEstimates; + if (hasPickUp) { + const { + pickupLongitude: longitude, pickupLatitude: latitude, pickupTime, + } = pickUpProperties as IDataObject; + workerTimeEstimates.pickupLocation = `${longitude},${latitude}`; + if (pickupTime) { + workerTimeEstimates.pickupTime = moment(new Date(pickupTime as Date)).local().unix(); + } + } + if (hasDropOff) { + const { dropOffLongitude: longitude, dropOffLatitude: latitude } = dropOffProperties as IDataObject; + workerTimeEstimates.dropoffLocation = `${longitude},${latitude}`; + } + + Object.assign(workerTimeEstimates, additionalFields); + return workerTimeEstimates; + } else if (operation === 'autoDispatch') { + /* -------------------------------------------------------------------------- */ + /* Dynamically dispatching tasks on the fly */ + /* -------------------------------------------------------------------------- */ + const teamAutoDispatch = {} as OnfleetTeamAutoDispatch; + const { + scheduleTimeWindow = {}, taskTimeWindow = {}, endingRoute = {}, ...additionalFields + } = this.getNodeParameter('additionalFields', item) as IDataObject; + const { endingRouteProperties = {} } = endingRoute as IDataObject; + const { scheduleTimeWindowProperties = {} } = scheduleTimeWindow as IDataObject; + const { taskTimeWindowProperties = {} } = taskTimeWindow as IDataObject; + + if (scheduleTimeWindowProperties && Object.keys((scheduleTimeWindowProperties as IDataObject)).length > 0) { + const { startTime, endTime } = scheduleTimeWindowProperties as IDataObject; + teamAutoDispatch.scheduleTimeWindow = [ + moment(new Date(startTime as Date)).local().unix(), moment(new Date(endTime as Date)).local().unix(), + ]; + } + + if (endingRouteProperties && Object.keys((endingRouteProperties as IDataObject)).length > 0) { + const { routeEnd, hub } = endingRouteProperties as IDataObject; + teamAutoDispatch.routeEnd = ({ + 'anywhere': null, + 'hub': `hub://${hub}`, + 'team_hub': 'teams://DEFAULT', + 'worker_routing_address': 'workers://ROUTING_ADDRESS', + })[routeEnd as string] as string; + } + + if (taskTimeWindowProperties && Object.keys((taskTimeWindowProperties as IDataObject)).length > 0) { + const { startTime, endTime } = taskTimeWindowProperties as IDataObject; + teamAutoDispatch.taskTimeWindow = [ + moment(new Date(startTime as Date)).local().unix(), moment(new Date(endTime as Date)).local().unix(), + ]; + } + + Object.assign(teamAutoDispatch, additionalFields); + return teamAutoDispatch; + } + return null; + } + + /** + * Execute the task operations + * @param resource Resource to be executed (Task) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Task information + */ + static async executeTaskOperations( + this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + if (operation === 'create' && Object.keys(items).length > 1) { + /* -------------------------------------------------------------------------- */ + /* Create multiple tasks by batch */ + /* -------------------------------------------------------------------------- */ + const path = `${resource}/batch`; + const tasksData = { tasks: items.map((_item, index) => Onfleet.getTaskFields.call(this, index, operation)) }; + //@ts-ignore + const { tasks: tasksCreated } = await onfleetApiRequest.call(this, 'POST', path, tasksData); + return tasksCreated; + } + + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new task */ + /* -------------------------------------------------------------------------- */ + const taskData = Onfleet.getTaskFields.call(this, index, operation); + if (!taskData) { continue; } + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, taskData)); + } else if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get a single task */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const shortId = String(id).length <= 8; + const path = `${resource}${(shortId ? '/shortId' : '')}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } else if (operation === 'clone') { + /* -------------------------------------------------------------------------- */ + /* Clone a task */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + // tslint:disable-next-line: no-any + const taskData = Onfleet.getTaskFields.call(this, index, operation) as any; + if (!taskData) { continue; } + const path = `${resource}/${id}/clone`; + responseData.push(await onfleetApiRequest.call(this, 'POST', path, taskData)); + } else if (operation === 'delete') { + /* -------------------------------------------------------------------------- */ + /* Delete a single task */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + await onfleetApiRequest.call(this, 'DELETE', path); + responseData.push({ success: true }); + } else if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get all tasks */ + /* -------------------------------------------------------------------------- */ + const taskData = Onfleet.getTaskFields.call(this, 0, operation) as IDataObject; + if (!taskData) return []; + const returnAll = this.getNodeParameter('returnAll', 0, false); + const path = `${resource}/all`; + let tasks; + if (returnAll === true) { + tasks = await onfleetApiRequestAllItems.call(this, 'tasks', 'GET', path, {}, taskData); + } else { + const limit = this.getNodeParameter('limit', 0); + tasks = await onfleetApiRequest.call(this, 'GET', path, {}, taskData); + tasks = tasks.tasks; + tasks = tasks.splice(0, limit); + } + responseData.push(...tasks); + } else if (operation === 'complete') { + /* -------------------------------------------------------------------------- */ + /* Force complete a task */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const taskData = Onfleet.getTaskFields.call(this, index, operation); + if (!taskData) { continue; } + const path = `${resource}/${id}/complete`; + await onfleetApiRequest.call(this, 'POST', path, taskData); + responseData.push({ success: true }); + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Update a task */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', 0) as string; + const path = `${resource}/${id}`; + const taskData = Onfleet.getTaskFields.call(this, index, operation); + if (!taskData) { continue; } + responseData.push(await onfleetApiRequest.call(this, 'PUT', path, taskData)); + } + } catch (error) { + //@ts-ignore + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + return responseData; + } + + /** + * Execute the destination operations + * @param resource Resource to be executed (Destination) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Destination information + */ + static async executeDestinationOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create destination */ + /* -------------------------------------------------------------------------- */ + const destinationData = Onfleet.getDestinationFields.call(this, index, operation); + if (!destinationData) { continue; } + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, destinationData)); + } else if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get single destination */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } + + /** + * Execute the organization operations + * @param resource Resource to be executed (Organization) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Organization information + */ + static async executeOrganizationOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get organization details */ + /* -------------------------------------------------------------------------- */ + const path = 'organization'; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } else if (operation === 'getDelegatee') { + /* -------------------------------------------------------------------------- */ + /* Get organization delegatee */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } + + /** + * Execute the recipient operations + * @param resource Resource to be executed (Recipient) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Recipient information + */ + static async executeRecipientOperations(this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new recipient */ + /* -------------------------------------------------------------------------- */ + const recipientData = Onfleet.getRecipientFields.call(this, index, operation); + if (!recipientData) { continue; } + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, recipientData)); + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Update a recipient */ + /* -------------------------------------------------------------------------- */ + const recipientData = Onfleet.getRecipientFields.call(this, index, operation); + if (!recipientData) { continue; } + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'PUT', path, recipientData)); + } else if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get recipient information */ + /* -------------------------------------------------------------------------- */ + const lookupBy = this.getNodeParameter('getBy', index) as string; + const lookupByValue = this.getNodeParameter(lookupBy, index) as string; + const path = `${resource}${lookupBy === 'id' ? '' : ('/' + lookupBy)}/${lookupByValue}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } + + /** + * Execute the administrator operations + * @param resource Resource to be executed (Administrator) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Administrator information + */ + static async executeAdministratorOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get administrators */ + /* -------------------------------------------------------------------------- */ + + const returnAll = this.getNodeParameter('returnAll', 0, false); + let adminUsers = await onfleetApiRequest.call(this, 'GET', resource); + if (!returnAll) { + const limit = this.getNodeParameter('limit', 0); + adminUsers = adminUsers.slice(0, limit); + } + responseData.push(...adminUsers); + } else if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new admin */ + /* -------------------------------------------------------------------------- */ + const adminData = Onfleet.getAdminFields.call(this, index, operation); + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, adminData)); + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Update admin */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const adminData = Onfleet.getAdminFields.call(this, index, operation); + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'PUT', path, adminData)); + } else if (operation === 'delete') { + /* -------------------------------------------------------------------------- */ + /* Delete admin */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + await onfleetApiRequest.call(this, 'DELETE', path); + responseData.push({ success: true }); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + return responseData; + } + + /** + * Execute the hub operations + * @param resource Resource to be executed (Hub) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Hub information + */ + static async executeHubOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get all hubs */ + /* -------------------------------------------------------------------------- */ + + const returnAll = this.getNodeParameter('returnAll', 0, false); + let hubs = await onfleetApiRequest.call(this, 'GET', resource); + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0); + hubs = hubs.slice(0, limit); + } + responseData.push(...hubs); + } else if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new hub */ + /* -------------------------------------------------------------------------- */ + const hubData = Onfleet.getHubFields.call(this, index, operation); + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, hubData)); + } + if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Update a hub */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const hubData = Onfleet.getHubFields.call(this, index, operation); + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'PUT', path, hubData)); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } + + /** + * Execute the worker operations + * @param resource Resource to be executed (Worker) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Workers information + */ + static async executeWorkerOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get all workers */ + /* -------------------------------------------------------------------------- */ + const byLocation = this.getNodeParameter('byLocation', index) as boolean; + const returnAll = this.getNodeParameter('returnAll', index, false) as boolean; + let workers; + + if (byLocation) { + const longitude = this.getNodeParameter('longitude', index) as string; + const latitude = this.getNodeParameter('latitude', index) as number; + const filters = this.getNodeParameter('filters', index) as IDataObject; + const path = `${resource}/location`; + workers = await onfleetApiRequest.call( + this, 'GET', path, {}, { longitude, latitude, ...filters }, + ); + workers = workers.workers; + } else { + const workerFilters = Onfleet.getWorkerFields.call(this, 0, operation) as OnfleetWorkerFilter; + workers = await onfleetApiRequest.call(this, 'GET', resource, {}, workerFilters); + } + + if (!returnAll) { + const limit = this.getNodeParameter('limit', index) as number; + workers = workers.slice(0, limit); + } + + responseData.push(...workers); + + } else if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get a worker */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const workerFilters = Onfleet.getWorkerFields.call(this, index, operation) as OnfleetWorkerFilter; + + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path, {}, workerFilters)); + } else if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new worker */ + /* -------------------------------------------------------------------------- */ + const workerData = Onfleet.getWorkerFields.call(this, index, operation); + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, workerData)); + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Update worker */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const workerData = Onfleet.getWorkerFields.call(this, index, operation); + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'PUT', path, workerData)); + } else if (operation === 'delete') { + /* -------------------------------------------------------------------------- */ + /* Delete worker */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + await onfleetApiRequest.call(this, 'DELETE', path); + responseData.push({ success: true }); + } else if (operation === 'getSchedule') { + /* -------------------------------------------------------------------------- */ + /* Get worker schedule */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}/schedule`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } else if (operation === 'setSchedule') { + /* -------------------------------------------------------------------------- */ + /* Set a worker schedule */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const workerSchedule = Onfleet.getWorkerFields.call(this, index, operation) as OnfleetWorkerSchedule; + const path = `${resource}/${id}/schedule`; + responseData.push(await onfleetApiRequest.call(this, 'POST', path, workerSchedule)); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + return responseData; + } + + /** + * Execute the webhook operations + * @param resource Resource to be executed (Webhook) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Webhook information + */ + static async executeWebhookOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get all webhooks */ + /* -------------------------------------------------------------------------- */ + responseData.push(...await onfleetApiRequest.call(this, 'GET', resource)); + } else if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new webhook */ + /* -------------------------------------------------------------------------- */ + const webhookData = Onfleet.getWebhookFields.call(this, index, operation); + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, webhookData)); + } else if (operation === 'delete') { + /* -------------------------------------------------------------------------- */ + /* Delete a webhook */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + await onfleetApiRequest.call(this, 'DELETE', path); + responseData.push({ success: true }); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } + + /** + * Execute the containers operations + * @param resource Resource to be executed (Container) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Container information + */ + static async executeContainerOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get container by id and type */ + /* -------------------------------------------------------------------------- */ + const containerId = this.getNodeParameter('containerId', index) as string; + const containerType = this.getNodeParameter('containerType', index) as string; + const path = `${resource}/${containerType}/${containerId}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } else if (['addTask', 'updateTask'].includes(operation)) { + /* -------------------------------------------------------------------------- */ + /* Add or update tasks to container */ + /* -------------------------------------------------------------------------- */ + const containerId = this.getNodeParameter('containerId', index) as string; + const containerType = this.getNodeParameter('containerType', index, 'workers') as string; + const options = this.getNodeParameter('options', index) as IDataObject; + + const tasks = this.getNodeParameter('tasks', index) as Array; + if (operation === 'addTask') { + const type = this.getNodeParameter('type', index) as number; + if (type === 1) { + const tasksIndex = this.getNodeParameter('index', index) as number; + tasks.unshift(tasksIndex); + } else { + tasks.unshift(type); + } + } + + const path = `${resource}/${containerType}/${containerId}`; + responseData.push( + await onfleetApiRequest.call(this, 'PUT', path, { tasks, ...options }), + ); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } + + /** + * Execute the team operations + * @param resource Resource to be executed (Team) + * @param operation Operation to be executed + * @param items Number of items to process by the node + * @returns Team information + */ + static async executeTeamOperations( + this: IExecuteFunctions, + resource: string, + operation: string, + items: INodeExecutionData[], + ): Promise { + const responseData = []; + for (const key of Object.keys(items)) { + const index = Number(key); + try { + if (operation === 'getAll') { + /* -------------------------------------------------------------------------- */ + /* Get all teams */ + /* -------------------------------------------------------------------------- */ + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + let teams = await onfleetApiRequest.call(this, 'GET', resource); + if (!returnAll) { + const limit = this.getNodeParameter('limit', 0) as number; + teams = teams.slice(0, limit); + } + + responseData.push(...teams); + } else if (operation === 'get') { + /* -------------------------------------------------------------------------- */ + /* Get a single team */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path)); + } else if (operation === 'create') { + /* -------------------------------------------------------------------------- */ + /* Create a new team */ + /* -------------------------------------------------------------------------- */ + const teamData = Onfleet.getTeamFields.call(this, index, operation); + responseData.push(await onfleetApiRequest.call(this, 'POST', resource, teamData)); + } else if (operation === 'update') { + /* -------------------------------------------------------------------------- */ + /* Update a team */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const teamData = Onfleet.getTeamFields.call(this, index, operation); + const path = `${resource}/${id}`; + responseData.push(await onfleetApiRequest.call(this, 'PUT', path, teamData)); + } else if (operation === 'delete') { + /* -------------------------------------------------------------------------- */ + /* Delete a team */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const path = `${resource}/${id}`; + await onfleetApiRequest.call(this, 'DELETE', path); + responseData.push({ success: true }); + } else if (operation === 'getTimeEstimates') { + /* -------------------------------------------------------------------------- */ + /* Get driver time estimates for tasks that haven't been created yet */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const workerTimeEstimates = Onfleet.getTeamFields.call(this, index, operation) as OnfleetWorkerSchedule; + const path = `${resource}/${id}/estimate`; + responseData.push(await onfleetApiRequest.call(this, 'GET', path, {}, workerTimeEstimates)); + } else if (operation === 'autoDispatch') { + /* -------------------------------------------------------------------------- */ + /* Dynamically dispatching tasks on the fly */ + /* -------------------------------------------------------------------------- */ + const id = this.getNodeParameter('id', index) as string; + const teamAutoDispatch = Onfleet.getTeamFields.call(this, index, operation) as OnfleetWorkerSchedule; + const path = `${resource}/${id}/dispatch`; + responseData.push(await onfleetApiRequest.call(this, 'POST', path, teamAutoDispatch)); + } + } catch (error) { + if (this.continueOnFail()) { + responseData.push({ error: (error as IDataObject).toString() }); + continue; + } + throw error; + } + } + + return responseData; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Onfleet/OnfleetTrigger.node.ts b/packages/nodes-base/nodes/Onfleet/OnfleetTrigger.node.ts new file mode 100644 index 0000000000..e5c143626c --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/OnfleetTrigger.node.ts @@ -0,0 +1,164 @@ +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, + NodeApiError, + NodeOperationError +} from 'n8n-workflow'; +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + eventDisplay, + eventNameField, +} from './descriptions/OnfleetWebhookDescription'; + +import { + onfleetApiRequest, +} from './GenericFunctions'; + +import { + webhookMapping, +} from './WebhookMapping'; + + +export class OnfleetTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Onfleet Trigger', + name: 'onfleetTrigger', + icon: 'file:Onfleet.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["triggerOn"]}}', + description: 'Starts the workflow when Onfleet events occur', + defaults: { + name: 'Onfleet Trigger', + color: '#AA81F3', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'onfleetApi', + required: true, + testedBy: 'onfleetApiTest', + }, + ], + webhooks: [ + { + name: 'setup', + httpMethod: 'GET', + responseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + eventDisplay, + eventNameField, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const webhookUrl = this.getNodeWebhookUrl('default') as string; + + // Webhook got created before so check if it still exists + const endpoint = '/webhooks'; + + const webhooks = await onfleetApiRequest.call(this, 'GET', endpoint); + + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && webhook.trigger === event) { + webhookData.webhookId = webhook.id; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const { name = '' } = this.getNodeParameter('additionalFields') as IDataObject; + const triggerOn = this.getNodeParameter('triggerOn') as string; + const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const webhookUrl = this.getNodeWebhookUrl('default') as string; + + if (webhookUrl.includes('//localhost')) { + throw new NodeOperationError(this.getNode(), 'The Webhook can not work on "localhost". Please, either setup n8n on a custom domain or start with "--tunnel"!'); + } + // Webhook name according to the field + let newWebhookName = `n8n-webhook:${webhookUrl}`; + + if (name) { + newWebhookName = `n8n-webhook:${name}`; + } + + const path = `/webhooks`; + const body = { + name: newWebhookName, + url: webhookUrl, + trigger: webhookMapping[triggerOn].key, + }; + + try { + const webhook = await onfleetApiRequest.call(this, 'POST', path, body); + + if (webhook.id === undefined) { + throw new NodeApiError(this.getNode(), webhook, { message: 'Onfleet webhook creation response did not contain the expected data' }); + } + + webhookData.id = webhook.id as string; + + } catch (error) { + const { httpCode = '' } = error as { httpCode: string }; + if (httpCode === '422') { + throw new NodeOperationError(this.getNode(), 'A webhook with the identical URL probably exists already. Please delete it manually in Onfleet!'); + } + throw error; + } + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node') as IDataObject; + // Get the data of the already registered webhook + const endpoint = `/webhooks/${webhookData.id}`; + await onfleetApiRequest.call(this, 'DELETE', endpoint); + return true; + }, + }, + }; + + /** + * Triggered function when an Onfleet webhook is executed + * @returns {Promise} Response data + */ + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + if (this.getWebhookName() === 'setup') { + /* -------------------------------------------------------------------------- */ + /* Validation request */ + /* -------------------------------------------------------------------------- */ + const res = this.getResponseObject(); + res.status(200).send(req.query.check); + return { noWebhookResponse: true }; + } + + const returnData: IDataObject = this.getBodyData(); + + return { + workflowData: [ + this.helpers.returnJsonArray(returnData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Onfleet/WebhookMapping.ts b/packages/nodes-base/nodes/Onfleet/WebhookMapping.ts new file mode 100644 index 0000000000..e785a4c092 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/WebhookMapping.ts @@ -0,0 +1,91 @@ +import { + OnfleetWebhooksMapping, +} from './interfaces'; + +export const webhookMapping: OnfleetWebhooksMapping = { + taskStarted: { + name: 'Task Started', + value: 'taskStarted', + key: 0, + }, + taskEta: { + name: 'Task ETA', + value: 'taskEta', + key: 1, + }, + taskArrival: { + name: 'Task Arrival', + value: 'taskArrival', + key: 2, + }, + taskCompleted: { + name: 'Task Completed', + value: 'taskCompleted', + key: 3, + }, + taskFailed: { + name: 'Task Failed', + value: 'taskFailed', + key: 4, + }, + workerDuty: { + name: 'Worker Duty', + value: 'workerDuty', + key: 5, + }, + taskCreated: { + name: 'Task Created', + value: 'taskCreated', + key: 6, + }, + taskUpdated: { + name: 'Task Updated', + value: 'taskUpdated', + key: 7, + }, + taskDeleted: { + name: 'Task Deleted', + value: 'taskDeleted', + key: 8, + }, + taskAssigned: { + name: 'Task Assigned', + value: 'taskAssigned', + key: 9, + }, + taskUnassigned: { + name: 'Task Unassigned', + value: 'taskUnassigned', + key: 10, + }, + taskDelayed: { + name: 'Task Delayed', + value: 'taskDelayed', + key: 12, + }, + taskCloned: { + name: 'Task Cloned', + value: 'taskCloned', + key: 13, + }, + smsRecipientResponseMissed: { + name: 'SMS Recipient Response Missed', + value: 'smsRecipientResponseMissed', + key: 14, + }, + workerCreated: { + name: 'Worker Created', + value: 'workerCreated', + key: 15, + }, + workerDeleted: { + name: 'Worker Deleted', + value: 'workerDeleted', + key: 16, + }, + SMSRecipientOptOut: { + name: 'SMS Recipient Opt Out', + value: 'SMSRecipientOptOut', + key: 17, + }, +}; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/AdministratorDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/AdministratorDescription.ts new file mode 100644 index 0000000000..cf87512d78 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/AdministratorDescription.ts @@ -0,0 +1,209 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const adminOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'admin', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet admin', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an Onfleet admin', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Onfleet admins', + }, + { + name: 'Update', + value: 'update', + description: 'Update an Onfleet admin', + }, + ], + default: 'getAll', + }, +]; + +const adminNameField = { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The administrator\'s name', +} as INodeProperties; + +const adminEmailField = { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The administrator\'s email address', +} as INodeProperties; + +const adminPhoneField = { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'The administrator\'s phone number', +} as INodeProperties; + +const adminReadOnlyField = { + displayName: 'Read Only', + name: 'isReadOnly', + type: 'boolean', + default: false, + description: 'Whether this administrator can perform write operations', +} as INodeProperties; + +export const adminFields: INodeProperties[] = [ + { + displayName: 'Admin ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'admin', + ], + }, + hide: { + operation: [ + 'create', + 'getAll', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the admin object for lookup', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'admin', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'admin', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 64, + }, + default: 64, + description: 'How many results to return', + }, + { + displayOptions: { + show: { + resource: [ + 'admin', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + ...adminNameField, + }, + { + displayOptions: { + show: { + resource: [ + 'admin', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + ...adminEmailField, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'admin', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + adminPhoneField, + adminReadOnlyField, + ], + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'admin', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + adminNameField, + adminPhoneField, + adminReadOnlyField, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/ContainerDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/ContainerDescription.ts new file mode 100644 index 0000000000..7f90f044ef --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/ContainerDescription.ts @@ -0,0 +1,219 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const containerOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'container', + ], + }, + }, + options: [ + { + name: 'Add Tasks', + value: 'addTask', + description: 'Add task at index (or append)', + }, + { + name: 'Get', + value: 'get', + description: 'Get container information', + }, + { + name: 'Update Tasks', + value: 'updateTask', + description: 'Fully replace a container\'s tasks', + }, + ], + default: 'get', + }, +]; + +const containerTypeField = { + displayName: 'Container Type', + name: 'containerType', + type: 'options', + options: [ + { + name: 'Organizations', + value: 'organizations', + }, + { + name: 'Teams', + value: 'teams', + }, + { + name: 'Workers', + value: 'workers', + }, + ], + default: '', + description: 'Container type', +} as INodeProperties; + +const containerIdField = { + displayName: 'Container ID', + name: 'containerId', + type: 'string', + default: '', + description: 'The object ID according to the container chosen', +} as INodeProperties; + +const insertTypeField = { + displayName: 'Insert Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Append', + value: -1, + }, + { + name: 'Prepend', + value: 0, + }, + { + name: 'At Specific Index', + value: 1, + }, + ], + default: '', +} as INodeProperties; + +const indexField = { + displayName: 'Index', + name: 'index', + type: 'number', + default: 0, + description: 'The index given indicates the position where the tasks are going to be inserted', +} as INodeProperties; + +const tasksField = { + displayName: 'Task IDs', + name: 'tasks', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Task', + }, + default: [], + description: 'Task\'s ID that are going to be used', +} as INodeProperties; + +const considerDependenciesField = { + displayName: 'Consider Dependencies', + name: 'considerDependencies', + type: 'boolean', + default: false, + description: 'Whether to include the target task\'s dependency family (parent and child tasks) in the resulting assignment operation', +} as INodeProperties; + +export const containerFields: INodeProperties[] = [ + { + ...containerTypeField, + displayOptions: { + show: { + resource: [ + 'container', + ], + operation: [ + 'get', + 'addTask', + ], + }, + }, + required: true, + }, + { + ...containerIdField, + displayOptions: { + show: { + resource: [ + 'container', + ], + operation: [ + 'get', + 'addTask', + 'updateTask', + ], + }, + }, + required: true, + }, + { + ...insertTypeField, + displayOptions: { + show: { + resource: [ + 'container', + ], + operation: [ + 'addTask', + ], + }, + }, + required: true, + }, + { + ...indexField, + displayOptions: { + show: { + resource: [ + 'container', + ], + operation: [ + 'addTask', + ], + type: [ + 1, + ], + }, + }, + required: true, + }, + { + ...tasksField, + displayOptions: { + show: { + resource: [ + 'container', + ], + operation: [ + 'addTask', + 'updateTask', + ], + }, + }, + required: true, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'container', + ], + operation: [ + 'addTask', + 'updateTask', + ], + }, + }, + options: [ + { + ...considerDependenciesField, + required: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/DestinationDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/DestinationDescription.ts new file mode 100644 index 0000000000..3feca0cd7c --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/DestinationDescription.ts @@ -0,0 +1,402 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const destinationOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'destination', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new destination', + }, + { + name: 'Get', + value: 'get', + description: 'Get a specific destination', + }, + + ], + default: 'get', + }, +]; + +const unparsedField = { + displayName: 'Unparsed Address', + name: 'unparsed', + type: 'boolean', + description: 'Whether or not the address is specified in a single unparsed string', + default: false, +} as INodeProperties; + +const unparsedAddressField = { + displayName: 'Destination Address', + name: 'address', + type: 'string', + description: 'The destination\'s street address details', + default: '', +} as INodeProperties; + +const unparsedAddressNumberField = { + displayName: 'Number', + name: 'addressNumber', + type: 'string', + description: 'The number component of this address, it may also contain letters', + default: '', +} as INodeProperties; + +const unparsedAddressStreetField = { + displayName: 'Street', + name: 'addressStreet', + type: 'string', + description: 'The name of the street', + default: '', +} as INodeProperties; + +const unparsedAddressCityField = { + displayName: 'City', + name: 'addressCity', + type: 'string', + description: 'The name of the municipality', + default: '', +} as INodeProperties; + +const unparsedAddressCountryField = { + displayName: 'Country', + name: 'addressCountry', + type: 'string', + description: 'The name of the country', + default: '', +} as INodeProperties; + +const unparsedAddressStateField = { + displayName: 'State', + name: 'addressState', + type: 'string', + default: '', +} as INodeProperties; + +const addressNameField = { + displayName: 'Address Name', + name: 'addressName', + type: 'string', + default: '', + description: 'A name associated with this address', +} as INodeProperties; + +const addressApartmentField = { + displayName: 'Apartment', + name: 'addressApartment', + type: 'string', + default: '', + description: 'The suite or apartment number, or any additional relevant information', +} as INodeProperties; + +const addressNoteField = { + displayName: 'Address Notes', + name: 'addressNotes', + type: 'string', + default: '', + description: 'Notes about the destination', +} as INodeProperties; + +const addressPostalCodeField = { + displayName: 'Postal Code', + name: 'addressPostalCode', + type: 'string', + default: '', + description: 'The postal or zip code', +} as INodeProperties; + +export const destinationExternalField = { + displayName: 'Destination', + name: 'destination', + type: 'fixedCollection', + placeholder: 'Add Destination', + default: {}, + options: [ + { + displayName: 'Destination Properties', + name: 'destinationProperties', + default: {}, + values: [ + { + ...unparsedField, + required: false, + }, + { + ...unparsedAddressField, + displayOptions: { + show: { + unparsed: [ + true, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressNumberField, + displayOptions: { + show: { + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressStreetField, + displayOptions: { + show: { + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressCityField, + displayOptions: { + show: { + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressStateField, + displayOptions: { + show: { + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressCountryField, + displayOptions: { + show: { + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + displayOptions: { + show: { + unparsed: [ + false, + ], + }, + }, + ...addressPostalCodeField, + required: false, + }, + { + ...addressNameField, + required: false, + }, + { + ...addressApartmentField, + required: false, + }, + { + ...addressNoteField, + required: false, + }, + ], + }, + ], +} as INodeProperties; + +export const destinationFields: INodeProperties[] = [ + { + displayName: 'Destination ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'destination', + ], + }, + hide: { + operation: [ + 'create', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the destination object for lookup', + }, + { + ...unparsedField, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...unparsedAddressField, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + true, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressNumberField, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressStreetField, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressCityField, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + ...unparsedAddressCountryField, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + false, + ], + }, + }, + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + true, + ], + }, + }, + options: [ + addressApartmentField, + addressNameField, + addressNoteField, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'destination', + ], + operation: [ + 'create', + ], + unparsed: [ + false, + ], + }, + }, + options: [ + addressApartmentField, + addressNameField, + addressNoteField, + addressPostalCodeField, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/HubDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/HubDescription.ts new file mode 100644 index 0000000000..7333ebc721 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/HubDescription.ts @@ -0,0 +1,199 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +import { + destinationExternalField, +} from './DestinationDescription'; + +export const hubOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'hub', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet hub', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Onfleet hubs', + }, + { + name: 'Update', + value: 'update', + description: 'Update an Onfleet hub', + }, + ], + default: 'getAll', + }, +]; + +const nameField = { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'A name to identify the hub', +} as INodeProperties; + +const teamsField = { + displayName: 'Teams Names/IDs', + name: 'teams', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + default: [], + description: 'These are the teams that this Hub will be assigned to', +} as INodeProperties; + +export const hubFields: INodeProperties[] = [ + { + displayName: 'Hub ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the hub object for lookup', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 64, + }, + default: 64, + description: 'How many results to return', + }, + { + ...nameField, + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...destinationExternalField, + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + ...teamsField, + required: false, + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'hub', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + ...destinationExternalField, + required: false, + }, + nameField, + { + ...teamsField, + required: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/OnfleetWebhookDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/OnfleetWebhookDescription.ts new file mode 100644 index 0000000000..4aa0b9790f --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/OnfleetWebhookDescription.ts @@ -0,0 +1,43 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +import { + webhookMapping, +} from '../WebhookMapping'; + +const sort = (a: { name: string }, b: { name: string }) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; +}; + +export const eventDisplay: INodeProperties = { + displayName: 'Trigger On', + name: 'triggerOn', + type: 'options', + options: Object.keys(webhookMapping).map((webhook) => { + const { name, value } = webhookMapping[webhook]; + return { name, value }; + }).sort(sort), + required: true, + default: [], +}; + +export const eventNameField = { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: false, + default: '', + description: 'A name for the webhook for identification', + }, + ], +} as INodeProperties; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/OrganizationDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/OrganizationDescription.ts new file mode 100644 index 0000000000..4f3fa8f89a --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/OrganizationDescription.ts @@ -0,0 +1,53 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const organizationOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'organization', + ], + }, + }, + options: [ + { + name: 'Get My Organization', + value: 'get', + description: 'Retrieve your own organization\'s details', + }, + { + name: 'Get Delegatee Details', + value: 'getDelegatee', + description: 'Retrieve the details of an organization with which you are connected', + }, + + ], + default: 'get', + }, +]; + +export const organizationFields: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'organization', + ], + operation: [ + 'getDelegatee', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the delegatees for lookup', + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/RecipientDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/RecipientDescription.ts new file mode 100644 index 0000000000..c83ab8fc89 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/RecipientDescription.ts @@ -0,0 +1,329 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const recipientOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet recipient', + }, + { + name: 'Get', + value: 'get', + description: 'Get a specific Onfleet recipient', + }, + { + name: 'Update', + value: 'update', + description: 'Update an Onfleet recipient', + }, + ], + default: 'get', + }, +]; + +const additionalRecipientFields: INodeProperties[] = [ + { + displayName: 'Recipient Notes', + name: 'recipientNotes', + type: 'string', + default: '', + description: 'Notes for this recipient: these are global notes that should not be task- or destination-specific', + required: false, + }, + { + displayName: 'Skip Recipient SMS Notifications', + name: 'recipientSkipSMSNotifications', + type: 'boolean', + default: false, + description: 'Whether this recipient has requested to skip SMS notifications', + required: false, + }, +]; + +const recipientName = { + displayName: 'Recipient Name', + name: 'recipientName', + type: 'string', + description: 'The recipient\'s complete name', + default: '', +} as INodeProperties; + +const recipientPhone = { + displayName: 'Recipient Phone', + name: 'recipientPhone', + type: 'string', + description: 'A unique, valid phone number as per the organization\'s country if there\'s no leading + sign. If a phone number has a leading + sign, it will disregard the organization\'s country setting.', + default: '', +} as INodeProperties; + +const updateFields: INodeProperties[] = [ + { + ...recipientName, + required: false, + }, + { + displayName: 'Recipient Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Notes for this recipient: these are global notes that should not be task- or destination-specific', + }, + { + ...recipientPhone, + required: false, + }, + { + displayName: 'Skip Recipient SMS Notifications', + name: 'skipSMSNotifications', + type: 'boolean', + default: false, + description: 'Whether this recipient has requested to skip SMS notifications', + }, +]; + +export const recipientExternalField = { + displayName: 'Recipient', + name: 'recipient', + type: 'fixedCollection', + placeholder: 'Add Recipient', + default: {}, + options: [ + { + displayName: 'Recipient Properties', + name: 'recipientProperties', + default: {}, + values: [ + { + ...recipientName, + required: true, + }, + { + ...recipientPhone, + required: true, + }, + ...additionalRecipientFields, + ], + }, + ], +} as INodeProperties; + +export const recipientFields: INodeProperties[] = [ + { + displayName: 'Get By', + name: 'getBy', + type: 'options', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + name: 'ID', + value: 'id', + }, + { + name: 'Phone', + value: 'phone', + }, + { + name: 'Name', + value: 'name', + }, + ], + description: 'The variable that is used for looking up a recipient', + required: true, + default: 'id', + }, + { + displayName: 'Recipient ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'get', + ], + getBy: [ + 'id', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the recipient object for lookup', + }, + { + displayName: 'Recipient ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the recipient object for lookup', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'get', + ], + getBy: [ + 'name', + ], + }, + }, + default: '', + required: true, + description: 'The name of the recipient for lookup', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'get', + ], + getBy: [ + 'phone', + ], + }, + }, + default: '', + required: true, + description: 'The phone of the recipient for lookup', + }, + { + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'create', + ], + }, + }, + ...recipientName, + required: true, + }, + { + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'create', + ], + }, + }, + ...recipientPhone, + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'create', + ], + }, + }, + options: additionalRecipientFields, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Update Fields', + default: {}, + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'update', + ], + }, + }, + options: updateFields, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Skip Recipient Phone Number Validation', + name: 'recipientSkipPhoneNumberValidation', + type: 'boolean', + default: false, + description: 'Whether to skip validation for this recipient\'s phone number', + required: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/TaskDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/TaskDescription.ts new file mode 100644 index 0000000000..e2fe0e9ce1 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/TaskDescription.ts @@ -0,0 +1,453 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +import { + destinationExternalField, +} from './DestinationDescription'; + +import { + recipientExternalField, +} from './RecipientDescription'; + +export const taskOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet task', + }, + { + name: 'Clone', + value: 'clone', + description: 'Clone an Onfleet task', + }, + { + name: 'Complete', + value: 'complete', + description: 'Force-complete a started Onfleet task', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an Onfleet task', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Onfleet tasks', + }, + { + name: 'Get', + value: 'get', + description: 'Get a specific Onfleet task', + }, + { + name: 'Update', + value: 'update', + description: 'Update an Onfleet task', + }, + + ], + default: 'get', + }, +]; + +const merchantIdField = { + displayName: 'Merchant ID', + name: 'merchant', + type: 'string', + default: '', + description: 'The ID of the organization that will be displayed to the recipient of the task', +} as INodeProperties; + +const executorIdField = { + displayName: 'Executor ID', + name: 'executor', + type: 'string', + default: '', + description: 'The ID of the organization that will be responsible for fulfilling the task', +} as INodeProperties; + +const completeAfterField = { + displayName: 'Complete After', + name: 'completeAfter', + type: 'dateTime', + default: null, + description: 'The earliest time the task should be completed', +} as INodeProperties; + +const completeBeforeField = { + displayName: 'Complete Before', + name: 'completeBefore', + type: 'dateTime', + default: null, + description: 'The latest time the task should be completed', +} as INodeProperties; + +const pickupTaskField = { + displayName: 'Pick Up Task', + name: 'pickupTask', + type: 'boolean', + default: false, + description: 'Whether the task is a pickup task', +} as INodeProperties; + +const notesField = { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Notes for the task', +} as INodeProperties; + +const quantityField = { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + default: 0, + description: 'The number of units to be dropped off while completing this task, for route optimization purposes', +} as INodeProperties; + +const serviceTimeField = { + displayName: 'Service Time', + name: 'serviceTime', + type: 'number', + default: 0, + description: 'The number of minutes to be spent by the worker on arrival at this task\'s destination, for route optimization purposes', +} as INodeProperties; + +export const taskFields: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + hide: { + operation: [ + 'create', + 'getAll', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the task object for lookup', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 64, + }, + default: 64, + description: 'How many results to return', + }, + { + ...destinationExternalField, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + required: true, + }, + { + displayName: 'Complete as a Success', + name: 'success', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'complete', + ], + }, + }, + description: 'Whether the task\'s completion was successful', + required: true, + default: true, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'The starting time of the range. Tasks created or completed at or after this time will be included.', + }, + { + displayName: 'State', + name: 'state', + type: 'multiOptions', + options: [ + { + name: '[All]', + value: 'all', + }, + { + name: 'Active', + value: 2, + }, + { + name: 'Assigned', + value: 1, + }, + { + name: 'Completed', + value: 3, + }, + { + name: 'Unassigned', + value: 0, + }, + ], + default: ['all'], + description: 'The state of the tasks', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'The ending time of the range. Defaults to current time if not specified.', + }, + ], + }, + { + displayName: 'Override Fields', + name: 'overrideFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'clone', + ], + }, + }, + options: [ + { + ...completeAfterField, + }, + { + ...completeBeforeField, + }, + { + displayName: 'Include Barcodes', + name: 'includeBarcodes', + type: 'boolean', + default: false, + }, + { + displayName: 'Include Dependencies', + name: 'includeDependencies', + type: 'boolean', + default: false, + }, + { + displayName: 'Include Metadata', + name: 'includeMetadata', + type: 'boolean', + default: false, + }, + { + ...notesField, + required: false, + }, + { + ...pickupTaskField, + required: false, + }, + { + ...serviceTimeField, + required: false, + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + completeAfterField, + completeBeforeField, + executorIdField, + merchantIdField, + notesField, + pickupTaskField, + quantityField, + serviceTimeField, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'complete', + ], + }, + }, + options: [ + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Completion Notes', + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + completeAfterField, + completeBeforeField, + executorIdField, + merchantIdField, + notesField, + pickupTaskField, + quantityField, + recipientExternalField, + { + displayName: 'Recipient Name Override', + name: 'recipientName', + type: 'string', + default: '', + description: 'Override the recipient name for this task only', + }, + { + displayName: 'Recipient Notes Override', + name: 'recipientNotes', + type: 'string', + default: '', + description: 'Override the recipient notes for this task only', + }, + { + displayName: 'Recipient Skip SMS Notifications Override', + name: 'recipientSkipSMSNotifications', + type: 'boolean', + default: false, + description: 'Whether to override the recipient notification settings for this task', + }, + serviceTimeField, + { + displayName: 'Use Merchant For Proxy Override', + name: 'useMerchantForProxy', + type: 'boolean', + default: false, + description: 'Whether to override the organization ID with the merchant\'s org ID for this task', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/TeamDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/TeamDescription.ts new file mode 100644 index 0000000000..71c770da5e --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/TeamDescription.ts @@ -0,0 +1,600 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const teamOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'team', + ], + }, + }, + options: [ + { + name: 'Auto-Dispatch', + value: 'autoDispatch', + description: 'Automatically dispatch tasks assigned to a team to on-duty drivers', + }, + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet team', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an Onfleet team', + }, + { + name: 'Get', + value: 'get', + description: 'Get a specific Onfleet team', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Onfleet teams', + }, + { + name: 'Get Time Estimates', + value: 'getTimeEstimates', + description: 'Get estimated times for upcoming tasks for a team, returns a selected driver', + }, + { + name: 'Update', + value: 'update', + description: 'Update an Onfleet team', + }, + ], + default: 'getAll', + }, +]; + +const nameField = { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'A unique name for the team', +} as INodeProperties; + +const workersField = { + displayName: 'Workers Names/IDs', + name: 'workers', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getWorkers', + }, + default: [], + description: 'A list of workers', +} as INodeProperties; + +const managersField = { + displayName: 'Administrators Names/IDs', + name: 'managers', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getAdmins', + }, + default: [], + description: 'A list of managing administrators', +} as INodeProperties; + +const hubField = { + displayName: 'Hub Name/ID', + name: 'hub', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getHubs', + }, + default: '', + description: 'The team\'s hub', +} as INodeProperties; + +const enableSelfAssignmentField = { + displayName: 'Self Assignment', + name: 'enableSelfAssignment', + type: 'boolean', + default: false, + description: 'Whether or not to allow drivers to self-assign tasks that are in the Team\'s unassigned container', +} as INodeProperties; + +const maxTasksPerRouteField = { + displayName: 'Max Number Of Tasks Per Route', + name: 'maxTasksPerRoute', + type: 'number', + default: 100, + typeOptions: { + maxValue: 200, + minValue: 1, + }, + description: 'Total number of tasks allowed on a route', +} as INodeProperties; + +const serviceTimeField = { + displayName: 'Service Time', + name: 'serviceTime', + type: 'number', + default: 2, + typeOptions: { + minValue: 0, + }, + description: 'The default service time to apply in Minutes to the tasks when no task service time exists', +} as INodeProperties; + +const routeEndField = { + displayName: 'Route End', + name: 'routeEnd', + type: 'options', + options: [ + { + name: 'Team’s Hub', + value: 'team_hub', + }, + { + name: 'Worker Routing Address', + value: 'worker_routing_address', + }, + { + name: 'Hub', + value: 'hub', + }, + { + name: 'End Anywhere', + value: 'anywhere', + }, + ], + default: '', + description: 'Where the route will end', +} as INodeProperties; + +const maxAllowedDelayField = { + displayName: 'Max Allowed Delay', + name: 'maxAllowedDelay', + type: 'number', + default: 10, + description: 'Max allowed time in minutes that a task can be late', + typeOptions: { + minValue: 1, + }, +} as INodeProperties; + +const longitudeDropOffField = { + displayName: 'Drop Off Longitude', + name: 'dropOffLongitude', + type: 'number', + typeOptions: { + numberPrecision: 14, + }, + default: 0, + description: 'The longitude for drop off location', +} as INodeProperties; + +const latitudeDropOffField = { + displayName: 'Drop Off Latitude', + name: 'dropOffLatitude', + type: 'number', + typeOptions: { + numberPrecision: 14, + }, + default: 0, + description: 'The latitude for drop off location', +} as INodeProperties; + +const longitudePickupField = { + displayName: 'Pick Up Longitude', + name: 'pickupLongitude', + type: 'number', + typeOptions: { + numberPrecision: 14, + }, + default: 0, + description: 'The longitude for pickup location', +} as INodeProperties; + +const latitudePickupField = { + displayName: 'Pick Up Latitude', + name: 'pickupLatitude', + type: 'number', + typeOptions: { + numberPrecision: 14, + }, + default: 0, + description: 'The latitude for pickup location', +} as INodeProperties; + +const pickupTimeField = { + displayName: 'Pick Up Time', + name: 'pickupTime', + type: 'dateTime', + default: '', + description: 'If the request includes pickupLocation, pickupTime must be present if the time is fewer than 3 hours in the future', +} as INodeProperties; + +const restrictedVehicleTypesField = { + displayName: 'Restricted Vehicle Types', + name: 'restrictedVehicleTypes', + type: 'options', + options: [ + { + name: 'Car', + value: 'CAR', + }, + { + name: 'Motorcycle', + value: 'MOTORCYCLE', + }, + { + name: 'Bicycle', + value: 'BICYCLE', + }, + { + name: 'Truck', + value: 'TRUCK', + }, + ], + default: 'CAR', + description: 'Vehicle types to ignore in the query', +} as INodeProperties; + +const serviceTimeEstimateField = { + displayName: 'Service Time', + name: 'serviceTime', + type: 'number', + default: 120, + typeOptions: { + minValue: 0, + }, + description: 'The expected time a worker will take at the pickupLocation, dropoffLocation, or both (as applicable) Unit: seconds', +} as INodeProperties; + +export const teamFields: INodeProperties[] = [ + { + displayName: 'Team ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'get', + 'update', + 'delete', + 'getTimeEstimates', + 'autoDispatch', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the team object for lookup', + }, + { + ...nameField, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...workersField, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...managersField, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + hubField, + enableSelfAssignmentField, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 64, + }, + default: 64, + description: 'How many results to return', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + managersField, + hubField, + nameField, + enableSelfAssignmentField, + workersField, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'autoDispatch', + ], + }, + }, + options: [ + { + displayName: 'Ending Route', + name: 'endingRoute', + type: 'fixedCollection', + placeholder: 'Add Route', + default: {}, + options: [ + { + displayName: 'Ending Route Properties', + name: 'endingRouteProperties', + type: 'fixedCollection', + default: {}, + values: [ + { + ...routeEndField, + required: true, + }, + { + ...hubField, + displayOptions: { + show: { + routeEnd: [ + 'hub', + ], + }, + }, + required: false, + }, + ], + }, + ], + }, + maxAllowedDelayField, + maxTasksPerRouteField, + { + displayName: 'Schedule Time Window', + name: 'scheduleTimeWindow', + type: 'fixedCollection', + placeholder: 'Add Time Window', + default: {}, + options: [ + { + displayName: 'Schedule Time Window Properties', + name: 'scheduleTimeWindowProperties', + type: 'fixedCollection', + default: {}, + values: [ + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + default: '', + }, + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + serviceTimeField, + { + displayName: 'Task Time Window', + name: 'taskTimeWindow', + type: 'fixedCollection', + placeholder: 'Add Time Window', + default: {}, + options: [ + { + displayName: 'Task Time Window Properties', + name: 'taskTimeWindowProperties', + type: 'fixedCollection', + default: {}, + values: [ + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + default: '', + }, + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'getTimeEstimates', + ], + }, + }, + options: [ + { + displayName: 'Drop Off', + name: 'dropOff', + type: 'fixedCollection', + placeholder: 'Add Drop Off', + default: {}, + options: [ + { + displayName: 'DropOff Properties', + name: 'dropOffProperties', + type: 'fixedCollection', + default: {}, + values: [ + { + ...longitudeDropOffField, + required: true, + }, + { + ...latitudeDropOffField, + required: true, + }, + ], + }, + ], + }, + { + displayName: 'Pick Up', + name: 'pickUp', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Pick Up', + options: [ + { + displayName: 'Pick Up Properties', + name: 'pickUpProperties', + type: 'fixedCollection', + default: {}, + values: [ + { + ...longitudePickupField, + required: true, + }, + { + ...latitudePickupField, + required: true, + }, + { + ...pickupTimeField, + required: false, + }, + ], + }, + ], + }, + { + ...restrictedVehicleTypesField, + required: false, + }, + { + ...serviceTimeEstimateField, + required: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/WebhookDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/WebhookDescription.ts new file mode 100644 index 0000000000..5a4a204eac --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/WebhookDescription.ts @@ -0,0 +1,161 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +import { + webhookMapping, +} from '../WebhookMapping'; + +export const webhookOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'webhook', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet webhook', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an Onfleet webhook', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Onfleet webhooks', + }, + ], + default: 'getAll', + }, +]; + +const urlField = { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'The URL that Onfleet should issue a request against as soon as the trigger condition is met. It must be HTTPS and have a valid certificate.', +} as INodeProperties; + +const nameField = { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'A name for the webhook for identification', +} as INodeProperties; + +const triggerField = { + displayName: 'Trigger', + name: 'trigger', + type: 'options', + options: Object.entries(webhookMapping).map(([key, value]) => { + return { + name: value.name, + value: value.key, + }; + }), + default: '', + description: 'The number corresponding to the trigger condition on which the webhook should fire', +} as INodeProperties; + +const thresholdField = { + displayName: 'Threshold', + name: 'threshold', + type: 'number', + default: 0, + description: 'For trigger Task Eta, the time threshold in seconds; for trigger Task Arrival, the distance threshold in meters', +} as INodeProperties; + +export const webhookFields: INodeProperties[] = [ + { + displayName: 'Webhook ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'webhook', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the webhook object for lookup', + }, + { + ...urlField, + displayOptions: { + show: { + resource: [ + 'webhook', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...nameField, + displayOptions: { + show: { + resource: [ + 'webhook', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...triggerField, + displayOptions: { + show: { + resource: [ + 'webhook', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'webhook', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + thresholdField, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/descriptions/WorkerDescription.ts b/packages/nodes-base/nodes/Onfleet/descriptions/WorkerDescription.ts new file mode 100644 index 0000000000..ec2b1b6260 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/descriptions/WorkerDescription.ts @@ -0,0 +1,754 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const workerOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: ['worker'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Onfleet worker', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an Onfleet worker', + }, + { + name: 'Get', + value: 'get', + description: 'Get a specific Onfleet worker', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all Onfleet workers', + }, + { + name: 'Get Schedule', + value: 'getSchedule', + description: 'Get a specific Onfleet worker schedule', + }, + // { + // name: 'Set Worker\'s Schedule', + // value: 'setSchedule', + // description: 'Set the worker\'s schedule', + // }, + { + name: 'Update', + value: 'update', + description: 'Update an Onfleet worker', + }, + ], + default: 'get', + }, +]; + +const byLocationField = { + displayName: 'Search by Location', + name: 'byLocation', + type: 'boolean', + default: false, + description: 'Whether to search for only those workers who are currently within a certain target area', +} as INodeProperties; + +const nameField = { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The worker\'s name', +} as INodeProperties; + +const phoneField = { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'A list of worker’s phone numbers', +} as INodeProperties; + +const capacityField = { + displayName: 'Capacity', + name: 'capacity', + type: 'number', + default: 0, + description: 'The maximum number of units this worker can carry, for route optimization purposes', +} as INodeProperties; + +const displayNameField = { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + default: '', + description: 'This value is used in place of the worker\'s actual name within sms notifications, delivery tracking pages, and across organization boundaries', +} as INodeProperties; + +const vehicleTypeField = { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Bicycle', + value: 'BICYCLE', + }, + { + name: 'Car', + value: 'CAR', + }, + { + name: 'Motorcycle', + value: 'MOTORCYCLE', + }, + { + name: 'Truck', + value: 'TRUCK', + }, + ], + default: '', + description: 'Whether the worker has vehicle or not. If it\'s not provided, this worker will be treated as if on foot.', +} as INodeProperties; + +const vehicleDescriptionField = { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The vehicle\'s make, model, year, or any other relevant identifying details', +} as INodeProperties; + +const vehicleLicensePlateField = { + displayName: 'License Plate', + name: 'licensePlate', + type: 'string', + default: '', + description: 'The vehicle\'s license plate number', +} as INodeProperties; + +const vehicleColorField = { + displayName: 'Color', + name: 'color', + type: 'string', + default: '', + description: 'The vehicle\'s color', +} as INodeProperties; + +const teamsField = { + displayName: 'Teams Names/IDs', + name: 'teams', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + default: [], + description: 'One or more teams of which the worker is a member', +} as INodeProperties; + +const teamsFilterField = { + displayName: 'Teams ID/Name', + name: 'teams', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + default: [], + description: 'A list of the teams that workers must be part of', +} as INodeProperties; + +const statesFilterField = { + displayName: 'States', + name: 'states', + type: 'multiOptions', + options: [ + { + name: 'Active (On-Duty, Active Task)', + value: 2, + }, + { + name: 'Idle (On-Duty, No Active Task)', + value: 1, + }, + { + name: 'Off-Duty', + value: 0, + }, + ], + default: [], + description: 'List of worker states', +} as INodeProperties; + +const phonesFilterField = { + displayName: 'Phones', + name: 'phones', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Phone', + }, + default: [], + description: 'A list of workers\' phone numbers', +} as INodeProperties; + +const filterField = { + displayName: 'Fields to Return', + name: 'filter', + type: 'multiOptions', + options: [ + { + name: 'Account Status', + value: 'accountStatus', + }, + { + name: 'Active Task', + value: 'activeTask', + }, + { + name: 'Capacity', + value: 'capacity', + }, + { + name: 'Delay Time', + value: 'delayTime', + }, + { + name: 'Display Name', + value: 'displayName', + }, + { + name: 'Image Url', + value: 'imageUrl', + }, + { + name: 'Location', + value: 'location', + }, + { + name: 'Metadata', + value: 'metadata', + }, + { + name: 'Name', + value: 'name', + }, + { + name: 'On Duty', + value: 'onDuty', + }, + { + name: 'Organization', + value: 'organization', + }, + { + name: 'Phone', + value: 'phone', + }, + { + name: 'Tasks', + value: 'tasks', + }, + { + name: 'Teams', + value: 'teams', + }, + { + name: 'Time Created', + value: 'timeCreated', + }, + { + name: 'Time Last Modified', + value: 'timeLastModified', + }, + { + name: 'Time Last Seen', + value: 'timeLastSeen', + }, + { + name: 'User Data', + value: 'userData', + }, + { + name: 'Vehicle', + value: 'vehicle', + }, + { + name: 'Worker ID', + value: 'id', + }, + ], + default: [], + description: 'A list of fields to show in the response, if all are not desired', +} as INodeProperties; + +const longitudeFilterField = { + displayName: 'Longitude', + name: 'longitude', + type: 'number', + typeOptions: { + numberPrecision: 14, + }, + default: 0, + description: 'The longitude component of the coordinate pair', +} as INodeProperties; + +const latitudeFilterField = { + displayName: 'Latitude', + name: 'latitude', + type: 'number', + typeOptions: { + numberPrecision: 14, + }, + default: 0, + description: 'The latitude component of the coordinate pair', +} as INodeProperties; + +const radiusFilterField = { + displayName: 'Radius', + name: 'radius', + type: 'number', + typeOptions: { + maxValue: 10000, + minValue: 0, + }, + default: 1000, + description: 'The length in meters of the radius of the spherical area in which to look for workers. Defaults to 1000 if missing. Maximum value is 10000.', +} as INodeProperties; + +const scheduleDateField = { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + description: 'Schedule\'s date', +} as INodeProperties; + +const scheduleTimezoneField = { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'A valid timezone', +} as INodeProperties; + +const scheduleStartField = { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + description: 'Start time', +} as INodeProperties; + +const scheduleEndField = { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + description: 'End time', +} as INodeProperties; + +export const workerFields: INodeProperties[] = [ + { + ...byLocationField, + required: true, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Worker ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'get', + 'getSchedule', + 'setSchedule', + 'update', + 'delete', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the worker object for lookup', + }, + { + ...nameField, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...phoneField, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...teamsField, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + ...longitudeFilterField, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + byLocation: [ + true, + ], + }, + }, + required: true, + }, + { + ...latitudeFilterField, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + byLocation: [ + true, + ], + }, + }, + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 64, + }, + default: 64, + description: 'How many results to return', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + capacityField, + displayNameField, + { + displayName: 'Vehicle', + name: 'vehicle', + type: 'fixedCollection', + placeholder: 'Add Vehicle', + default: {}, + options: [ + { + displayName: 'Vehicle Properties', + name: 'vehicleProperties', + values: [ + { + ...vehicleTypeField, + required: true, + }, + { + ...vehicleDescriptionField, + required: false, + }, + { + ...vehicleLicensePlateField, + required: false, + }, + { + ...vehicleColorField, + required: false, + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + capacityField, + displayNameField, + nameField, + teamsField, + ], + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + byLocation: [ + true, + ], + }, + }, + options: [ + radiusFilterField, + ], + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + byLocation: [ + false, + ], + }, + }, + options: [ + statesFilterField, + teamsFilterField, + phonesFilterField, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + filterField, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Analytics', + name: 'analytics', + type: 'boolean', + default: true, + required: false, + description: 'Whether a more detailed response is needed, includes basic worker duty event, traveled distance (meters) and time analytics', + }, + { + ...filterField, + required: false, + }, + ], + }, + { + displayName: 'Schedule', + name: 'schedule', + type: 'fixedCollection', + placeholder: 'Add Schedule', + displayOptions: { + show: { + resource: [ + 'worker', + ], + operation: [ + 'setSchedule', + ], + }, + }, + default: {}, + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Schedule', + }, + options: [ + { + displayName: 'Schedule Properties', + name: 'scheduleProperties', + default: {}, + values: [ + { + ...scheduleDateField, + required: true, + }, + { + ...scheduleTimezoneField, + required: true, + }, + { + displayName: 'Shifts', + name: 'shifts', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Shift', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Shifts Properties', + name: 'shiftsProperties', + default: {}, + values: [ + { + ...scheduleStartField, + required: true, + }, + { + ...scheduleEndField, + required: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Onfleet/interfaces.ts b/packages/nodes-base/nodes/Onfleet/interfaces.ts new file mode 100644 index 0000000000..c4999a6485 --- /dev/null +++ b/packages/nodes-base/nodes/Onfleet/interfaces.ts @@ -0,0 +1,184 @@ +export interface OnfleetRecipient { + name?: string; + phone?: string; + notes?: string; + skipSMSNotifications?: boolean; + skipPhoneNumberValidation?: boolean; +} + +export interface OnfleetDestinationAddress { + name?: string; + number?: string; + street?: string; + apartment?: string; + city?: string; + state?: string; + postalCode?: string; + country?: string; + unparsed?: string; +} + +export interface OnfleetDestinationOptions { + language?: string; +} + +export interface OnfleetDestination { + address: OnfleetDestinationAddress; + location?: [number, number]; + notes?: string; + options?: OnfleetDestinationOptions; +} + +export interface OnfleetTask { + merchant?: string; + executor?: string; + destination: OnfleetDestination; + recipients: OnfleetRecipient[]; + completeAfter?: number; + completeBefore?: number; + pickupTask?: boolean; + notes?: string; + quantity?: number; + serviceTime?: number; +} + +export interface OnfleetTaskUpdate { + merchant?: string; + executor?: string; + completeAfter?: number; + completeBefore?: number; + pickupTask?: boolean; + notes?: string; + quantity?: number; + serviceTime?: number; +} + +export interface OnfleetListTaskFilters { + from?: number; + to?: number; + lastId?: string; + state?: string; + worker?: string; + completeBeforeBefore?: number; + completeAfterAfter?: number; + dependencies?: string; +} + +export interface OnfleetCloneOverrideTaskOptions { + completeAfter?: number; + completeBefore?: number; + destination?: OnfleetDestination; + notes?: string; + pickupTask?: boolean; + recipients?: OnfleetRecipient[]; + serviceTime?: number; +} + +export interface OnfleetCloneTaskOptions { + includeMetadata?: boolean; + includeBarcodes?: boolean; + includeDependencies?: boolean; + overrides?: OnfleetCloneOverrideTaskOptions; +} + +export interface OnfleetCloneTask { + options?: OnfleetCloneTaskOptions; +} + +export interface OnfleetTaskCompletionDetails { + success: boolean; + notes?: string; +} + +export interface OnfleetTaskComplete { + completionDetails: OnfleetTaskCompletionDetails; +} + +export interface OnfleetAdmins { + name?: string; + email?: string; + phone?: string; + isReadOnly?: boolean; +} + +export interface OnfleetHubs extends OnfleetDestination { + name?: string; + teams?: string[]; +} + +export interface OnfleetVehicle { + type?: string; + description?: string; + licensePlate?: string; + color?: string; +} + +export interface OnfleetWorker { + name?: string; + phone?: string; + vehicle?: OnfleetVehicle; + teams?: string[]; + capacity?: number; + displayName?: string; +} + +export interface OnfleetWorkerFilter { + [key: string]: string | undefined; + filter?: string; + teams?: string; + states?: string; + phones?: string; + analytics?: string; +} + +export interface OnfleetWorkerScheduleEntry { + date?: string; + timezone?: string; + shifts?: [[number, number]]; +} + +export interface OnfleetWebhook { + url?: string; + name?: string; + trigger?: number; + threshold?: number; +} + +export interface OnfleetTeams { + name?: string; + workers?: string[]; + managers?: string[]; + hub?: string; + enableSelfAssignment?: boolean; +} + +export interface OnfleetWorkerSchedule { + entries: OnfleetWorkerScheduleEntry[]; +} + +export interface OnfleetWebhookMapping { + key: number; + name: string; + value: string; +} + +export interface OnfleetWebhooksMapping { + [key: string]: OnfleetWebhookMapping; +} + +export interface OnfleetWorkerEstimates { + dropoffLocation?: string; + pickupLocation?: string; + pickupTime?: number; + restrictedVehicleTypes?: string; + serviceTime?: number; +} + +export interface OnfleetTeamAutoDispatch { + maxTasksPerRoute?: number; + taskTimeWindow?: [number, number]; + scheduleTimeWindow?: [number, number]; + serviceTime?: number; + routeEnd?: string; + maxAllowedDelay?: number; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7e30eee574..cff4481413 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -210,6 +210,7 @@ "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OneSimpleApi.credentials.js", + "dist/credentials/OnfleetApi.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/OrbitApi.credentials.js", "dist/credentials/OuraApi.credentials.js", @@ -543,6 +544,8 @@ "dist/nodes/NextCloud/NextCloud.node.js", "dist/nodes/NocoDB/NocoDB.node.js", "dist/nodes/NoOp/NoOp.node.js", + "dist/nodes/Onfleet/Onfleet.node.js", + "dist/nodes/Onfleet/OnfleetTrigger.node.js", "dist/nodes/Notion/Notion.node.js", "dist/nodes/Notion/NotionTrigger.node.js", "dist/nodes/OneSimpleApi/OneSimpleApi.node.js", diff --git a/packages/workflow/src/NodeErrors.ts b/packages/workflow/src/NodeErrors.ts index 33a591b5fc..d2b6772594 100644 --- a/packages/workflow/src/NodeErrors.ts +++ b/packages/workflow/src/NodeErrors.ts @@ -13,6 +13,7 @@ import { INode, IStatusCodeMessages, JsonObject } from '.'; * Top-level properties where an error message can be found in an API response. */ const ERROR_MESSAGE_PROPERTIES = [ + 'cause', 'error', 'message', 'Message',