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,40 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdsMultiSelectDropdown, extractApiKey } from '../common/props';
export const bulkDeleteLeads = createAction({
name: 'bulkDeleteLeads',
displayName: 'Bulk Delete Leads',
description: 'Delete multiple leads (max 500 per request)',
auth: bookedinAuth,
props: {
lead_ids: leadIdsMultiSelectDropdown,
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const leadIds = Array.isArray(propsValue.lead_ids) ? propsValue.lead_ids : [propsValue.lead_ids];
if (leadIds.length === 0) {
throw new Error('At least one lead must be selected');
}
if (leadIds.length > 500) {
throw new Error('Maximum 500 leads can be deleted per request');
}
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/leads/bulk-delete`,
headers: {
...getBookedinHeaders(apiKey),
'Content-Type': 'application/json',
},
body: {
lead_ids: leadIds,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,55 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, extractApiKey } from '../common/props';
export const createLead = createAction({
name: 'createLead',
displayName: 'Create Lead',
description: 'Creates a new lead in Bookedin AI',
auth: bookedinAuth,
props: {
firstName: Property.ShortText({
displayName: 'First Name',
required: true,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
required: true,
}),
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
phone: Property.ShortText({
displayName: 'Phone Number',
required: true,
}),
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const payload = {
contact: {
name: {
last: propsValue.lastName,
first: propsValue.firstName,
},
email: propsValue.email,
number: propsValue.phone,
},
};
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/leads/`,
headers: {
...getBookedinHeaders(apiKey),
'Content-Type': 'application/json',
},
body: payload,
});
return response.body;
},
});

View File

@@ -0,0 +1,25 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdDropdown, extractApiKey } from '../common/props';
export const deleteLead = createAction({
name: 'deleteLead',
displayName: 'Delete Lead',
description: 'Delete a lead.',
auth: bookedinAuth,
props: {
lead_id: leadIdDropdown,
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${BASE_URL}/leads/${propsValue.lead_id}`,
headers: getBookedinHeaders(apiKey),
});
return response.body;
},
});

View File

@@ -0,0 +1,23 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, extractApiKey } from '../common/props';
export const getLeadStats = createAction({
name: 'getLeadStats',
displayName: 'Get Lead Stats',
description: 'Get lead statistics (Hot, Warm, Cold, Objectives Met, Total).',
auth: bookedinAuth,
props: {},
async run({ auth }) {
const apiKey = extractApiKey(auth);
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/stats`,
headers: getBookedinHeaders(apiKey),
});
return response.body;
},
});

View File

@@ -0,0 +1,25 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdDropdown, extractApiKey } from '../common/props';
export const getLead = createAction({
name: 'getLead',
displayName: 'Get Lead',
description: 'Get a specific lead by ID.',
auth: bookedinAuth,
props: {
lead_id: leadIdDropdown,
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/${propsValue.lead_id}`,
headers: getBookedinHeaders(apiKey),
});
return response.body;
},
});

View File

@@ -0,0 +1,67 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, extractApiKey } from '../common/props';
export const getLeads = createAction({
name: 'getLeads',
displayName: 'Get Leads',
description: 'Get all leads for the current business with pagination metadata.',
auth: bookedinAuth,
props: {
search: Property.ShortText({
displayName: 'Search',
description: 'Search text in name, email, or phone number',
required: false,
}),
source: Property.ShortText({
displayName: 'Source',
description: 'Filter by lead source (e.g., "API", "Import")',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Filter by exact email address',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone',
description: 'Filter by phone number',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Number of leads to return',
required: false,
defaultValue: 100,
}),
skip: Property.Number({
displayName: 'Skip',
description: 'Number of leads to skip (pagination)',
required: false,
defaultValue: 0,
}),
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const queryParams: Record<string, string> = {
limit: (propsValue.limit ?? 100).toString(),
skip: (propsValue.skip ?? 0).toString(),
};
if (propsValue.search) queryParams['search'] = propsValue.search;
if (propsValue.source) queryParams['source'] = propsValue.source;
if (propsValue.email) queryParams['email'] = propsValue.email;
if (propsValue.phone) queryParams['phone'] = propsValue.phone;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/`,
headers: getBookedinHeaders(apiKey),
queryParams,
});
return response.body;
},
});

View File

@@ -0,0 +1,90 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdDropdown, extractApiKey } from '../common/props';
import { isNil } from '@activepieces/shared';
export const updateLead = createAction({
name: 'updateLead',
displayName: 'Update Lead',
description: 'Update a lead.',
auth: bookedinAuth,
props: {
lead_id: leadIdDropdown,
firstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone Number',
required: false,
}),
handling_status: Property.ShortText({
displayName: 'Handling Status',
required: false,
}),
update_json: Property.Json({
displayName: 'Update Payload (JSON)',
description: 'Optional JSON body for complex updates. Merges with individual fields above.',
required: false,
defaultValue: {},
}),
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const basePayload: Record<string, unknown> = {};
if (!isNil(propsValue.firstName) || !isNil(propsValue.lastName) || !isNil(propsValue.email) || !isNil(propsValue.phone)) {
const contact: Record<string, unknown> = {};
const nameParts: Record<string, string> = {};
if (!isNil(propsValue.firstName) && propsValue.firstName !== '') {
nameParts['first'] = propsValue.firstName;
}
if (!isNil(propsValue.lastName) && propsValue.lastName !== '') {
nameParts['last'] = propsValue.lastName;
}
if (Object.keys(nameParts).length > 0) {
contact['name'] = nameParts;
}
if (!isNil(propsValue.email) && propsValue.email !== '') {
contact['email'] = propsValue.email;
}
if (!isNil(propsValue.phone) && propsValue.phone !== '') {
contact['number'] = propsValue.phone;
}
basePayload['contact'] = contact;
}
if (!isNil(propsValue.handling_status) && propsValue.handling_status !== '') {
basePayload['handling_status'] = propsValue.handling_status;
}
const finalPayload = {
...basePayload,
...(propsValue.update_json || {}),
};
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: `${BASE_URL}/leads/${propsValue.lead_id}`,
headers: {
...getBookedinHeaders(apiKey),
'Content-Type': 'application/json',
},
body: finalPayload,
});
return response.body;
},
});

View File

@@ -0,0 +1,138 @@
import { Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
export const BASE_URL = 'https://api.bookedin.ai/api/v1';
export const getBookedinHeaders = (apiKey: string) => {
return {
'X-API-Key': apiKey,
'accept': 'application/json',
};
};
export const extractApiKey = (auth: unknown): string => {
if (typeof auth === 'string') {
return auth;
}
const authObj = auth as { secret_text?: string; auth?: string };
return authObj?.secret_text || authObj?.auth || '';
};
const fetchLeadOptions = async (apiKey: string) => {
const response = await httpClient.sendRequest<{
items: Array<{
id: string;
contact: {
name: {
first: string;
last: string;
};
email: string;
};
}>;
}>({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/`,
headers: getBookedinHeaders(apiKey),
queryParams: {
limit: '100',
skip: '0',
},
});
return response.body.items.map((lead) => {
const firstName = lead.contact?.name?.first || '';
const lastName = lead.contact?.name?.last || '';
const email = lead.contact?.email || '';
const name = [firstName, lastName].filter(Boolean).join(' ').trim();
const label = name ? `${name} (${email})` : email || lead.id;
return {
label,
value: lead.id,
};
});
};
export const leadIdDropdown = Property.Dropdown({
auth: bookedinAuth,
displayName: 'Lead',
description: 'Select a lead',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
try {
const apiKey = extractApiKey(auth);
if (!apiKey) {
return {
disabled: true,
placeholder: 'API key is missing',
options: [],
};
}
const options = await fetchLeadOptions(apiKey);
return {
disabled: false,
options,
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load leads. Please check your connection.',
options: [],
};
}
},
});
export const leadIdsMultiSelectDropdown = Property.MultiSelectDropdown({
auth: bookedinAuth,
displayName: 'Leads',
description: 'Select leads to delete (max 500)',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
try {
const apiKey = extractApiKey(auth);
if (!apiKey) {
return {
disabled: true,
placeholder: 'API key is missing',
options: [],
};
}
const options = await fetchLeadOptions(apiKey);
return {
disabled: false,
options,
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load leads. Please check your connection.',
options: [],
};
}
},
});