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,302 @@
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { formatDateTime, zohoBookingsAuth, zohoBookingsCommon } from '../common';
|
||||
|
||||
export const bookAppointment = createAction({
|
||||
auth: zohoBookingsAuth,
|
||||
name: 'bookAppointment',
|
||||
displayName: 'Book Appointment',
|
||||
description: 'Book an appointment for a customer for a desired service',
|
||||
props: {
|
||||
workspace_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Workspace',
|
||||
description: 'Select the workspace for the appointment',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Authentication required',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const workspaces = await zohoBookingsCommon.fetchWorkspaces(
|
||||
(auth as any).access_token,
|
||||
location
|
||||
);
|
||||
|
||||
return {
|
||||
options: workspaces.map((workspace: any) => ({
|
||||
label: workspace.name,
|
||||
value: workspace.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load workspaces',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
service_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Service',
|
||||
description: 'Select the service for which the appointment is booked',
|
||||
required: true,
|
||||
refreshers: ['workspace_id'],
|
||||
options: async ({ auth, workspace_id }) => {
|
||||
if (!workspace_id || !auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please enter workspace ID first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const services = await zohoBookingsCommon.fetchServices(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
workspace_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: services.map((service: any) => ({
|
||||
label: `${service.name} (${service.duration})`,
|
||||
value: service.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load services',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
staff_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Staff',
|
||||
description:
|
||||
'Select the staff member (use this OR resource_id OR group_id)',
|
||||
required: false,
|
||||
refreshers: ['service_id'],
|
||||
options: async ({ auth, service_id }) => {
|
||||
if (!service_id || !auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please select service first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const staff = await zohoBookingsCommon.fetchStaff(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
service_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: staff.map((member: any) => ({
|
||||
label: `${member.name} - ${member.designation || 'Staff'}`,
|
||||
value: member.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load staff',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
resource_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Resource',
|
||||
description: 'Select the resource (use this OR staff_id OR group_id)',
|
||||
required: false,
|
||||
refreshers: ['service_id'],
|
||||
options: async ({ auth, service_id }) => {
|
||||
if (!service_id || !auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please select service first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = auth.props?.['location'] as string || 'zoho.com';
|
||||
const resources = await zohoBookingsCommon.fetchResources(
|
||||
auth.access_token,
|
||||
location,
|
||||
service_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: resources.map((resource: any) => ({
|
||||
label: resource.name,
|
||||
value: resource.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load resources',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
group_id: Property.ShortText({
|
||||
displayName: 'Group ID',
|
||||
description:
|
||||
'The unique ID of the staff group for collective booking (use this OR staff_id OR resource_id)',
|
||||
required: false,
|
||||
}),
|
||||
from_time: Property.DateTime({
|
||||
displayName: 'From Time',
|
||||
description:
|
||||
'The starting time for the appointment (format: mm-dd-yyyy HH:mm:ss)',
|
||||
required: true,
|
||||
}),
|
||||
to_time: Property.DateTime({
|
||||
displayName: 'To Time',
|
||||
description:
|
||||
'End time for resource booking (optional, format: dd-MMM-yyyy HH:mm:ss)',
|
||||
required: false,
|
||||
}),
|
||||
timezone: Property.ShortText({
|
||||
displayName: 'Timezone',
|
||||
description: 'The timezone for the appointment (optional)',
|
||||
required: false,
|
||||
}),
|
||||
customer_name: Property.ShortText({
|
||||
displayName: 'Customer Name',
|
||||
description: 'Name of the customer',
|
||||
required: true,
|
||||
}),
|
||||
customer_email: Property.ShortText({
|
||||
displayName: 'Customer Email',
|
||||
description: 'Email address of the customer',
|
||||
required: true,
|
||||
}),
|
||||
customer_phone: Property.ShortText({
|
||||
displayName: 'Customer Phone',
|
||||
description: 'Phone number of the customer',
|
||||
required: true,
|
||||
}),
|
||||
notes: Property.LongText({
|
||||
displayName: 'Notes',
|
||||
description: 'Additional information about the appointment (optional)',
|
||||
required: false,
|
||||
}),
|
||||
additional_fields: Property.Json({
|
||||
displayName: 'Additional Fields',
|
||||
description: 'Additional customer details as JSON object (optional)',
|
||||
required: false,
|
||||
}),
|
||||
cost_paid: Property.Number({
|
||||
displayName: 'Cost Paid',
|
||||
description: 'Amount paid for the booking (optional)',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
const location = auth.props?.['location'] as string || 'zoho.com';
|
||||
|
||||
// Validate props using Zod schema
|
||||
await propsValidation.validateZod(
|
||||
propsValue,
|
||||
zohoBookingsCommon.bookAppointmentSchema
|
||||
);
|
||||
|
||||
// Validate that at least one of staff_id, resource_id, or group_id is provided
|
||||
if (
|
||||
!propsValue.staff_id &&
|
||||
!propsValue.resource_id &&
|
||||
!propsValue.group_id
|
||||
) {
|
||||
throw new Error(
|
||||
'Either staff_id, resource_id, or group_id must be provided'
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare customer details
|
||||
const customer_details = {
|
||||
name: String(propsValue.customer_name),
|
||||
email: String(propsValue.customer_email),
|
||||
phone_number: String(propsValue.customer_phone),
|
||||
};
|
||||
const customer_details_json = JSON.stringify(customer_details);
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append('service_id', propsValue.service_id as string);
|
||||
formData.append('from_time', formatDateTime(propsValue.from_time));
|
||||
|
||||
formData.append('customer_details', customer_details_json);
|
||||
|
||||
// Add optional staff/resource/group ID
|
||||
if (propsValue.staff_id != null && propsValue.staff_id) {
|
||||
formData.append('staff_id', propsValue.staff_id as string);
|
||||
}
|
||||
if (propsValue.resource_id != null && propsValue.resource_id) {
|
||||
formData.append('resource_id', propsValue.resource_id as string);
|
||||
}
|
||||
if (propsValue.group_id != null && propsValue.group_id) {
|
||||
formData.append('group_id', propsValue.group_id as string);
|
||||
}
|
||||
|
||||
// Add optional fields
|
||||
if (propsValue.to_time) {
|
||||
formData.append('to_time', formatDateTime(propsValue.to_time));
|
||||
}
|
||||
if (propsValue.timezone) {
|
||||
formData.append('timezone', propsValue.timezone);
|
||||
}
|
||||
if (propsValue.notes) {
|
||||
formData.append('notes', propsValue.notes);
|
||||
}
|
||||
if (propsValue.additional_fields) {
|
||||
formData.append(
|
||||
'additional_fields',
|
||||
JSON.stringify(propsValue.additional_fields)
|
||||
);
|
||||
}
|
||||
if (propsValue.cost_paid) {
|
||||
formData.append(
|
||||
'payment_info',
|
||||
JSON.stringify({ cost_paid: propsValue.cost_paid.toString() })
|
||||
);
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${zohoBookingsCommon.baseUrl(location)}/appointment`,
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.body.response.status === 'failure') {
|
||||
throw new Error(`${response.body.response.errormessage}`);
|
||||
}
|
||||
return response.body.response;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { bookingIdDropdown, zohoBookingsAuth, zohoBookingsCommon } from '../common';
|
||||
|
||||
export const cancelAppointment = createAction({
|
||||
auth: zohoBookingsAuth,
|
||||
name: 'cancelAppointment',
|
||||
displayName: 'Cancel Appointment',
|
||||
description:
|
||||
'Update the status of a booking (cancel, complete, or mark as no-show)',
|
||||
props: {
|
||||
from_time: Property.DateTime({
|
||||
displayName: 'From Time',
|
||||
description:
|
||||
'The starting time for the appointment (format: mm-dd-yyyy HH:mm:ss)',
|
||||
required: true,
|
||||
}),
|
||||
to_time: Property.DateTime({
|
||||
displayName: 'To Time',
|
||||
description:
|
||||
'The ending time for the appointment (format: mm-dd-yyyy HH:mm:ss)',
|
||||
required: false,
|
||||
}),
|
||||
booking_id: bookingIdDropdown,
|
||||
},
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
const location = auth.props?.['location'] as string || 'zoho.com';
|
||||
|
||||
// Validate props using Zod schema
|
||||
await propsValidation.validateZod(
|
||||
propsValue,
|
||||
zohoBookingsCommon.cancelAppointmentSchema
|
||||
);
|
||||
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append('booking_id', propsValue.booking_id as string);
|
||||
formData.append('action', 'cancel');
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${zohoBookingsCommon.baseUrl(location)}/updateappointment`,
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { zohoBookingsAuth, zohoBookingsCommon } from '../common';
|
||||
|
||||
export const fetchAvailability = createAction({
|
||||
auth: zohoBookingsAuth,
|
||||
name: 'fetchAvailability',
|
||||
displayName: 'Fetch Availability',
|
||||
description: 'Fetch availability of appointments across services',
|
||||
props: {
|
||||
workspace_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Workspace',
|
||||
description: 'Select the workspace to fetch availability for',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Authentication required',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const workspaces = await zohoBookingsCommon.fetchWorkspaces(
|
||||
(auth as any).access_token,
|
||||
location
|
||||
);
|
||||
|
||||
return {
|
||||
options: workspaces.map((workspace: any) => ({
|
||||
label: workspace.name,
|
||||
value: workspace.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load workspaces',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
service_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Service',
|
||||
description: 'Select the service for which availability is to be fetched',
|
||||
required: true,
|
||||
refreshers: ['workspace_id'],
|
||||
options: async ({ auth, workspace_id }) => {
|
||||
if (!workspace_id || !auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please enter workspace ID first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const services = await zohoBookingsCommon.fetchServices(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
workspace_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: services.map((service: any) => ({
|
||||
label: `${service.name} (${service.duration})`,
|
||||
value: service.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load services',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
staff_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Staff',
|
||||
description: 'Select the staff member (use this OR group_id OR resource_id)',
|
||||
required: false,
|
||||
refreshers: ['service_id'],
|
||||
options: async ({ auth, service_id }) => {
|
||||
if (!service_id || !auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please select service first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const staff = await zohoBookingsCommon.fetchStaff(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
service_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: staff.map((member: any) => ({
|
||||
label: `${member.name} - ${member.designation || 'Staff'}`,
|
||||
value: member.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load staff',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
group_id: Property.ShortText({
|
||||
displayName: 'Group ID',
|
||||
description: 'The unique ID of the staff group associated with the service (use this OR staff_id OR resource_id)',
|
||||
required: false,
|
||||
}),
|
||||
resource_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Resource',
|
||||
description: 'Select the resource (use this OR staff_id OR group_id)',
|
||||
required: false,
|
||||
refreshers: ['service_id'],
|
||||
options: async ({ auth, service_id }) => {
|
||||
if (!service_id || !auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please select service first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const resources = await zohoBookingsCommon.fetchResources(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
service_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: resources.map((resource: any) => ({
|
||||
label: resource.name,
|
||||
value: resource.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load resources',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
selected_date: Property.DateTime({
|
||||
displayName: 'Selected Date',
|
||||
description: 'The date on which services are checked for availability (format: mm-dd-yyyy)',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
const location = auth.props?.['location'] as string || 'zoho.com';
|
||||
|
||||
// Validate props using Zod schema
|
||||
await propsValidation.validateZod(propsValue, zohoBookingsCommon.fetchAvailabilitySchema);
|
||||
|
||||
// Validate that at least one of staff_id, group_id, or resource_id is provided
|
||||
if (!propsValue.staff_id && !propsValue.group_id && !propsValue.resource_id) {
|
||||
throw new Error('Either staff_id, group_id, or resource_id must be provided');
|
||||
}
|
||||
|
||||
// Format date to YYYY-MM-DD format
|
||||
const formatDate = (date: string) => {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Prepare query parameters
|
||||
const queryParams: Record<string, string> = {
|
||||
service_id: propsValue.service_id,
|
||||
selected_date: formatDate(propsValue.selected_date),
|
||||
};
|
||||
|
||||
// Add the staff/group/resource ID (only one should be provided)
|
||||
if (propsValue.staff_id) {
|
||||
queryParams['staff_id'] = propsValue.staff_id;
|
||||
}
|
||||
if (propsValue.group_id) {
|
||||
queryParams['group_id'] = propsValue.group_id;
|
||||
}
|
||||
if (propsValue.resource_id) {
|
||||
queryParams['resource_id'] = propsValue.resource_id;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${zohoBookingsCommon.baseUrl(location)}/availableslots`,
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
|
||||
},
|
||||
queryParams,
|
||||
});
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { bookingIdDropdown, zohoBookingsAuth, zohoBookingsCommon } from '../common';
|
||||
|
||||
export const getAppointmentDetails = createAction({
|
||||
auth: zohoBookingsAuth,
|
||||
name: 'getAppointmentDetails',
|
||||
displayName: 'Get Appointment Details',
|
||||
description: 'Get details of an appointment using its booking ID',
|
||||
props: {
|
||||
from_time: Property.DateTime({
|
||||
displayName: 'From Time',
|
||||
description: 'The start time of the appointment (in ISO 8601 format)',
|
||||
required: true,
|
||||
}),
|
||||
to_time: Property.DateTime({
|
||||
displayName: 'To Time',
|
||||
description: 'The end time of the appointment (in ISO 8601 format)',
|
||||
required: false,
|
||||
}),
|
||||
booking_id: bookingIdDropdown
|
||||
},
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
const location = auth.props?.['location'] as string || 'zoho.com';
|
||||
|
||||
// Validate props using Zod schema
|
||||
await propsValidation.validateZod(propsValue, zohoBookingsCommon.getAppointmentDetailsSchema);
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${zohoBookingsCommon.baseUrl(location)}/getappointment`,
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
|
||||
|
||||
},
|
||||
queryParams: {
|
||||
booking_id: propsValue.booking_id as string,
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import {
|
||||
bookingIdDropdown,
|
||||
formatDateTime,
|
||||
zohoBookingsAuth,
|
||||
zohoBookingsCommon,
|
||||
} from '../common';
|
||||
import { access } from 'fs';
|
||||
|
||||
export const rescheduleAppointment = createAction({
|
||||
auth: zohoBookingsAuth,
|
||||
name: 'rescheduleAppointment',
|
||||
displayName: 'Reschedule Appointment',
|
||||
description:
|
||||
'Reschedule an appointment to a different time or to a different staff',
|
||||
props: {
|
||||
from_time: Property.DateTime({
|
||||
displayName: 'From Time',
|
||||
description:
|
||||
'Start of the date range used to fetch existing bookings (to help you select the Booking ID to reschedule). Not sent to the reschedule API. Format: dd-MMM-yyyy HH:mm:ss',
|
||||
required: true,
|
||||
}),
|
||||
to_time: Property.DateTime({
|
||||
displayName: 'To Time',
|
||||
description:
|
||||
'End of the date range used to fetch existing bookings (optional). Not sent to the reschedule API. Format: dd-MMM-yyyy HH:mm:ss',
|
||||
required: false,
|
||||
}),
|
||||
booking_id: bookingIdDropdown,
|
||||
service_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Service (Optional)',
|
||||
description: 'Select service to filter staff options',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Authentication required',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const services = await zohoBookingsCommon.fetchServices(
|
||||
(auth as any).access_token,
|
||||
location
|
||||
);
|
||||
|
||||
return {
|
||||
options: services.map((service: any) => ({
|
||||
label: `${service.name} (${service.duration})`,
|
||||
value: service.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load services',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
staff_id: Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Staff',
|
||||
description:
|
||||
'Select the staff to reschedule to (use this OR group_id OR start_time)',
|
||||
required: false,
|
||||
refreshers: ['service_id'],
|
||||
options: async ({ auth, service_id }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Authentication required',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const staff = await zohoBookingsCommon.fetchStaff(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
service_id as string
|
||||
);
|
||||
|
||||
return {
|
||||
options: staff.map((member: any) => ({
|
||||
label: `${member.name} - ${member.designation || 'Staff'}`,
|
||||
value: member.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load staff',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
group_id: Property.ShortText({
|
||||
displayName: 'Group ID',
|
||||
description:
|
||||
'The unique ID of the staff group to reschedule to (use this OR staff_id OR start_time)',
|
||||
required: false,
|
||||
}),
|
||||
start_time: Property.DateTime({
|
||||
displayName: 'New Start Time',
|
||||
description:
|
||||
'The new time to reschedule the appointment to (format: dd-MMM-yyyy HH:mm:ss, use this OR staff_id OR group_id)',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
const location = auth.props?.['location'] as string || 'zoho.com';
|
||||
|
||||
// Validate props using Zod schema
|
||||
await propsValidation.validateZod(
|
||||
propsValue,
|
||||
zohoBookingsCommon.rescheduleAppointmentSchema
|
||||
);
|
||||
|
||||
// Validate that at least one of staff_id, group_id, or start_time is provided
|
||||
if (
|
||||
!propsValue.staff_id &&
|
||||
!propsValue.group_id &&
|
||||
!propsValue.start_time
|
||||
) {
|
||||
throw new Error(
|
||||
'Either staff_id, group_id, or start_time must be provided'
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append('booking_id', propsValue.booking_id as string);
|
||||
|
||||
// Add the reschedule parameter (only one should be provided)
|
||||
if (propsValue.staff_id) {
|
||||
formData.append('staff_id', propsValue.staff_id);
|
||||
}
|
||||
if (propsValue.group_id) {
|
||||
formData.append('group_id', propsValue.group_id);
|
||||
}
|
||||
if (propsValue.start_time) {
|
||||
formData.append('start_time', formatDateTime(propsValue.start_time));
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${zohoBookingsCommon.baseUrl(location)}/rescheduleappointment`,
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
console.log(auth.access_token)
|
||||
const responseBody = response.body.response;
|
||||
if (responseBody.status !== 'success') {
|
||||
throw new Error(
|
||||
`Failed to reschedule appointment: ${responseBody.message || 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
if (responseBody.returnvalue.Status == "failure") {
|
||||
throw new Error(responseBody.returnvalue.message || 'Failed to reschedule appointment');
|
||||
}
|
||||
return responseBody;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { PieceAuth, Property } from '@activepieces/pieces-framework';
|
||||
import { OAuth2GrantType } from '@activepieces/shared';
|
||||
import * as schemas from './schemas';
|
||||
|
||||
export const zohoBookingsAuth = PieceAuth.OAuth2({
|
||||
props: {
|
||||
location: Property.StaticDropdown({
|
||||
displayName: 'Data Center',
|
||||
description: 'The data center location of your Zoho Bookings account',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{
|
||||
label: 'zoho.com (United States)',
|
||||
value: 'zoho.com',
|
||||
},
|
||||
{
|
||||
label: 'zoho.eu (Europe)',
|
||||
value: 'zoho.eu',
|
||||
},
|
||||
{
|
||||
label: 'zoho.in (India)',
|
||||
value: 'zoho.in',
|
||||
},
|
||||
{
|
||||
label: 'zoho.com.au (Australia)',
|
||||
value: 'zoho.com.au',
|
||||
},
|
||||
{
|
||||
label: 'zoho.jp (Japan)',
|
||||
value: 'zoho.jp',
|
||||
},
|
||||
{
|
||||
label: 'zoho.com.cn (China)',
|
||||
value: 'zoho.com.cn',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Connect your Zoho Bookings account using OAuth2',
|
||||
grantType: OAuth2GrantType.AUTHORIZATION_CODE,
|
||||
required: true,
|
||||
authUrl: 'https://accounts.{location}/oauth/v2/auth',
|
||||
tokenUrl: 'https://accounts.{location}/oauth/v2/token',
|
||||
scope: ['zohobookings.data.CREATE', 'zohobookings.data.READ'],
|
||||
});
|
||||
|
||||
export const zohoBookingsCommon = {
|
||||
baseUrl: (location = 'zoho.com') => {
|
||||
return `https://www.zohoapis.${location
|
||||
.substring(5)
|
||||
.trim()}/bookings/v1/json`;
|
||||
},
|
||||
baseHeaders: (accessToken: string) => {
|
||||
return {
|
||||
Authorization: `Zoho-oauthtoken ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
},
|
||||
|
||||
// API Methods
|
||||
async fetchWorkspaces(accessToken: string, location: string) {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${this.baseUrl(location)}/workspaces`,
|
||||
headers: this.baseHeaders(accessToken),
|
||||
});
|
||||
|
||||
return response.body?.response?.returnvalue?.data || [];
|
||||
},
|
||||
|
||||
async fetchServices(
|
||||
accessToken: string,
|
||||
location: string,
|
||||
workspaceId?: string
|
||||
) {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (workspaceId) {
|
||||
queryParams['workspace_id'] = workspaceId;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${this.baseUrl(location)}/services`,
|
||||
headers: this.baseHeaders(accessToken),
|
||||
queryParams,
|
||||
});
|
||||
|
||||
return response.body?.response?.returnvalue?.data || [];
|
||||
},
|
||||
|
||||
async fetchResources(
|
||||
accessToken: string,
|
||||
location: string,
|
||||
serviceId?: string
|
||||
) {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (serviceId) {
|
||||
queryParams['service_id'] = serviceId;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${this.baseUrl(location)}/resources`,
|
||||
headers: this.baseHeaders(accessToken),
|
||||
queryParams,
|
||||
});
|
||||
|
||||
return response.body?.response?.returnvalue?.data || [];
|
||||
},
|
||||
|
||||
async fetchStaff(accessToken: string, location: string, serviceId?: string) {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (serviceId) {
|
||||
queryParams['service_id'] = serviceId;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${this.baseUrl(location)}/staffs`,
|
||||
headers: this.baseHeaders(accessToken),
|
||||
queryParams,
|
||||
});
|
||||
|
||||
return response.body?.response?.returnvalue?.data || [];
|
||||
},
|
||||
|
||||
async fetchAppointments(
|
||||
accessToken: string,
|
||||
location: string,
|
||||
options?: {
|
||||
serviceId?: string;
|
||||
staffId?: string;
|
||||
status?: string;
|
||||
from_time?: string;
|
||||
to_time?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
) {
|
||||
const payload: Record<string, string | number> = {};
|
||||
|
||||
// if (options?.staffId !== undefined) payload['staff_id'] = Number(options.staffId);
|
||||
// if (options?.serviceId) payload['service_id'] = options.serviceId;
|
||||
if (options?.from_time) payload['from_time'] = options.from_time;
|
||||
// if (options?.to_time) payload['to_time'] = options.to_time;
|
||||
// if (options?.status) payload['status'] = options.status;
|
||||
// if (options?.page != null) payload['page'] = options.page;
|
||||
// if (options?.perPage != null) payload['per_page'] = options.perPage;
|
||||
// if (options?.need_customer_more_info != null) {
|
||||
// payload.need_customer_more_info = String(options.need_customer_more_info);
|
||||
// }
|
||||
// if (options?.customer_name) payload['customer_name'] = options.customer_name;
|
||||
// if (options?.customer_email) payload['customer_email'] = options.customer_email;
|
||||
|
||||
// Send as multipart/form-data with a single `data` field
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify(payload));
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${this.baseUrl(location)}/fetchappointment`,
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
console.log(JSON.stringify(response.body.response));
|
||||
|
||||
return response.body?.response?.returnvalue?.response || [];
|
||||
},
|
||||
|
||||
// Schemas
|
||||
bookAppointmentSchema: schemas.bookAppointment,
|
||||
rescheduleAppointmentSchema: schemas.rescheduleAppointment,
|
||||
fetchAvailabilitySchema: schemas.fetchAvailability,
|
||||
getAppointmentDetailsSchema: schemas.getAppointmentDetails,
|
||||
cancelAppointmentSchema: schemas.cancelAppointment,
|
||||
};
|
||||
|
||||
export const formatDateTime = (date: string) => {
|
||||
const d = new Date(date);
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
const month = months[d.getMonth()];
|
||||
const year = d.getFullYear();
|
||||
const hours = d.getHours().toString().padStart(2, '0');
|
||||
const minutes = d.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = d.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
return `${day}-${month}-${year} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
export const bookingIdDropdown = Property.Dropdown({
|
||||
auth: zohoBookingsAuth,
|
||||
displayName: 'Appointment',
|
||||
description: 'Select the appointment to get details for',
|
||||
required: true,
|
||||
refreshers: ['from_time', 'to_time'],
|
||||
options: async ({ auth, from_time, to_time }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Authentication required',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
if (!from_time) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please select From Time first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
const formattedFromTime = formatDateTime(from_time as string);
|
||||
try {
|
||||
const location = (auth as any).props?.['location'] || 'zoho.com';
|
||||
const appointments = await zohoBookingsCommon.fetchAppointments(
|
||||
(auth as any).access_token,
|
||||
location,
|
||||
{
|
||||
perPage: 50,
|
||||
from_time: formattedFromTime,
|
||||
to_time: to_time ? formatDateTime(to_time as string) : undefined,
|
||||
}
|
||||
);
|
||||
return {
|
||||
options: appointments.map((appointment: any) => ({
|
||||
label: `${appointment.booking_id} - ${appointment.customer_name} (${appointment.service_name}) - ${appointment.start_time} [${appointment.status}]`,
|
||||
value: appointment.booking_id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to load appointments',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const bookAppointment = {
|
||||
workspace_id: z.string().min(1),
|
||||
service_id: z.string().min(1),
|
||||
staff_id: z.string().optional(),
|
||||
resource_id: z.string().optional(),
|
||||
group_id: z.string().optional(),
|
||||
from_time: z.string(),
|
||||
to_time: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
customer_name: z.string().min(1),
|
||||
customer_email: z.string().email(),
|
||||
customer_phone: z.string().min(1),
|
||||
notes: z.string().optional(),
|
||||
additional_fields: z.record(z.string(), z.unknown()).optional(),
|
||||
cost_paid: z.number().min(0).optional(),
|
||||
};
|
||||
|
||||
export const rescheduleAppointment = {
|
||||
booking_id: z.string().min(1),
|
||||
service_id: z.string().optional(),
|
||||
staff_id: z.string().optional(),
|
||||
group_id: z.string().optional(),
|
||||
start_time: z.string().optional(),
|
||||
};
|
||||
|
||||
export const fetchAvailability = {
|
||||
workspace_id: z.string().min(1),
|
||||
service_id: z.string().min(1),
|
||||
staff_id: z.string().optional(),
|
||||
group_id: z.string().optional(),
|
||||
resource_id: z.string().optional(),
|
||||
selected_date: z.string(),
|
||||
};
|
||||
|
||||
export const getAppointmentDetails = {
|
||||
booking_id: z.string().min(1),
|
||||
};
|
||||
|
||||
export const cancelAppointment = {
|
||||
booking_id: z.string().min(1),
|
||||
action: z.enum(['cancel', 'completed', 'noshow']),
|
||||
};
|
||||
Reference in New Issue
Block a user