diff --git a/packages/backend/src/apps/virtualq/actions/create-waiter/index.js b/packages/backend/src/apps/virtualq/actions/create-waiter/index.js
new file mode 100644
index 00000000..af22d2a5
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/actions/create-waiter/index.js
@@ -0,0 +1,134 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Create waiter',
+ key: 'createWaiter',
+ description: 'Enqueues a waiter to the line with the selected line.',
+ arguments: [
+ {
+ label: 'Line',
+ key: 'lineId',
+ type: 'dropdown',
+ required: true,
+ variables: true,
+ description: 'The line to join',
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listLines',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Phone',
+ key: 'phone',
+ type: 'string',
+ required: true,
+ variables: true,
+ description:
+ "The caller's phone number including country code (for example +4017111112233)",
+ },
+ {
+ label: 'Channel',
+ key: 'channel',
+ type: 'dropdown',
+ description:
+ 'Option describing if the waiter expects a callback or will receive a text message',
+ required: true,
+ variables: true,
+ options: [
+ { label: 'Call back', value: 'CallBack' },
+ { label: 'Call in', value: 'CallIn' },
+ ],
+ },
+ {
+ label: 'Source',
+ key: 'source',
+ type: 'dropdown',
+ description: 'Option describing the source where the caller came from',
+ required: true,
+ variables: true,
+ options: [
+ { label: 'Widget', value: 'Widget' },
+ { label: 'Phone', value: 'Phone' },
+ { label: 'Mobile', value: 'Mobile' },
+ { label: 'App', value: 'App' },
+ { label: 'Other', value: 'Other' },
+ ],
+ },
+ {
+ label: 'Appointment',
+ key: 'appointment',
+ type: 'dropdown',
+ required: true,
+ variables: true,
+ value: false,
+ description:
+ 'If set to true, then this marks this as an appointment. If appointment_time is set, this is automatically set to true.',
+ options: [
+ { label: 'Yes', value: true },
+ { label: 'No', value: false },
+ ],
+ additionalFields: {
+ type: 'query',
+ name: 'getDynamicFields',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listAppointmentFields',
+ },
+ {
+ name: 'parameters.appointment',
+ value: '{parameters.appointment}',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Service phone to call',
+ key: 'servicePhoneToCall',
+ type: 'string',
+ description:
+ "If set, callback uses this number instead of the line's service phone number",
+ required: false,
+ variables: true,
+ },
+ ],
+ async run($) {
+ const {
+ lineId,
+ phone,
+ channel,
+ source,
+ appointment,
+ appointmentTime,
+ servicePhoneToCall,
+ } = $.step.parameters;
+
+ const body = {
+ data: {
+ type: 'waiters',
+ attributes: {
+ line_id: lineId,
+ phone,
+ channel,
+ source,
+ appointment,
+ servicePhoneToCall,
+ },
+ },
+ };
+
+ if (appointment) {
+ body.data.attributes.appointmentTime = appointmentTime;
+ }
+
+ const { data } = await $.http.post('/v2/waiters', body);
+
+ $.setActionItem({ raw: data });
+ },
+});
diff --git a/packages/backend/src/apps/virtualq/actions/delete-waiter/index.js b/packages/backend/src/apps/virtualq/actions/delete-waiter/index.js
new file mode 100644
index 00000000..3922046d
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/actions/delete-waiter/index.js
@@ -0,0 +1,34 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Delete waiter',
+ key: 'deleteWaiter',
+ description:
+ 'Cancels waiting. The provided waiter will be removed from the queue.',
+ arguments: [
+ {
+ label: 'Waiter',
+ key: 'waiterId',
+ type: 'dropdown',
+ required: true,
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listWaiters',
+ },
+ ],
+ },
+ },
+ ],
+ async run($) {
+ const waiterId = $.step.parameters.waiterId;
+
+ const { data } = await $.http.delete(`/v2/waiters/${waiterId}`);
+
+ $.setActionItem({ raw: { output: data } });
+ },
+});
diff --git a/packages/backend/src/apps/virtualq/actions/index.js b/packages/backend/src/apps/virtualq/actions/index.js
new file mode 100644
index 00000000..96dd545e
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/actions/index.js
@@ -0,0 +1,6 @@
+import createWaiter from './create-waiter/index.js';
+import deleteWaiter from './delete-waiter/index.js';
+import showWaiter from './show-waiter/index.js';
+import updateWaiter from './update-waiter/index.js';
+
+export default [createWaiter, deleteWaiter, showWaiter, updateWaiter];
diff --git a/packages/backend/src/apps/virtualq/actions/show-waiter/index.js b/packages/backend/src/apps/virtualq/actions/show-waiter/index.js
new file mode 100644
index 00000000..1166a183
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/actions/show-waiter/index.js
@@ -0,0 +1,33 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Show waiter',
+ key: 'showWaiter',
+ description: 'Returns the complete waiter information.',
+ arguments: [
+ {
+ label: 'Waiter',
+ key: 'waiterId',
+ type: 'dropdown',
+ required: true,
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listWaiters',
+ },
+ ],
+ },
+ },
+ ],
+ async run($) {
+ const waiterId = $.step.parameters.waiterId;
+
+ const { data } = await $.http.get(`/v2/waiters/${waiterId}`);
+
+ $.setActionItem({ raw: data });
+ },
+});
diff --git a/packages/backend/src/apps/virtualq/actions/update-waiter/index.js b/packages/backend/src/apps/virtualq/actions/update-waiter/index.js
new file mode 100644
index 00000000..51f9d8b4
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/actions/update-waiter/index.js
@@ -0,0 +1,157 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Update waiter',
+ key: 'updateWaiter',
+ description: 'Updates a waiter to the line with the selected line.',
+ arguments: [
+ {
+ label: 'Waiter',
+ key: 'waiterId',
+ type: 'dropdown',
+ required: true,
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listWaiters',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Line',
+ key: 'lineId',
+ type: 'dropdown',
+ required: false,
+ variables: true,
+ description: 'Used to find caller if 0 is used for waiter field',
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listLines',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Phone',
+ key: 'phone',
+ type: 'string',
+ required: false,
+ variables: true,
+ description: 'Used to find caller if 0 is used for waiter field',
+ },
+ {
+ label: 'EWT',
+ key: 'serviceWaiterEwt',
+ type: 'string',
+ description: 'EWT as calculated by the service',
+ required: false,
+ variables: true,
+ },
+ {
+ label: 'State',
+ key: 'serviceWaiterState',
+ type: 'dropdown',
+ description: 'State of caller in the call center',
+ required: false,
+ variables: true,
+ options: [
+ { label: 'Waiting', value: 'Waiting' },
+ { label: 'Connected', value: 'Connected' },
+ { label: 'Transferred', value: 'Transferred' },
+ { label: 'Timeout', value: 'Timeout' },
+ { label: 'Canceled', value: 'Canceled' },
+ ],
+ },
+ {
+ label: 'Wait time',
+ key: 'waitTimeWhenUp',
+ type: 'string',
+ description: 'Wait time in seconds before being transferred to agent',
+ required: false,
+ variables: true,
+ },
+ {
+ label: 'Talk time',
+ key: 'talkTime',
+ type: 'string',
+ description: 'Time in seconds spent talking with Agent',
+ required: false,
+ variables: true,
+ },
+ {
+ label: 'Agent',
+ key: 'agentId',
+ type: 'string',
+ description: 'Agent where call was transferred to',
+ required: false,
+ variables: true,
+ },
+ {
+ label: 'Service phone to call',
+ key: 'servicePhoneToCall',
+ type: 'string',
+ description:
+ "If set, callback uses this number instead of the line's service phone number",
+ required: false,
+ variables: true,
+ },
+ ],
+
+ async run($) {
+ const {
+ waiterId,
+ lineId,
+ phone,
+ serviceWaiterEwt,
+ serviceWaiterState,
+ waitTimeWhenUp,
+ talkTime,
+ agentId,
+ servicePhoneToCall,
+ } = $.step.parameters;
+
+ const body = {
+ data: {
+ type: 'waiters',
+ attributes: {
+ line_id: lineId,
+ phone,
+ service_phone_to_call: servicePhoneToCall,
+ },
+ },
+ };
+
+ if (serviceWaiterEwt) {
+ body.data.attributes.service_waiter_ewt = serviceWaiterEwt;
+ }
+
+ if (serviceWaiterState) {
+ body.data.attributes.service_waiter_state = serviceWaiterState;
+ }
+
+ if (talkTime) {
+ body.data.attributes.talk_time = talkTime;
+ }
+
+ if (agentId) {
+ body.data.attributes.agent_id = agentId;
+ }
+
+ if (waitTimeWhenUp) {
+ body.data.attributes.wait_time_when_up = waitTimeWhenUp;
+ }
+
+ const { data } = await $.http.put(`/v2/waiters/${waiterId}`, body);
+
+ $.setActionItem({ raw: data });
+ },
+});
diff --git a/packages/backend/src/apps/virtualq/assets/favicon.svg b/packages/backend/src/apps/virtualq/assets/favicon.svg
new file mode 100644
index 00000000..41162b4d
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/assets/favicon.svg
@@ -0,0 +1,35 @@
+
diff --git a/packages/backend/src/apps/virtualq/auth/index.js b/packages/backend/src/apps/virtualq/auth/index.js
new file mode 100644
index 00000000..41659238
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/auth/index.js
@@ -0,0 +1,21 @@
+import verifyCredentials from './verify-credentials.js';
+import isStillVerified from './is-still-verified.js';
+
+export default {
+ fields: [
+ {
+ key: 'apiKey',
+ label: 'API Key',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: 'API key of the VirtualQ API service.',
+ clickToCopy: false,
+ },
+ ],
+
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/virtualq/auth/is-still-verified.js b/packages/backend/src/apps/virtualq/auth/is-still-verified.js
new file mode 100644
index 00000000..6663679a
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/auth/is-still-verified.js
@@ -0,0 +1,8 @@
+import verifyCredentials from './verify-credentials.js';
+
+const isStillVerified = async ($) => {
+ await verifyCredentials($);
+ return true;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/virtualq/auth/verify-credentials.js b/packages/backend/src/apps/virtualq/auth/verify-credentials.js
new file mode 100644
index 00000000..1899f9e9
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/auth/verify-credentials.js
@@ -0,0 +1,14 @@
+const verifyCredentials = async ($) => {
+ const response = await $.http.get('/v2/call_centers');
+
+ const callCenterNames = response.data.data
+ .map((callCenter) => callCenter.attributes.name)
+ .join(' - ');
+
+ await $.auth.set({
+ screenName: callCenterNames,
+ apiKey: $.auth.data.apiKey,
+ });
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/virtualq/common/add-auth-header.js b/packages/backend/src/apps/virtualq/common/add-auth-header.js
new file mode 100644
index 00000000..91d34fe8
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/common/add-auth-header.js
@@ -0,0 +1,15 @@
+const addAuthHeader = ($, requestConfig) => {
+ if ($.auth.data?.apiKey) {
+ requestConfig.headers['X-API-Key'] = $.auth.data.apiKey;
+ }
+
+ if (requestConfig.method === 'post' || requestConfig.method === 'put') {
+ requestConfig.headers['Content-Type'] = 'application/vnd.api+json';
+ }
+
+ requestConfig.headers['X-Keys-Format'] = 'underscore';
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/virtualq/dynamic-data/index.js b/packages/backend/src/apps/virtualq/dynamic-data/index.js
new file mode 100644
index 00000000..e8aed5d3
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/dynamic-data/index.js
@@ -0,0 +1,4 @@
+import listLines from './list-lines/index.js';
+import listWaiters from './list-waiters/index.js';
+
+export default [listLines, listWaiters];
diff --git a/packages/backend/src/apps/virtualq/dynamic-data/list-lines/index.js b/packages/backend/src/apps/virtualq/dynamic-data/list-lines/index.js
new file mode 100644
index 00000000..de2cf230
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/dynamic-data/list-lines/index.js
@@ -0,0 +1,15 @@
+export default {
+ name: 'List lines',
+ key: 'listLines',
+
+ async run($) {
+ const response = await $.http.get('/v2/lines');
+
+ const lines = response.data.data.map((line) => ({
+ value: line.id,
+ name: line.attributes.name,
+ }));
+
+ return { data: lines };
+ },
+};
diff --git a/packages/backend/src/apps/virtualq/dynamic-data/list-waiters/index.js b/packages/backend/src/apps/virtualq/dynamic-data/list-waiters/index.js
new file mode 100644
index 00000000..d7264cc2
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/dynamic-data/list-waiters/index.js
@@ -0,0 +1,15 @@
+export default {
+ name: 'List waiters',
+ key: 'listWaiters',
+
+ async run($) {
+ const response = await $.http.get('/v2/waiters');
+
+ const lines = response.data.data.map((line) => ({
+ value: line.id,
+ name: line.attributes.name,
+ }));
+
+ return { data: lines };
+ },
+};
diff --git a/packages/backend/src/apps/virtualq/dynamic-fields/index.js b/packages/backend/src/apps/virtualq/dynamic-fields/index.js
new file mode 100644
index 00000000..9473451b
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/dynamic-fields/index.js
@@ -0,0 +1,3 @@
+import listAppointmentFields from './list-appointment-fields/index.js';
+
+export default [listAppointmentFields];
diff --git a/packages/backend/src/apps/virtualq/dynamic-fields/list-appointment-fields/index.js b/packages/backend/src/apps/virtualq/dynamic-fields/list-appointment-fields/index.js
new file mode 100644
index 00000000..8ce2a3ca
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/dynamic-fields/list-appointment-fields/index.js
@@ -0,0 +1,20 @@
+export default {
+ name: 'List appointment fields',
+ key: 'listAppointmentFields',
+
+ async run($) {
+ if ($.step.parameters.appointment) {
+ return [
+ {
+ label: 'Appointment Time',
+ key: 'appointmentTime',
+ type: 'string',
+ required: true,
+ variables: true,
+ description:
+ 'Overrides the estimated up time with this time. Specify number of seconds since 1970 UTC.',
+ },
+ ];
+ }
+ },
+};
diff --git a/packages/backend/src/apps/virtualq/index.js b/packages/backend/src/apps/virtualq/index.js
new file mode 100644
index 00000000..09ae740a
--- /dev/null
+++ b/packages/backend/src/apps/virtualq/index.js
@@ -0,0 +1,22 @@
+import defineApp from '../../helpers/define-app.js';
+import addAuthHeader from './common/add-auth-header.js';
+import auth from './auth/index.js';
+import actions from './actions/index.js';
+import dynamicData from './dynamic-data/index.js';
+import dynamicFields from './dynamic-fields/index.js';
+
+export default defineApp({
+ name: 'VirtualQ',
+ key: 'virtualq',
+ iconUrl: '{BASE_URL}/apps/virtualq/assets/favicon.svg',
+ authDocUrl: '{DOCS_URL}/apps/virtualq/connection',
+ supportsConnections: true,
+ baseUrl: 'https://www.virtualq.io',
+ apiBaseUrl: 'https://api.virtualq.io/api/',
+ primaryColor: '#2E3D59',
+ beforeRequest: [addAuthHeader],
+ auth,
+ actions,
+ dynamicData,
+ dynamicFields,
+});
diff --git a/packages/docs/pages/public/favicons/virtualq.svg b/packages/docs/pages/public/favicons/virtualq.svg
new file mode 100644
index 00000000..41162b4d
--- /dev/null
+++ b/packages/docs/pages/public/favicons/virtualq.svg
@@ -0,0 +1,35 @@
+