Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,62 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
import { calendarIdDropdown } from '../common/props';
export const addBlockedTimeAction = createAction({
auth: acuitySchedulingAuth,
name: 'add_blocked_time',
displayName: 'Add Blocked Off Time',
description: 'Block off a specific time range on a calendar.',
props: {
start: Property.DateTime({
displayName: 'Start Time',
description: 'The start date and time for the block (ISO 8601 format).',
required: true,
}),
end: Property.DateTime({
displayName: 'End Time',
description: 'The end date and time for the block (ISO 8601 format).',
required: true,
}),
calendarID: calendarIdDropdown({
displayName: 'Calendar ID',
description: 'The numeric ID of the calendar to add this block to.',
required: true,
}),
notes: Property.LongText({
displayName: 'Notes',
description: 'Optional notes for the blocked off time.',
required: false,
}),
},
async run(context) {
const props = context.propsValue;
// Basic validation: end time must be after start time
if (new Date(props.start) >= new Date(props.end)) {
throw new Error('End time must be after start time.');
}
const body: Record<string, unknown> = {
start: props.start,
end: props.end,
calendarID: props.calendarID,
};
if (props.notes) body['notes'] = props.notes;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${API_URL}/blocks`,
body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,156 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
import {
addonIdsDropdown,
appointmentTypeIdDropdown,
calendarIdDropdown,
labelIdDropdown,
} from '../common/props';
export const createAppointmentAction = createAction({
auth: acuitySchedulingAuth,
name: 'create_appointment',
displayName: 'Create Appointment',
description: 'Creates a new appointment.',
props: {
datetime: Property.DateTime({
displayName: 'DateTime',
description: 'Date and time of the appointment.',
required: true,
}),
appointmentTypeID: appointmentTypeIdDropdown({
displayName: 'Appointment Type',
description: 'Select the type of appointment.',
required: true,
}),
firstName: Property.ShortText({
displayName: 'First Name',
description: "Client's first name.",
required: true,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
description: "Client's last name.",
required: true,
}),
email: Property.ShortText({
displayName: 'Email',
description: "Client's email address. (Optional if booking as admin).",
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone',
description: "Client's phone number.",
required: false,
}),
timezone: Property.ShortText({
displayName: 'Timezone',
description:
"Client's timezone (e.g., America/New_York). Required for accurate availability checking.",
required: true,
defaultValue: 'UTC',
}),
adminBooking: Property.Checkbox({
displayName: 'Book as Admin',
description:
'Set to true to book as an admin. Disables availability/attribute validations, allows setting notes, and makes Calendar ID required.',
required: false,
defaultValue: false,
}),
calendarID: calendarIdDropdown({
displayName: 'Calendar ID',
description:
'Numeric ID of the calendar. Required if booking as admin. If not provided, Acuity tries to find an available calendar automatically for non-admin bookings.',
required: false,
}),
noEmail: Property.Checkbox({
displayName: 'Suppress Confirmation Email/SMS',
description: 'If true, confirmation emails or SMS will not be sent.',
required: false,
defaultValue: false,
}),
certificate: Property.ShortText({
displayName: 'Certificate Code',
description: 'Package or coupon certificate code.',
required: false,
}),
notes: Property.LongText({
displayName: 'Notes',
description: 'Appointment notes. Only settable if booking as admin.',
required: false,
}),
smsOptIn: Property.Checkbox({
displayName: 'SMS Opt-In',
description:
'Indicates whether the client has explicitly given permission to receive SMS messages.',
required: false,
defaultValue: false,
}),
addonIDs: addonIdsDropdown({
displayName: 'Addons',
description:
'Select addons for the appointment. Addons are filtered by selected Appointment Type if available.',
required: false,
}),
labelId: labelIdDropdown({
displayName: 'Label',
description: 'Apply a label to the appointment. The API currently supports one label.',
required: false,
}),
},
async run(context) {
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.adminBooking) {
queryParams['admin'] = 'true';
}
if (props.noEmail) {
queryParams['noEmail'] = 'true';
}
const body: Record<string, unknown> = {
datetime: props.datetime,
appointmentTypeID: props.appointmentTypeID,
firstName: props.firstName,
lastName: props.lastName,
email: props.email,
};
if (props.calendarID) body['calendarID'] = props.calendarID;
if (props.phone) body['phone'] = props.phone;
if (props.timezone) body['timezone'] = props.timezone;
if (props.certificate) body['certificate'] = props.certificate;
if (props.adminBooking && props.notes) body['notes'] = props.notes;
if (props.smsOptIn) body['smsOptIn'] = props.smsOptIn;
if (props.addonIDs && props.addonIDs.length > 0) {
body['addonIDs'] = props.addonIDs;
}
if (props.labelId) {
body['labelID'] = [{ id: props.labelId }];
}
if (props.adminBooking && !props.calendarID) {
throw new Error('Calendar ID is required when booking as admin.');
}
if (props.adminBooking && props.email === '') {
delete body['email'];
}
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${API_URL}/appointments`,
queryParams: queryParams,
body: body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,62 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
export const createClientAction = createAction({
auth: acuitySchedulingAuth,
name: 'create_client',
displayName: 'Create Client',
description: 'Creates a new client.',
props: {
firstName: Property.ShortText({
displayName: 'First Name',
description: "Client's first name.",
required: true,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
description: "Client's last name.",
required: true,
}),
phone: Property.ShortText({
displayName: 'Phone',
description: "Client's phone number.",
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: "Client's email address.",
required: false,
}),
notes: Property.LongText({
displayName: 'Notes',
description: 'Notes about the client.',
required: false,
}),
},
async run(context) {
const props = context.propsValue;
const body: Record<string, unknown> = {
firstName: props.firstName,
lastName: props.lastName,
};
if (props.phone) body['phone'] = props.phone;
if (props.email) body['email'] = props.email;
if (props.notes) body['notes'] = props.notes;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${API_URL}/clients`,
body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,146 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
import { appointmentTypeIdDropdown, calendarIdDropdown } from '../common/props';
export const findAppointmentAction = createAction({
auth: acuitySchedulingAuth,
name: 'find_appointment',
displayName: 'Find Appointment(s)',
description: 'Find appointments based on various criteria, including client information.',
props: {
// Client Info Filters
firstName: Property.ShortText({
displayName: 'Client First Name',
description: 'Filter appointments by client first name.',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Client Last Name',
description: 'Filter appointments by client last name.',
required: false,
}),
email: Property.ShortText({
displayName: 'Client Email',
description: 'Filter appointments by client e-mail address.',
required: false,
}),
phone: Property.ShortText({
displayName: 'Client Phone',
description:
"Filter appointments by client phone number. URL encode '+' if using country codes (e.g., %2B1234567890).",
required: false,
}),
// Date Filters
minDate: Property.DateTime({
displayName: 'Min Date',
description: 'Only get appointments on or after this date.',
required: false,
}),
maxDate: Property.DateTime({
displayName: 'Max Date',
description: 'Only get appointments on or before this date.',
required: false,
}),
// Other Filters
calendarID: calendarIdDropdown({
displayName: 'Calendar ID',
description: 'Show only appointments on the calendar with this ID.',
required: false,
}),
appointmentTypeID: appointmentTypeIdDropdown({
displayName: 'Appointment Type',
description: 'Show only appointments of this type.',
required: false,
}),
status: Property.StaticDropdown({
displayName: 'Appointment Status',
description: 'Filter by appointment status.',
required: false,
defaultValue: 'scheduled',
options: {
options: [
{ label: 'Scheduled', value: 'scheduled' },
{ label: 'Canceled', value: 'canceled' },
{ label: 'All (Scheduled & Canceled)', value: 'all' },
],
},
}),
// Result Control
maxResults: Property.Number({
displayName: 'Max Results',
description: 'Maximum number of results to return (default 100).',
required: false,
}),
direction: Property.StaticDropdown({
displayName: 'Sort Direction',
description: 'Sort direction for the results.',
required: false,
defaultValue: 'DESC',
options: {
options: [
{ label: 'Descending (DESC)', value: 'DESC' },
{ label: 'Ascending (ASC)', value: 'ASC' },
],
},
}),
},
async run(context) {
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.firstName) queryParams['firstName'] = props.firstName;
if (props.lastName) queryParams['lastName'] = props.lastName;
if (props.email) queryParams['email'] = props.email;
if (props.phone) queryParams['phone'] = props.phone;
// Dates are expected in YYYY-MM-DD by Acuity from examples, Property.DateTime returns ISO string
if (props.minDate) queryParams['minDate'] = props.minDate.split('T')[0];
if (props.maxDate) queryParams['maxDate'] = props.maxDate.split('T')[0];
if (props.calendarID) queryParams['calendarID'] = props.calendarID.toString();
if (props.appointmentTypeID)
queryParams['appointmentTypeID'] = props.appointmentTypeID.toString();
if (props.status === 'canceled') {
queryParams['canceled'] = 'true';
} else if (props.status === 'all') {
queryParams['showall'] = 'true';
} // 'scheduled' is default, no param needed
if (props.maxResults) queryParams['max'] = props.maxResults.toString();
if (props.direction) queryParams['direction'] = props.direction;
// Ensure at least one client identifier or a broad filter like calendarID/appointmentTypeID is used to avoid fetching all appointments if not intended.
// This is a soft validation suggestion for the user, not a hard error.
if (
!props.firstName &&
!props.lastName &&
!props.email &&
!props.phone &&
!props.calendarID &&
!props.appointmentTypeID
) {
console.warn(
"Acuity Scheduling 'Find Appointments': No specific client or calendar/type filters provided. This might return a large number of appointments up to the maximum limit.",
);
}
const response = await httpClient.sendRequest<Array<Record<string, any>>>({
method: HttpMethod.GET,
url: `${API_URL}/appointments`,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return {
found: response.body.length > 0,
data: response.body,
};
},
});

View File

@@ -0,0 +1,41 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
export const findClientAction = createAction({
auth: acuitySchedulingAuth,
name: 'find_client',
displayName: 'Find Client',
description: 'Finds client based on seach term.',
props: {
search: Property.ShortText({
displayName: 'Search Term',
description: 'Filter client list by first name, last name, or phone number.',
required: true,
}),
},
async run(context) {
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.search) {
queryParams['search'] = props.search;
}
const response = await httpClient.sendRequest<Array<Record<string, any>>>({
method: HttpMethod.GET,
url: `${API_URL}/clients`,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return {
found: response.body.length > 0,
data: response.body,
};
},
});

View File

@@ -0,0 +1,7 @@
export * from './create-appointment';
export * from './reschedule-appointment';
export * from './create-client';
export * from './update-client';
export * from './add-blocked-time';
export * from './find-appointments';
export * from './find-client';

View File

@@ -0,0 +1,89 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
import { appointmentTypeIdDropdown, calendarIdDropdown } from '../common/props';
export const rescheduleAppointmentAction = createAction({
auth: acuitySchedulingAuth,
name: 'reschedule_appointment',
displayName: 'Reschedule Appointment',
description: 'Reschedules an existing appointment to a new date/time.',
props: {
id: Property.Number({
displayName: 'Appointment ID',
description: 'The ID of the appointment to reschedule.',
required: true,
}),
appointmentTypeID: appointmentTypeIdDropdown({
displayName: 'Appointment Type',
description: 'Select the type of appointment (used for finding new available slots).',
required: true,
}),
datetime: Property.DateTime({
displayName: 'DateTime',
description: 'New Date and time of the appointment.',
required: true,
}),
timezone: Property.ShortText({
displayName: 'Timezone',
description: "Client's timezone (e.g., America/New_York).",
required: true,
defaultValue: 'UTC',
}),
calendarID: calendarIdDropdown({
displayName: 'New Calendar ID',
description:
'Numeric ID of the new calendar to reschedule to. If blank, stays on current calendar. Submit 0 to auto-assign.',
required: false,
}),
adminReschedule: Property.Checkbox({
displayName: 'Reschedule as Admin',
description: 'Set to true to reschedule as an admin. Disables availability validations.',
required: false,
defaultValue: false,
}),
noEmail: Property.Checkbox({
displayName: 'Suppress Rescheduling Email/SMS',
description: 'If true, rescheduling emails or SMS will not be sent.',
required: false,
defaultValue: false,
}),
},
async run(context) {
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.adminReschedule) {
queryParams['admin'] = 'true';
}
if (props.noEmail) {
queryParams['noEmail'] = 'true';
}
const body: Record<string, unknown> = {
datetime: props.datetime,
};
if (props.calendarID !== undefined) {
// Allow 0 for auto-assign
body['calendarID'] = props.calendarID === 0 ? null : props.calendarID;
}
if (props.timezone) {
body['timezone'] = props.timezone;
}
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: `${API_URL}/appointments/${props.id}/reschedule`,
queryParams,
body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,91 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, HttpMethod, httpClient } from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL } from '../common';
export const updateClientAction = createAction({
auth: acuitySchedulingAuth,
name: 'update_client',
displayName: 'Update Client',
description: 'Updates an existing client.',
props: {
currentFirstName: Property.ShortText({
displayName: 'Current First Name (Identifier)',
description: 'The current first name of the client to update.',
required: true,
}),
currentLastName: Property.ShortText({
displayName: 'Current Last Name (Identifier)',
description: 'The current last name of the client to update.',
required: true,
}),
currentPhone: Property.ShortText({
displayName: 'Current Phone (Identifier, Optional)',
description:
'The current phone number of the client to update. Helps identify the client if names are not unique.',
required: false,
}),
newFirstName: Property.ShortText({
displayName: 'New First Name',
description: "Client's new first name. Leave blank to keep current.",
required: false,
}),
newLastName: Property.ShortText({
displayName: 'New Last Name',
description: "Client's new last name. Leave blank to keep current.",
required: false,
}),
newEmail: Property.ShortText({
displayName: 'New Email',
description: "Client's new email address. Leave blank to keep current.",
required: false,
}),
newPhone: Property.ShortText({
displayName: 'New Phone',
description: "Client's new phone number. Leave blank to keep current.",
required: false,
}),
newNotes: Property.LongText({
displayName: 'New Notes',
description: 'New notes about the client. Leave blank to keep current.',
required: false,
}),
},
async run(context) {
const props = context.propsValue;
const queryParams: Record<string, string> = {
firstName: props.currentFirstName,
lastName: props.currentLastName,
};
if (props.currentPhone) {
queryParams['phone'] = props.currentPhone;
}
const body: Record<string, unknown> = {};
if (props.newFirstName) body['firstName'] = props.newFirstName;
if (props.newLastName) body['lastName'] = props.newLastName;
if (props.newEmail) body['email'] = props.newEmail;
if (props.newPhone) body['phone'] = props.newPhone;
if (props.newNotes) body['notes'] = props.newNotes;
if (Object.keys(body).length === 0) {
throw new Error(
'At least one field to update (New First Name, New Last Name, etc.) must be provided.',
);
}
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: `${API_URL}/clients`,
queryParams,
body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,232 @@
import {
HttpMethod,
httpClient,
HttpRequest,
AuthenticationType,
} from '@activepieces/pieces-common';
export const API_URL = 'https://acuityscheduling.com/api/v1';
export async function fetchAvailableDates(
accessToken: string,
appointmentTypeId: number,
month: string,
timezone?: string,
calendarId?: number,
) {
const queryParams: Record<string, string> = {
month,
appointmentTypeID: appointmentTypeId.toString(),
};
if (timezone) queryParams['timezone'] = timezone;
if (calendarId) queryParams['calendarID'] = calendarId.toString();
const response = await httpClient.sendRequest<Array<{ date: string }>>({
method: HttpMethod.GET,
url: `${API_URL}/availability/dates`,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
});
if (Array.isArray(response.body)) {
return response.body.map((item) => item.date);
}
return [];
}
export async function fetchAvailableTimes(
accessToken: string,
appointmentTypeId: number,
date: string,
timezone?: string,
calendarId?: number,
ignoreAppointmentIDs?: number[],
) {
const params = new URLSearchParams();
params.append('date', date);
params.append('appointmentTypeID', appointmentTypeId.toString());
if (timezone) params.append('timezone', timezone);
if (calendarId) params.append('calendarID', calendarId.toString());
if (ignoreAppointmentIDs && ignoreAppointmentIDs.length > 0) {
ignoreAppointmentIDs.forEach((id) => params.append('ignoreAppointmentIDs[]', id.toString()));
}
const response = await httpClient.sendRequest<Array<{ time: string; datetime: string }>>({
method: HttpMethod.GET,
url: `${API_URL}/availability/times?${params.toString()}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
});
return response.body;
}
// Helper function to get full appointment details
export async function getAppointmentDetails(appointmentId: string, accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointments/${appointmentId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest(request);
return response.body;
}
export async function fetchAppointmentTypes(accessToken: string, includeDeleted = false) {
const queryParams: Record<string, string> = {};
if (includeDeleted) {
queryParams['includeDeleted'] = 'true';
}
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointment-types`,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<
Array<{ id: number; name: string; active: boolean | string }>
>(request);
if (Array.isArray(response.body)) {
// Filter for active types unless includeDeleted is true, and map to dropdown options
return response.body
.filter((type) => includeDeleted || type.active === true || type.active === 'true')
.map((type) => ({ label: type.name, value: type.id }));
}
return [];
}
export async function fetchCalendars(accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/calendars`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<Array<{ id: number; name: string }>>(request);
if (Array.isArray(response.body)) {
return response.body.map((calendar) => ({ label: calendar.name, value: calendar.id }));
}
return [];
}
export async function fetchFormFields(accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/forms`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<
Array<{ id: number; name: string; fields: Array<{ id: number; name: string }> }>
>(request);
if (Array.isArray(response.body)) {
const formFields: Array<{ label: string; value: number }> = [];
response.body.forEach((form) => {
if (Array.isArray(form.fields)) {
form.fields.forEach((field) => {
formFields.push({ label: `${form.name} - ${field.name}`, value: field.id });
});
}
});
return formFields;
}
return [];
}
export async function fetchAddons(accessToken: string, appointmentTypeId?: number) {
// First, fetch all addons
const allAddonsRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointment-addons`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const allAddonsResponse = await httpClient.sendRequest<Array<{ id: number; name: string }>>(
allAddonsRequest,
);
if (!Array.isArray(allAddonsResponse.body)) {
return [];
}
let compatibleAddonIds: number[] | null = null;
// If appointmentTypeId is provided, fetch the specific appointment type to get its compatible addonIDs
if (appointmentTypeId) {
const appointmentTypeRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointment-types/${appointmentTypeId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
try {
const appointmentTypeResponse = await httpClient.sendRequest<{ addonIDs: number[] }>(
appointmentTypeRequest,
);
if (appointmentTypeResponse.body && Array.isArray(appointmentTypeResponse.body.addonIDs)) {
compatibleAddonIds = appointmentTypeResponse.body.addonIDs;
}
} catch (e) {
// Log error or handle if type not found, but still proceed with all addons if necessary
console.warn(
`Could not fetch compatible addons for appointment type ${appointmentTypeId}, returning all addons. Error: ${e}`,
);
}
}
const allAddons = allAddonsResponse.body.map((addon) => ({ label: addon.name, value: addon.id }));
if (compatibleAddonIds) {
return allAddons.filter((addon) => compatibleAddonIds.includes(addon.value));
}
return allAddons;
}
export async function fetchLabels(accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/labels`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<Array<{ id: number; name: string; color: string }>>(
request,
);
if (Array.isArray(response.body)) {
return response.body.map((label) => ({
label: `${label.name} (${label.color})`,
value: label.id,
}));
}
return [];
}

View File

@@ -0,0 +1,105 @@
import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework';
import { fetchAddons, fetchAppointmentTypes, fetchCalendars, fetchLabels } from '.';
import { acuitySchedulingAuth } from '../..';
interface DropdownParams {
displayName: string;
description?: string;
required: boolean;
}
export const appointmentTypeIdDropdown = (params: DropdownParams) =>
Property.Dropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchAppointmentTypes(access_token),
};
},
});
export const calendarIdDropdown = (params: DropdownParams) =>
Property.Dropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchCalendars(access_token),
};
},
});
export const addonIdsDropdown = (params: DropdownParams) =>
Property.MultiSelectDropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: ['appointmentTypeID'],
options: async ({ auth, appointmentTypeID }) => {
if (!auth || !appointmentTypeID) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchAddons(access_token, appointmentTypeID as number),
};
},
});
export const labelIdDropdown = (params: DropdownParams) =>
Property.Dropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchLabels(access_token),
};
},
});

View File

@@ -0,0 +1,136 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
QueryParams,
} from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL, getAppointmentDetails } from '../common';
import { appointmentTypeIdDropdown, calendarIdDropdown } from '../common/props';
const TRIGGER_KEY = 'trigger_appointment_canceled';
export const appointmentCanceledTrigger = createTrigger({
auth: acuitySchedulingAuth,
name: 'appointment_canceled',
displayName: 'Appointment Canceled',
description: 'Triggers when an appointment is canceled.',
props: {
calendarId: calendarIdDropdown({
displayName: 'Calendar',
required: false,
}),
appointmentTypeId: appointmentTypeIdDropdown({
displayName: 'Appointment Type',
required: false,
}),
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${API_URL}/webhooks`,
body: {
target: context.webhookUrl,
event: 'appointment.canceled',
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
const response = await httpClient.sendRequest<{ id: string }>(request);
await context.store.put<string>(TRIGGER_KEY, response.body.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>(TRIGGER_KEY);
if (webhookId) {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${API_URL}/webhooks/${webhookId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
await httpClient.sendRequest(request);
await context.store.delete(TRIGGER_KEY);
}
},
async test(context) {
const { calendarId, appointmentTypeId } = context.propsValue;
const qs: QueryParams = {
max: '10',
canceled: 'true',
};
if (calendarId) qs['calendarID'] = calendarId.toString();
if (appointmentTypeId) qs['appointmentTypeID'] = appointmentTypeId.toString();
const response = await httpClient.sendRequest<Array<Record<string, any>>>({
method: HttpMethod.GET,
url: `${API_URL}/appointments`,
queryParams: qs,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
async run(context) {
const { calendarId, appointmentTypeId } = context.propsValue;
const payload = context.payload.body as {
action: string;
id: number;
calendarID: number;
appointmentTypeID: number;
};
// Check for 'canceled' action
if (
payload.action === 'appointment.canceled' &&
payload.id &&
(!calendarId || calendarId === payload.calendarID) &&
(!appointmentTypeId || appointmentTypeId === payload.appointmentTypeID)
) {
try {
const appointmentDetails = await getAppointmentDetails(
payload.id.toString(),
context.auth.access_token,
);
return [appointmentDetails];
} catch (error) {
console.error(`Failed to fetch appointment details for ID ${payload.id}:`, error);
return [];
}
} else {
console.log('Received webhook for non-canceled event or missing ID:', payload.action);
return [];
}
},
sampleData: {
id: 67890,
firstName: 'Jane',
lastName: 'Smith',
email: 'jane.smith@example.com',
phone: '555-5678',
date: '2023-12-05',
time: '02:00 PM',
datetime: '2023-12-05T14:00:00-0500',
endTime: '03:00 PM',
datetimeCreated: '2023-11-30T10:15:00-0500',
appointmentTypeID: 102,
calendarID: 2,
notes: 'Follow-up meeting.',
price: '75.00',
paid: 'no',
status: 'canceled',
noShow: false,
},
});

View File

@@ -0,0 +1,132 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
QueryParams,
} from '@activepieces/pieces-common';
import { acuitySchedulingAuth } from '../../index';
import { API_URL, getAppointmentDetails } from '../common';
import { appointmentTypeIdDropdown, calendarIdDropdown } from '../common/props';
const TRIGGER_KEY = 'trigger_new_appointment';
export const appointmentScheduledTrigger = createTrigger({
auth: acuitySchedulingAuth,
name: 'new_appointment',
displayName: 'New Appointment',
description: 'Triggers when a new appointment is scheduled.',
props: {
calendarId: calendarIdDropdown({
displayName: 'Calendar',
required: false,
}),
appointmentTypeId: appointmentTypeIdDropdown({
displayName: 'Appointment Type',
required: false,
}),
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${API_URL}/webhooks`,
body: {
target: context.webhookUrl,
event: 'appointment.scheduled',
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
const response = await httpClient.sendRequest<{ id: string }>(request);
await context.store.put(TRIGGER_KEY, response.body.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>(TRIGGER_KEY);
if (webhookId) {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${API_URL}/webhooks/${webhookId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
await httpClient.sendRequest(request);
await context.store.delete(TRIGGER_KEY);
}
},
async test(context) {
const { calendarId, appointmentTypeId } = context.propsValue;
const qs: QueryParams = {
max: '10',
};
if (calendarId) qs['calendarID'] = calendarId.toString();
if (appointmentTypeId) qs['appointmentTypeID'] = appointmentTypeId.toString();
const response = await httpClient.sendRequest<Array<Record<string, any>>>({
method: HttpMethod.GET,
url: `${API_URL}/appointments`,
queryParams: qs,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
return response.body;
},
async run(context) {
const { calendarId, appointmentTypeId } = context.propsValue;
const payload = context.payload.body as {
action: string;
id: number;
calendarID: number;
appointmentTypeID: number;
};
if (
payload.action === 'appointment.scheduled' &&
payload.id &&
(!calendarId || calendarId === payload.calendarID) &&
(!appointmentTypeId || appointmentTypeId === payload.appointmentTypeID)
) {
try {
const appointmentDetails = await getAppointmentDetails(
payload.id.toString(),
context.auth.access_token,
);
return [appointmentDetails];
} catch (error) {
console.error(`Failed to fetch appointment details for ID ${payload.id}:`, error);
return [];
}
} else {
console.log('Received webhook for non-scheduled event or missing ID:', payload.action);
return [];
}
},
sampleData: {
id: 12345,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '555-1234',
date: '2023-12-01',
time: '10:00 AM',
datetime: '2023-12-01T10:00:00-0500',
endTime: '11:00 AM',
datetimeCreated: '2023-11-28T14:30:00-0500',
appointmentTypeID: 101,
calendarID: 1,
notes: 'First appointment.',
price: '50.00',
paid: 'yes',
status: 'scheduled',
},
});

View File

@@ -0,0 +1,2 @@
export * from './appointment-scheduled';
export * from './appointment-canceled'