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:
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './appointment-scheduled';
|
||||
export * from './appointment-canceled'
|
||||
Reference in New Issue
Block a user