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,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