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,115 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchHouseholds, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const addHouseholdMember = createAction({
|
||||
name: 'add_household_member',
|
||||
displayName: 'Add Member to Household',
|
||||
description: 'Adds a member to an existing household. Link multiple contacts under one family unit.',
|
||||
auth: wealthboxAuth,
|
||||
props: {
|
||||
household_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Household',
|
||||
description: 'Select the household that will receive the new member',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const households = await fetchHouseholds(auth.secret_text);
|
||||
return {
|
||||
options: households.map((household: any) => ({
|
||||
label: household.first_name || `Household ${household.id}`,
|
||||
value: household.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load households. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
contact_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Contact',
|
||||
description: 'Select the contact to add to the household',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load contacts. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
title: Property.StaticDropdown({
|
||||
displayName: 'Household Title',
|
||||
description: 'The household title to assign to the added contact',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Head', value: 'Head' },
|
||||
{ label: 'Spouse', value: 'Spouse' },
|
||||
{ label: 'Partner', value: 'Partner' },
|
||||
{ label: 'Child', value: 'Child' },
|
||||
{ label: 'Grandchild', value: 'Grandchild' },
|
||||
{ label: 'Parent', value: 'Parent' },
|
||||
{ label: 'Grandparent', value: 'Grandparent' },
|
||||
{ label: 'Sibling', value: 'Sibling' },
|
||||
{ label: 'Other', value: 'Other' },
|
||||
{ label: 'Dependent', value: 'Dependent' }
|
||||
]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
id: propsValue.contact_id,
|
||||
title: propsValue.title
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/households/${propsValue.household_id}/members`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('add household member', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add member to household: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,319 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createContact = createAction({
|
||||
name: 'create_contact',
|
||||
displayName: 'Create Contact',
|
||||
description: 'Adds a new contact with rich details (name, address, email, tags, etc.)',
|
||||
auth: wealthboxAuth,
|
||||
props: {
|
||||
first_name: Property.ShortText({
|
||||
displayName: 'First Name',
|
||||
description: 'The first name of the contact',
|
||||
required: true
|
||||
}),
|
||||
last_name: Property.ShortText({
|
||||
displayName: 'Last Name',
|
||||
description: 'The last name of the contact',
|
||||
required: true
|
||||
}),
|
||||
|
||||
prefix: Property.ShortText({
|
||||
displayName: 'Prefix',
|
||||
description: 'The preferred prefix for the contact (e.g., Mr., Ms., Dr.)',
|
||||
required: false
|
||||
}),
|
||||
middle_name: Property.ShortText({
|
||||
displayName: 'Middle Name',
|
||||
description: 'The middle name of the contact',
|
||||
required: false
|
||||
}),
|
||||
suffix: Property.ShortText({
|
||||
displayName: 'Suffix',
|
||||
description: 'The suffix associated with the contact (e.g., Jr., Sr., M.D.)',
|
||||
required: false
|
||||
}),
|
||||
nickname: Property.ShortText({
|
||||
displayName: 'Nickname',
|
||||
description: 'A preferred shortname for the contact',
|
||||
required: false
|
||||
}),
|
||||
|
||||
job_title: Property.ShortText({
|
||||
displayName: 'Job Title',
|
||||
description: 'The title the contact holds at their present company',
|
||||
required: false
|
||||
}),
|
||||
company_name: Property.ShortText({
|
||||
displayName: 'Company Name',
|
||||
description: 'The name of the contact\'s present company',
|
||||
required: false
|
||||
}),
|
||||
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Contact Type',
|
||||
description: 'The type of the contact being created',
|
||||
required: false,
|
||||
defaultValue: 'Person',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Person', value: 'Person' },
|
||||
{ label: 'Household', value: 'Household' },
|
||||
{ label: 'Organization', value: 'Organization' },
|
||||
{ label: 'Trust', value: 'Trust' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
contact_type: Property.StaticDropdown({
|
||||
displayName: 'Contact Classification',
|
||||
description: 'A string further classifying the contact',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Client', value: 'Client' },
|
||||
{ label: 'Past Client', value: 'Past Client' },
|
||||
{ label: 'Prospect', value: 'Prospect' },
|
||||
{ label: 'Vendor', value: 'Vendor' },
|
||||
{ label: 'Organization', value: 'Organization' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
status: Property.StaticDropdown({
|
||||
displayName: 'Status',
|
||||
description: 'Whether the contact is currently active',
|
||||
required: false,
|
||||
defaultValue: 'Active',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Inactive', value: 'Inactive' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
gender: Property.StaticDropdown({
|
||||
displayName: 'Gender',
|
||||
description: 'The gender of the contact',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Female', value: 'Female' },
|
||||
{ label: 'Male', value: 'Male' },
|
||||
{ label: 'Non-binary', value: 'Non-binary' },
|
||||
{ label: 'Unknown', value: 'Unknown' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
birth_date: Property.DateTime({
|
||||
displayName: 'Birth Date',
|
||||
description: 'The birthdate of the contact (YYYY-MM-DD format)',
|
||||
required: false
|
||||
}),
|
||||
marital_status: Property.StaticDropdown({
|
||||
displayName: 'Marital Status',
|
||||
description: 'The marital status of the contact',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Married', value: 'Married' },
|
||||
{ label: 'Single', value: 'Single' },
|
||||
{ label: 'Divorced', value: 'Divorced' },
|
||||
{ label: 'Widowed', value: 'Widowed' },
|
||||
{ label: 'Life Partner', value: 'Life Partner' },
|
||||
{ label: 'Separated', value: 'Separated' },
|
||||
{ label: 'Unknown', value: 'Unknown' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
email_address: Property.ShortText({
|
||||
displayName: 'Email Address',
|
||||
description: 'Primary email address for the contact',
|
||||
required: false
|
||||
}),
|
||||
phone_number: Property.ShortText({
|
||||
displayName: 'Phone Number',
|
||||
description: 'Primary phone number for the contact (e.g., (555) 123-4567)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
street_line_1: Property.ShortText({
|
||||
displayName: 'Street Address Line 1',
|
||||
description: 'First line of street address',
|
||||
required: false
|
||||
}),
|
||||
street_line_2: Property.ShortText({
|
||||
displayName: 'Street Address Line 2',
|
||||
description: 'Second line of street address (apt, suite, etc.)',
|
||||
required: false
|
||||
}),
|
||||
city: Property.ShortText({
|
||||
displayName: 'City',
|
||||
description: 'City for the address',
|
||||
required: false
|
||||
}),
|
||||
state: Property.ShortText({
|
||||
displayName: 'State',
|
||||
description: 'State or province for the address',
|
||||
required: false
|
||||
}),
|
||||
zip_code: Property.ShortText({
|
||||
displayName: 'ZIP Code',
|
||||
description: 'ZIP or postal code for the address',
|
||||
required: false
|
||||
}),
|
||||
country: Property.ShortText({
|
||||
displayName: 'Country',
|
||||
description: 'Country for the address',
|
||||
required: false,
|
||||
defaultValue: 'United States'
|
||||
}),
|
||||
|
||||
twitter_name: Property.ShortText({
|
||||
displayName: 'Twitter Handle',
|
||||
description: 'The twitter handle of the contact',
|
||||
required: false
|
||||
}),
|
||||
linkedin_url: Property.LongText({
|
||||
displayName: 'LinkedIn URL',
|
||||
description: 'The LinkedIn URL for the contact',
|
||||
required: false
|
||||
}),
|
||||
|
||||
background_information: Property.LongText({
|
||||
displayName: 'Background Information',
|
||||
description: 'A brief description of the contact',
|
||||
required: false
|
||||
}),
|
||||
important_information: Property.LongText({
|
||||
displayName: 'Important Information',
|
||||
description: 'Any other important info for the contact',
|
||||
required: false
|
||||
}),
|
||||
personal_interests: Property.LongText({
|
||||
displayName: 'Personal Interests',
|
||||
description: 'Personal interests for the contact',
|
||||
required: false
|
||||
}),
|
||||
|
||||
contact_source: Property.StaticDropdown({
|
||||
displayName: 'Contact Source',
|
||||
description: 'The method in which this contact was acquired',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Referral', value: 'Referral' },
|
||||
{ label: 'Conference', value: 'Conference' },
|
||||
{ label: 'Direct Mail', value: 'Direct Mail' },
|
||||
{ label: 'Cold Call', value: 'Cold Call' },
|
||||
{ label: 'Other', value: 'Other' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
tags: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Tags to associate with the contact (e.g., "Client", "VIP", "Referral")',
|
||||
required: false
|
||||
}),
|
||||
|
||||
external_unique_id: Property.ShortText({
|
||||
displayName: 'External Unique ID',
|
||||
description: 'A unique identifier for this contact in an external system',
|
||||
required: false
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const requestBody: any = {};
|
||||
|
||||
if (propsValue.type === 'Household') {
|
||||
if (propsValue.first_name) {
|
||||
requestBody.name = propsValue.first_name;
|
||||
}
|
||||
} else {
|
||||
if (propsValue.first_name) requestBody.first_name = propsValue.first_name;
|
||||
if (propsValue.last_name) requestBody.last_name = propsValue.last_name;
|
||||
}
|
||||
|
||||
if (propsValue.prefix) requestBody.prefix = propsValue.prefix;
|
||||
if (propsValue.middle_name) requestBody.middle_name = propsValue.middle_name;
|
||||
if (propsValue.suffix) requestBody.suffix = propsValue.suffix;
|
||||
if (propsValue.nickname) requestBody.nickname = propsValue.nickname;
|
||||
if (propsValue.job_title) requestBody.job_title = propsValue.job_title;
|
||||
if (propsValue.company_name) requestBody.company_name = propsValue.company_name;
|
||||
if (propsValue.type) requestBody.type = propsValue.type;
|
||||
if (propsValue.contact_type) requestBody.contact_type = propsValue.contact_type;
|
||||
if (propsValue.status) requestBody.status = propsValue.status;
|
||||
if (propsValue.gender) requestBody.gender = propsValue.gender;
|
||||
if (propsValue.birth_date) requestBody.birth_date = propsValue.birth_date;
|
||||
if (propsValue.marital_status) requestBody.marital_status = propsValue.marital_status;
|
||||
if (propsValue.twitter_name) requestBody.twitter_name = propsValue.twitter_name;
|
||||
if (propsValue.linkedin_url) requestBody.linkedin_url = propsValue.linkedin_url;
|
||||
if (propsValue.background_information) requestBody.background_information = propsValue.background_information;
|
||||
if (propsValue.important_information) requestBody.important_information = propsValue.important_information;
|
||||
if (propsValue.personal_interests) requestBody.personal_interests = propsValue.personal_interests;
|
||||
if (propsValue.contact_source) requestBody.contact_source = propsValue.contact_source;
|
||||
if (propsValue.external_unique_id) requestBody.external_unique_id = propsValue.external_unique_id;
|
||||
|
||||
if (propsValue.email_address) {
|
||||
requestBody.email_addresses = [{
|
||||
address: propsValue.email_address,
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}];
|
||||
}
|
||||
|
||||
if (propsValue.phone_number) {
|
||||
requestBody.phone_numbers = [{
|
||||
address: propsValue.phone_number,
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}];
|
||||
}
|
||||
|
||||
if (propsValue.street_line_1 || propsValue.city || propsValue.state || propsValue.zip_code) {
|
||||
requestBody.street_addresses = [{
|
||||
street_line_1: propsValue.street_line_1 || '',
|
||||
street_line_2: propsValue.street_line_2 || '',
|
||||
city: propsValue.city || '',
|
||||
state: propsValue.state || '',
|
||||
zip_code: propsValue.zip_code || '',
|
||||
country: propsValue.country || 'United States',
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}];
|
||||
}
|
||||
|
||||
if (propsValue.tags && Array.isArray(propsValue.tags)) {
|
||||
requestBody.tags = propsValue.tags;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: 'https://api.crmworkspace.com/v1/contacts',
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Wealthbox API error: ${response.status} - ${JSON.stringify(response.body)}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create contact: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,454 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchUsers, fetchUserGroups, fetchEventCategories, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES, EVENT_STATES } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createEvent = createAction({
|
||||
name: 'create_event',
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Create Event',
|
||||
description: 'Creates a calendar event linked to contact. Schedule advisory meetings on behalf of clients.',
|
||||
props: {
|
||||
title: Property.ShortText({
|
||||
displayName: 'Event Title',
|
||||
description: 'The name of the event (e.g., "Client Meeting", "Portfolio Review")',
|
||||
required: true
|
||||
}),
|
||||
starts_at: Property.DateTime({
|
||||
displayName: 'Start Date & Time',
|
||||
description: 'When the event starts (yyyy-mm-dd hh:mm format)',
|
||||
required: true
|
||||
}),
|
||||
ends_at: Property.DateTime({
|
||||
displayName: 'End Date & Time',
|
||||
description: 'When the event ends (yyyy-mm-dd hh:mm format)',
|
||||
required: true
|
||||
}),
|
||||
|
||||
location: Property.ShortText({
|
||||
displayName: 'Location',
|
||||
description: 'Where the event takes place (e.g., "Conference Room", "Client Office", "Zoom Meeting")',
|
||||
required: false
|
||||
}),
|
||||
description: Property.LongText({
|
||||
displayName: 'Description',
|
||||
description: 'A detailed explanation of the event purpose and agenda',
|
||||
required: false
|
||||
}),
|
||||
|
||||
all_day: Property.Checkbox({
|
||||
displayName: 'All Day Event',
|
||||
description: 'Check if this is an all-day event',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
}),
|
||||
repeats: Property.Checkbox({
|
||||
displayName: 'Repeating Event',
|
||||
description: 'Check if this event repeats',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
}),
|
||||
|
||||
state: Property.StaticDropdown({
|
||||
displayName: 'Event Status',
|
||||
description: 'The current state of the event',
|
||||
required: false,
|
||||
defaultValue: EVENT_STATES.UNCONFIRMED,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Unconfirmed', value: EVENT_STATES.UNCONFIRMED },
|
||||
{ label: 'Confirmed', value: EVENT_STATES.CONFIRMED },
|
||||
{ label: 'Tentative', value: EVENT_STATES.TENTATIVE },
|
||||
{ label: 'Completed', value: EVENT_STATES.COMPLETED },
|
||||
{ label: 'Cancelled', value: EVENT_STATES.CANCELLED }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
contact_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Linked Contact',
|
||||
description: 'Select the contact to link this event to',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load contacts. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
invitees: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Invitees',
|
||||
description: 'Add people to invite to this event',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
invitees_array: Property.Array({
|
||||
displayName: 'Invitees',
|
||||
description: 'Add invitees to this event',
|
||||
required: false,
|
||||
properties: {
|
||||
invitee: Property.ShortText({
|
||||
displayName: 'Invitee',
|
||||
description: 'Invitee name',
|
||||
required: true
|
||||
}),
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Type',
|
||||
description: 'Type of invitee',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Contact', value: 'Contact' },
|
||||
{ label: 'User', value: 'User' }
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [contacts, users] = await Promise.all([
|
||||
fetchContacts(auth.secret_text, { active: true }),
|
||||
fetchUsers(auth.secret_text)
|
||||
]);
|
||||
|
||||
const contactOptions = contacts.map((contact: any) => ({
|
||||
label: `${contact.name || `${contact.first_name} ${contact.last_name}`.trim()} (Contact)`,
|
||||
value: `contact_${contact.id}`
|
||||
}));
|
||||
|
||||
const userOptions = users.map((user: any) => ({
|
||||
label: `${user.name} (User)`,
|
||||
value: `user_${user.id}`
|
||||
}));
|
||||
|
||||
const allInviteeOptions = [...contactOptions, ...userOptions];
|
||||
|
||||
return {
|
||||
invitees_array: Property.Array({
|
||||
displayName: 'Invitees',
|
||||
description: 'Add invitees to this event',
|
||||
required: false,
|
||||
properties: {
|
||||
invitee: Property.StaticDropdown({
|
||||
displayName: 'Invitee',
|
||||
description: 'Select a contact or user to invite',
|
||||
required: true,
|
||||
options: {
|
||||
options: allInviteeOptions
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
invitees_array: Property.Array({
|
||||
displayName: 'Invitees',
|
||||
description: 'Add invitees to this event (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
invitee: Property.ShortText({
|
||||
displayName: 'Invitee Name',
|
||||
description: 'Enter the invitee name',
|
||||
required: true
|
||||
}),
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Type',
|
||||
description: 'Type of invitee',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Contact', value: 'Contact' },
|
||||
{ label: 'User', value: 'User' }
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
event_category: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Event Category',
|
||||
description: 'Select the category for this event',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const categories = await fetchEventCategories(auth.secret_text);
|
||||
return {
|
||||
options: categories.map((category: any) => ({
|
||||
label: category.name,
|
||||
value: category.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load event categories. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
email_invitees: Property.Checkbox({
|
||||
displayName: 'Email Invitees',
|
||||
description: 'Send email invitations to invitees',
|
||||
required: false,
|
||||
defaultValue: true
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this event',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
return {
|
||||
options: userGroups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.name
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
custom_fields: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this event',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this event',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Custom field name',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.EVENT);
|
||||
const customFieldOptions = customFields.map((field: any) => ({
|
||||
label: field.name,
|
||||
value: field.name
|
||||
}));
|
||||
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this event',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.StaticDropdown({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Select a custom field for this event',
|
||||
required: true,
|
||||
options: {
|
||||
options: customFieldOptions
|
||||
}
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this event (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field Name',
|
||||
description: 'Enter the custom field name exactly',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
title: propsValue.title,
|
||||
starts_at: propsValue.starts_at,
|
||||
ends_at: propsValue.ends_at
|
||||
};
|
||||
|
||||
if (propsValue.location) requestBody.location = propsValue.location;
|
||||
if (propsValue.description) requestBody.description = propsValue.description;
|
||||
if (propsValue.all_day !== undefined) requestBody.all_day = propsValue.all_day;
|
||||
if (propsValue.repeats !== undefined) requestBody.repeats = propsValue.repeats;
|
||||
if (propsValue.state) requestBody.state = propsValue.state;
|
||||
if (propsValue.event_category) requestBody.event_category = propsValue.event_category;
|
||||
if (propsValue.visible_to) requestBody.visible_to = propsValue.visible_to;
|
||||
if (propsValue.email_invitees !== undefined) requestBody.email_invitees = propsValue.email_invitees;
|
||||
|
||||
if (propsValue.contact_id) {
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true });
|
||||
const selectedContact = contacts.find((contact: any) => contact.id === propsValue.contact_id);
|
||||
|
||||
requestBody.linked_to = [{
|
||||
id: propsValue.contact_id,
|
||||
type: 'Contact',
|
||||
name: selectedContact ? (selectedContact.name || `${selectedContact.first_name} ${selectedContact.last_name}`.trim()) : `Contact ${propsValue.contact_id}`
|
||||
}];
|
||||
} catch (error) {
|
||||
requestBody.linked_to = [{
|
||||
id: propsValue.contact_id,
|
||||
type: 'Contact',
|
||||
name: `Contact ${propsValue.contact_id}`
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
const inviteesArray = (propsValue as any).invitees_array;
|
||||
if (inviteesArray && Array.isArray(inviteesArray) && inviteesArray.length > 0) {
|
||||
const invitees: any[] = [];
|
||||
|
||||
for (const inviteeItem of inviteesArray) {
|
||||
const inviteeValue = inviteeItem.invitee;
|
||||
|
||||
if (inviteeValue && typeof inviteeValue === 'string') {
|
||||
if (inviteeValue.startsWith('contact_')) {
|
||||
const contactId = inviteeValue.replace('contact_', '');
|
||||
invitees.push({
|
||||
id: parseInt(contactId),
|
||||
type: 'Contact'
|
||||
});
|
||||
} else if (inviteeValue.startsWith('user_')) {
|
||||
const userId = inviteeValue.replace('user_', '');
|
||||
invitees.push({
|
||||
id: parseInt(userId),
|
||||
type: 'User'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invitees.length > 0) {
|
||||
requestBody.invitees = invitees;
|
||||
}
|
||||
}
|
||||
|
||||
const customFieldsArray = (propsValue as any).custom_fields_array;
|
||||
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.EVENT);
|
||||
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
|
||||
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => {
|
||||
const fieldId = customFieldMap.get(field.custom_field);
|
||||
if (!fieldId) {
|
||||
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
|
||||
}
|
||||
return {
|
||||
id: fieldId,
|
||||
value: field.value
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Custom field')) {
|
||||
throw error;
|
||||
}
|
||||
console.warn('Could not fetch custom fields for validation:', error);
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
|
||||
id: field.custom_field,
|
||||
value: field.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/events`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('create event', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create event: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,391 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchUserGroups, fetchTags, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createHousehold = createAction({
|
||||
name: 'create_household',
|
||||
displayName: 'Create Household',
|
||||
description: 'Creates a household record with emails, tags. Group family member contacts into one household.',
|
||||
auth: wealthboxAuth,
|
||||
props: {
|
||||
name: Property.ShortText({
|
||||
displayName: 'Household Name',
|
||||
description: 'The name of the household (e.g., "The Anderson Family", "Smith Household")',
|
||||
required: true
|
||||
}),
|
||||
|
||||
head_contact_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Head of Household',
|
||||
description: 'Select the contact who will be the head of this household',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load contacts. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
spouse_contact_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Spouse/Partner (Optional)',
|
||||
description: 'Select the spouse or partner to automatically add to this household',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load contacts. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
email_address: Property.ShortText({
|
||||
displayName: 'Primary Email Address',
|
||||
description: 'Primary email address for the household',
|
||||
required: false
|
||||
}),
|
||||
|
||||
street_line_1: Property.ShortText({
|
||||
displayName: 'Street Address Line 1',
|
||||
description: 'First line of street address',
|
||||
required: false
|
||||
}),
|
||||
street_line_2: Property.ShortText({
|
||||
displayName: 'Street Address Line 2',
|
||||
description: 'Second line of street address (apt, suite, etc.)',
|
||||
required: false
|
||||
}),
|
||||
city: Property.ShortText({
|
||||
displayName: 'City',
|
||||
description: 'City for the household address',
|
||||
required: false
|
||||
}),
|
||||
state: Property.ShortText({
|
||||
displayName: 'State',
|
||||
description: 'State or province for the household address',
|
||||
required: false
|
||||
}),
|
||||
zip_code: Property.ShortText({
|
||||
displayName: 'ZIP Code',
|
||||
description: 'ZIP or postal code for the household address',
|
||||
required: false
|
||||
}),
|
||||
country: Property.ShortText({
|
||||
displayName: 'Country',
|
||||
description: 'Country for the household address',
|
||||
required: false,
|
||||
defaultValue: 'United States'
|
||||
}),
|
||||
|
||||
phone_number: Property.ShortText({
|
||||
displayName: 'Primary Phone Number',
|
||||
description: 'Primary phone number for the household',
|
||||
required: false
|
||||
}),
|
||||
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Household Type',
|
||||
description: 'The type of household being created',
|
||||
required: false,
|
||||
defaultValue: 'Household',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Household', value: 'Household' },
|
||||
{ label: 'Organization', value: 'Organization' },
|
||||
{ label: 'Trust', value: 'Trust' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
status: Property.StaticDropdown({
|
||||
displayName: 'Status',
|
||||
description: 'Whether the household is currently active',
|
||||
required: false,
|
||||
defaultValue: 'Active',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Inactive', value: 'Inactive' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
background_information: Property.LongText({
|
||||
displayName: 'Background Information',
|
||||
description: 'Background information about the household',
|
||||
required: false
|
||||
}),
|
||||
important_information: Property.LongText({
|
||||
displayName: 'Important Information',
|
||||
description: 'Any important information about the household',
|
||||
required: false
|
||||
}),
|
||||
|
||||
tags: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Tags',
|
||||
description: 'Select tags to associate with this household',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
tags_array: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Add tags to this household',
|
||||
required: false,
|
||||
properties: {
|
||||
tag: Property.ShortText({
|
||||
displayName: 'Tag',
|
||||
description: 'Tag name',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = await fetchTags(auth.secret_text, DOCUMENT_TYPES.CONTACT);
|
||||
const tagOptions = tags.map((tag: any) => ({
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}));
|
||||
|
||||
return {
|
||||
tags_array: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Add tags to this household',
|
||||
required: false,
|
||||
properties: {
|
||||
tag: Property.StaticDropdown({
|
||||
displayName: 'Tag',
|
||||
description: 'Select a tag for this household',
|
||||
required: true,
|
||||
options: {
|
||||
options: tagOptions
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
tags_array: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Add tags to this household (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
tag: Property.ShortText({
|
||||
displayName: 'Tag Name',
|
||||
description: 'Enter the tag name exactly',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this household',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
|
||||
const filteredGroups = userGroups.filter((group: any) => group.name !== 'Only Me');
|
||||
|
||||
return {
|
||||
options: filteredGroups.map((group: any) => {
|
||||
const displayName = group.user ? `${group.name} (${group.user.name || group.user.email})` : group.name;
|
||||
return {
|
||||
label: displayName,
|
||||
value: group.name
|
||||
};
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
external_unique_id: Property.ShortText({
|
||||
displayName: 'External Unique ID',
|
||||
description: 'A unique identifier for this household in an external system',
|
||||
required: false
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
name: propsValue.name,
|
||||
type: 'Household',
|
||||
status: propsValue.status || 'Active'
|
||||
};
|
||||
|
||||
if (propsValue.background_information) requestBody.background_information = propsValue.background_information;
|
||||
if (propsValue.important_information) requestBody.important_information = propsValue.important_information;
|
||||
|
||||
if (propsValue.visible_to && propsValue.visible_to.trim() !== '') {
|
||||
requestBody.visible_to = propsValue.visible_to;
|
||||
}
|
||||
|
||||
if (propsValue.external_unique_id) requestBody.external_unique_id = propsValue.external_unique_id;
|
||||
|
||||
if (propsValue.email_address) {
|
||||
requestBody.email_addresses = [{
|
||||
address: propsValue.email_address,
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}];
|
||||
}
|
||||
|
||||
if (propsValue.phone_number) {
|
||||
requestBody.phone_numbers = [{
|
||||
address: propsValue.phone_number,
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}];
|
||||
}
|
||||
|
||||
if (propsValue.street_line_1 || propsValue.city || propsValue.state || propsValue.zip_code) {
|
||||
requestBody.street_addresses = [{
|
||||
street_line_1: propsValue.street_line_1 || '',
|
||||
street_line_2: propsValue.street_line_2 || '',
|
||||
city: propsValue.city || '',
|
||||
state: propsValue.state || '',
|
||||
zip_code: propsValue.zip_code || '',
|
||||
country: propsValue.country || 'United States',
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}];
|
||||
}
|
||||
|
||||
const tagsArray = (propsValue as any).tags_array;
|
||||
if (tagsArray && Array.isArray(tagsArray) && tagsArray.length > 0) {
|
||||
requestBody.tags = tagsArray.map((tagItem: any) => tagItem.tag);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/contacts`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('create household', response.status, response.body);
|
||||
}
|
||||
|
||||
const householdContact = response.body;
|
||||
|
||||
const members: any[] = [];
|
||||
|
||||
if (propsValue.head_contact_id && householdContact.id) {
|
||||
try {
|
||||
const memberResponse = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/households/${householdContact.id}/members`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
id: propsValue.head_contact_id,
|
||||
title: 'Head'
|
||||
}
|
||||
});
|
||||
|
||||
if (memberResponse.status < 400) {
|
||||
members.push(memberResponse.body);
|
||||
}
|
||||
} catch (memberError) {
|
||||
console.warn('Failed to add head of household member:', memberError);
|
||||
}
|
||||
}
|
||||
|
||||
if (propsValue.spouse_contact_id && householdContact.id) {
|
||||
try {
|
||||
const spouseResponse = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/households/${householdContact.id}/members`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
id: propsValue.spouse_contact_id,
|
||||
title: 'Spouse'
|
||||
}
|
||||
});
|
||||
|
||||
if (spouseResponse.status < 400) {
|
||||
members.push(spouseResponse.body);
|
||||
}
|
||||
} catch (spouseError) {
|
||||
console.warn('Failed to add spouse/partner member:', spouseError);
|
||||
}
|
||||
}
|
||||
|
||||
if (members.length > 0) {
|
||||
return {
|
||||
household: householdContact,
|
||||
members: members
|
||||
};
|
||||
}
|
||||
|
||||
return householdContact;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create household: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchUserGroups, fetchContacts, fetchTags, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createNote = createAction({
|
||||
name: 'create_note',
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Create Note',
|
||||
description: 'Adds a note linked to a contact. Log call summaries against client records.',
|
||||
props: {
|
||||
content: Property.LongText({
|
||||
displayName: 'Note Content',
|
||||
description: 'The main body of the note (e.g., call summary, meeting notes, client interaction details)',
|
||||
required: true
|
||||
}),
|
||||
|
||||
contact_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Contact',
|
||||
description: 'Select the contact to link this note to',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load contacts. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this note',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
return {
|
||||
options: userGroups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.name
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
tags: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Tags',
|
||||
description: 'Select tags to associate with this note',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
tags_array: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Add tags to this note',
|
||||
required: false,
|
||||
properties: {
|
||||
tag: Property.ShortText({
|
||||
displayName: 'Tag',
|
||||
description: 'Tag name',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = await fetchTags(auth.secret_text, DOCUMENT_TYPES.CONTACT_NOTE);
|
||||
const tagOptions = tags.map((tag: any) => ({
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}));
|
||||
|
||||
return {
|
||||
tags_array: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Add tags to this note',
|
||||
required: false,
|
||||
properties: {
|
||||
tag: Property.StaticDropdown({
|
||||
displayName: 'Tag',
|
||||
description: 'Select a tag for this note',
|
||||
required: true,
|
||||
options: {
|
||||
options: tagOptions
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
tags_array: Property.Array({
|
||||
displayName: 'Tags',
|
||||
description: 'Add tags to this note (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
tag: Property.ShortText({
|
||||
displayName: 'Tag Name',
|
||||
description: 'Enter the tag name exactly',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
|
||||
|
||||
const linkedToResource: any = {
|
||||
id: propsValue.contact_id,
|
||||
type: 'Contact'
|
||||
};
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true });
|
||||
const selectedContact = contacts.find((contact: any) => contact.id === propsValue.contact_id);
|
||||
if (selectedContact) {
|
||||
linkedToResource.name = selectedContact.name || `${selectedContact.first_name} ${selectedContact.last_name}`.trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch contact name for reference:', error);
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
content: propsValue.content,
|
||||
linked_to: [linkedToResource]
|
||||
};
|
||||
|
||||
if (propsValue.visible_to) {
|
||||
requestBody.visible_to = propsValue.visible_to;
|
||||
}
|
||||
|
||||
const tagsArray = (propsValue as any).tags_array;
|
||||
if (tagsArray && Array.isArray(tagsArray) && tagsArray.length > 0) {
|
||||
requestBody.tags = tagsArray.map((tagItem: any) => tagItem.tag);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/notes`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('create note', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create note: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchUsers, fetchUserGroups, fetchOpportunityStages, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES, OPPORTUNITY_AMOUNT_KINDS, CURRENCIES } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createOpportunity = createAction({
|
||||
name: 'create_opportunity',
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Create Opportunity',
|
||||
description: 'Logs an opportunity including stage, close date, amount. Automate opportunity tracking after meetings.',
|
||||
props: {
|
||||
name: Property.ShortText({
|
||||
displayName: 'Opportunity Name',
|
||||
description: 'The name of the opportunity (e.g., "Financial Plan", "Investment Advisory", "Estate Planning")',
|
||||
required: true
|
||||
}),
|
||||
target_close: Property.DateTime({
|
||||
displayName: 'Target Close Date',
|
||||
description: 'When the opportunity should close',
|
||||
required: true
|
||||
}),
|
||||
probability: Property.Number({
|
||||
displayName: 'Probability (%)',
|
||||
description: 'The chance the opportunity will close, as a percentage (0-100)',
|
||||
required: true
|
||||
}),
|
||||
|
||||
amount: Property.Number({
|
||||
displayName: 'Amount',
|
||||
description: 'The monetary value of the opportunity',
|
||||
required: true
|
||||
}),
|
||||
currency: Property.StaticDropdown({
|
||||
displayName: 'Currency',
|
||||
description: 'The currency for the opportunity amount',
|
||||
required: false,
|
||||
defaultValue: CURRENCIES.USD,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'USD ($)', value: CURRENCIES.USD },
|
||||
{ label: 'EUR (€)', value: CURRENCIES.EUR },
|
||||
{ label: 'GBP (£)', value: CURRENCIES.GBP },
|
||||
{ label: 'CAD (C$)', value: CURRENCIES.CAD },
|
||||
{ label: 'AUD (A$)', value: CURRENCIES.AUD }
|
||||
]
|
||||
}
|
||||
}),
|
||||
amount_kind: Property.StaticDropdown({
|
||||
displayName: 'Amount Type',
|
||||
description: 'The type of amount this represents',
|
||||
required: false,
|
||||
defaultValue: OPPORTUNITY_AMOUNT_KINDS.FEE,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Fee', value: OPPORTUNITY_AMOUNT_KINDS.FEE },
|
||||
{ label: 'Commission', value: OPPORTUNITY_AMOUNT_KINDS.COMMISSION },
|
||||
{ label: 'AUM', value: OPPORTUNITY_AMOUNT_KINDS.AUM },
|
||||
{ label: 'Other', value: OPPORTUNITY_AMOUNT_KINDS.OTHER }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
stage: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Stage',
|
||||
description: 'Select the current stage of this opportunity',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const stages = await fetchOpportunityStages(auth.secret_text);
|
||||
return {
|
||||
options: stages.map((stage: any) => ({
|
||||
label: stage.name,
|
||||
value: stage.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load opportunity stages. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
contact_id: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Linked Contact',
|
||||
description: 'Select the contact linked to this opportunity',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load contacts. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
description: Property.LongText({
|
||||
displayName: 'Description',
|
||||
description: 'A detailed explanation of the opportunity',
|
||||
required: false
|
||||
}),
|
||||
|
||||
manager: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Opportunity Manager',
|
||||
description: 'Select the user designated as manager of this opportunity',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const users = await fetchUsers(auth.secret_text);
|
||||
const assignableUsers = users.filter((user: any) => !user.excluded_from_assignments);
|
||||
return {
|
||||
options: assignableUsers.map((user: any) => ({
|
||||
label: `${user.name} (${user.email})`,
|
||||
value: user.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load users. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this opportunity',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
return {
|
||||
options: userGroups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.name
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
custom_fields: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this opportunity',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this opportunity',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Custom field name',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.OPPORTUNITY);
|
||||
const customFieldOptions = customFields.map((field: any) => ({
|
||||
label: field.name,
|
||||
value: field.name
|
||||
}));
|
||||
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this opportunity',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.StaticDropdown({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Select a custom field for this opportunity',
|
||||
required: true,
|
||||
options: {
|
||||
options: customFieldOptions
|
||||
}
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this opportunity (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field Name',
|
||||
description: 'Enter the custom field name exactly',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
if (propsValue.probability < 0 || propsValue.probability > 100) {
|
||||
throw new Error('Probability must be between 0 and 100');
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
name: propsValue.name,
|
||||
target_close: propsValue.target_close,
|
||||
probability: propsValue.probability,
|
||||
stage: propsValue.stage,
|
||||
amounts: [
|
||||
{
|
||||
amount: propsValue.amount,
|
||||
currency: propsValue.currency || CURRENCIES.USD,
|
||||
kind: propsValue.amount_kind || OPPORTUNITY_AMOUNT_KINDS.FEE
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (propsValue.contact_id) {
|
||||
try {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true });
|
||||
const selectedContact = contacts.find((contact: any) => contact.id === propsValue.contact_id);
|
||||
|
||||
requestBody.linked_to = [{
|
||||
id: propsValue.contact_id,
|
||||
type: 'Contact',
|
||||
name: selectedContact ? (selectedContact.name || `${selectedContact.first_name} ${selectedContact.last_name}`.trim()) : `Contact ${propsValue.contact_id}`
|
||||
}];
|
||||
} catch (error) {
|
||||
requestBody.linked_to = [{
|
||||
id: propsValue.contact_id,
|
||||
type: 'Contact',
|
||||
name: `Contact ${propsValue.contact_id}`
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (propsValue.description) {
|
||||
requestBody.description = propsValue.description;
|
||||
}
|
||||
|
||||
if (propsValue.manager) {
|
||||
requestBody.manager = propsValue.manager;
|
||||
}
|
||||
|
||||
if (propsValue.visible_to) {
|
||||
requestBody.visible_to = propsValue.visible_to;
|
||||
}
|
||||
|
||||
const customFieldsArray = (propsValue as any).custom_fields_array;
|
||||
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.OPPORTUNITY);
|
||||
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
|
||||
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => {
|
||||
const fieldId = customFieldMap.get(field.custom_field);
|
||||
if (!fieldId) {
|
||||
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
|
||||
}
|
||||
return {
|
||||
id: fieldId,
|
||||
value: field.value
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Custom field')) {
|
||||
throw error;
|
||||
}
|
||||
console.warn('Could not fetch custom fields for validation:', error);
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
|
||||
id: field.custom_field,
|
||||
value: field.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/opportunities`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('create opportunity', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create opportunity: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchUserGroups, fetchUsers, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createProject = createAction({
|
||||
auth: wealthboxAuth,
|
||||
name: 'create_project',
|
||||
displayName: 'Create Project',
|
||||
description: 'Starts a new project with description and organizer. Launch project-based onboarding when new clients sign up.',
|
||||
props: {
|
||||
name: Property.ShortText({
|
||||
displayName: 'Project Name',
|
||||
description: 'The name of the project (e.g., "Client Onboarding", "Q1 Review Process")',
|
||||
required: true
|
||||
}),
|
||||
description: Property.LongText({
|
||||
displayName: 'Project Description',
|
||||
description: 'A detailed explanation of the project goals and scope',
|
||||
required: true
|
||||
}),
|
||||
|
||||
organizer: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Organizer',
|
||||
description: 'Select the user who will be responsible for organizing this project',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const users = await fetchUsers(auth.secret_text);
|
||||
const assignableUsers = users.filter((user: any) => !user.excluded_from_assignments);
|
||||
return {
|
||||
options: assignableUsers.map((user: any) => ({
|
||||
label: `${user.name} (${user.email})`,
|
||||
value: user.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load users. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this project',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
return {
|
||||
options: userGroups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.name
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
custom_fields: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this project',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this project',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Custom field name',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.PROJECT);
|
||||
const customFieldOptions = customFields.map((field: any) => ({
|
||||
label: field.name,
|
||||
value: field.name
|
||||
}));
|
||||
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this project',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.StaticDropdown({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Select a custom field for this project',
|
||||
required: true,
|
||||
options: {
|
||||
options: customFieldOptions
|
||||
}
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this project (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field Name',
|
||||
description: 'Enter the custom field name exactly',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const requestBody: any = {
|
||||
name: propsValue.name,
|
||||
description: propsValue.description
|
||||
};
|
||||
|
||||
if (propsValue.organizer) {
|
||||
requestBody.organizer = propsValue.organizer;
|
||||
}
|
||||
|
||||
if (propsValue.visible_to) {
|
||||
requestBody.visible_to = propsValue.visible_to;
|
||||
}
|
||||
|
||||
const customFieldsArray = (propsValue as any).custom_fields_array;
|
||||
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.PROJECT);
|
||||
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
|
||||
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => {
|
||||
const fieldId = customFieldMap.get(field.custom_field);
|
||||
if (!fieldId) {
|
||||
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
|
||||
}
|
||||
return {
|
||||
id: fieldId,
|
||||
value: field.value
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Custom field')) {
|
||||
throw error;
|
||||
}
|
||||
console.warn('Could not fetch custom fields for validation:', error);
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
|
||||
id: field.custom_field,
|
||||
value: field.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/projects`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('create project', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,442 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchUsers, fetchUserGroups, fetchTaskCategories, fetchProjects, fetchOpportunities, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES, TASK_PRIORITIES, LINK_TYPES } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const createTask = createAction({
|
||||
auth: wealthboxAuth,
|
||||
name: 'create_task',
|
||||
displayName: 'Create Task',
|
||||
description: 'Creates tasks tied to contacts with due dates and assignment types. Assign follow-up actions when opportunities are created.',
|
||||
props: {
|
||||
name: Property.ShortText({
|
||||
displayName: 'Task Name',
|
||||
description: 'The name of the task (e.g., "Return Bill\'s call", "Follow up on proposal")',
|
||||
required: true
|
||||
}),
|
||||
due_date: Property.DateTime({
|
||||
displayName: 'Due Date',
|
||||
description: 'When the task is due',
|
||||
required: true
|
||||
}),
|
||||
|
||||
assigned_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Assigned To',
|
||||
description: 'Select the user who the task is assigned to',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const users = await fetchUsers(auth.secret_text);
|
||||
const assignableUsers = users.filter((user: any) => !user.excluded_from_assignments);
|
||||
return {
|
||||
options: assignableUsers.map((user: any) => ({
|
||||
label: `${user.name} (${user.email})`,
|
||||
value: user.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load users. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
description: Property.LongText({
|
||||
displayName: 'Description',
|
||||
description: 'A detailed explanation of the task',
|
||||
required: false
|
||||
}),
|
||||
priority: Property.StaticDropdown({
|
||||
displayName: 'Priority',
|
||||
description: 'The priority level of the task',
|
||||
required: false,
|
||||
defaultValue: TASK_PRIORITIES.MEDIUM,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Low', value: TASK_PRIORITIES.LOW },
|
||||
{ label: 'Medium', value: TASK_PRIORITIES.MEDIUM },
|
||||
{ label: 'High', value: TASK_PRIORITIES.HIGH }
|
||||
]
|
||||
}
|
||||
}),
|
||||
complete: Property.Checkbox({
|
||||
displayName: 'Mark as Complete',
|
||||
description: 'Check if the task should be created as already completed',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
}),
|
||||
|
||||
link_type: Property.StaticDropdown({
|
||||
displayName: 'Link To',
|
||||
description: 'What type of record to link this task to',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Contact', value: LINK_TYPES.CONTACT },
|
||||
{ label: 'Project', value: LINK_TYPES.PROJECT },
|
||||
{ label: 'Opportunity', value: LINK_TYPES.OPPORTUNITY }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
linked_record: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Linked Record',
|
||||
description: 'Select the record to link this task to',
|
||||
required: false,
|
||||
refreshers: ['link_type'],
|
||||
props: async ({ auth, link_type }) => {
|
||||
if (!auth || !link_type) {
|
||||
return {
|
||||
linked_id: Property.ShortText({
|
||||
displayName: 'Linked Record ID',
|
||||
description: 'Enter the record ID to link to',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const linkTypeStr = link_type as unknown as string;
|
||||
if (linkTypeStr === LINK_TYPES.CONTACT) {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
return {
|
||||
linked_id: Property.StaticDropdown({
|
||||
displayName: 'Contact',
|
||||
description: 'Select the contact to link this task to',
|
||||
required: false,
|
||||
options: {
|
||||
options: contacts.map((contact: any) => ({
|
||||
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
|
||||
value: contact.id
|
||||
}))
|
||||
}
|
||||
})
|
||||
};
|
||||
} else if (linkTypeStr === LINK_TYPES.PROJECT) {
|
||||
const projects = await fetchProjects(auth.secret_text);
|
||||
return {
|
||||
linked_id: Property.StaticDropdown({
|
||||
displayName: 'Project',
|
||||
description: 'Select the project to link this task to',
|
||||
required: false,
|
||||
options: {
|
||||
options: projects.map((project: any) => ({
|
||||
label: project.name || `Project ${project.id}`,
|
||||
value: project.id
|
||||
}))
|
||||
}
|
||||
})
|
||||
};
|
||||
} else if (linkTypeStr === LINK_TYPES.OPPORTUNITY) {
|
||||
const opportunities = await fetchOpportunities(auth.secret_text);
|
||||
return {
|
||||
linked_id: Property.StaticDropdown({
|
||||
displayName: 'Opportunity',
|
||||
description: 'Select the opportunity to link this task to',
|
||||
required: false,
|
||||
options: {
|
||||
options: opportunities.map((opportunity: any) => ({
|
||||
label: opportunity.name || `Opportunity ${opportunity.id}`,
|
||||
value: opportunity.id
|
||||
}))
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
linked_id: Property.ShortText({
|
||||
displayName: 'Linked Record ID',
|
||||
description: 'Enter the record ID to link to (API unavailable)',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
linked_id: Property.ShortText({
|
||||
displayName: 'Linked Record ID',
|
||||
description: 'Enter the record ID to link to',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
category: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Category',
|
||||
description: 'Select the category this task belongs to',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const categories = await fetchTaskCategories(auth.secret_text);
|
||||
return {
|
||||
options: categories.map((category: any) => ({
|
||||
label: category.name,
|
||||
value: category.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load task categories. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this task',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
return {
|
||||
options: userGroups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.name
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
due_later: Property.ShortText({
|
||||
displayName: 'Due Later',
|
||||
description: 'Interval for when this task is due after start (e.g., "2 days later at 5:00 PM")',
|
||||
required: false
|
||||
}),
|
||||
|
||||
custom_fields: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this task',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this task',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Custom field name',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.TASK);
|
||||
const customFieldOptions = customFields.map((field: any) => ({
|
||||
label: field.name,
|
||||
value: field.name
|
||||
}));
|
||||
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this task',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.StaticDropdown({
|
||||
displayName: 'Custom Field',
|
||||
description: 'Select a custom field for this task',
|
||||
required: true,
|
||||
options: {
|
||||
options: customFieldOptions
|
||||
}
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
custom_fields_array: Property.Array({
|
||||
displayName: 'Custom Fields',
|
||||
description: 'Add custom fields to this task (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
custom_field: Property.ShortText({
|
||||
displayName: 'Custom Field Name',
|
||||
description: 'Enter the custom field name exactly',
|
||||
required: true
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value for this custom field',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('API access token is required');
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
name: propsValue.name,
|
||||
due_date: propsValue.due_date
|
||||
};
|
||||
|
||||
if (propsValue.assigned_to) {
|
||||
requestBody.assigned_to = propsValue.assigned_to;
|
||||
}
|
||||
|
||||
if (propsValue.description) {
|
||||
requestBody.description = propsValue.description;
|
||||
}
|
||||
|
||||
if (propsValue.priority) {
|
||||
requestBody.priority = propsValue.priority;
|
||||
}
|
||||
|
||||
if (propsValue.complete !== undefined) {
|
||||
requestBody.complete = propsValue.complete;
|
||||
}
|
||||
|
||||
if (propsValue.category) {
|
||||
requestBody.category = propsValue.category;
|
||||
}
|
||||
|
||||
if (propsValue.visible_to) {
|
||||
requestBody.visible_to = propsValue.visible_to;
|
||||
}
|
||||
|
||||
if (propsValue.due_later) {
|
||||
requestBody.due_later = propsValue.due_later;
|
||||
}
|
||||
|
||||
const linkedRecord = (propsValue as any).linked_record;
|
||||
if (propsValue.link_type && linkedRecord?.linked_id) {
|
||||
try {
|
||||
let recordName = `${propsValue.link_type} ${linkedRecord.linked_id}`;
|
||||
|
||||
const linkTypeStr = propsValue.link_type as string;
|
||||
if (linkTypeStr === LINK_TYPES.CONTACT) {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true });
|
||||
const contact = contacts.find((c: any) => c.id === linkedRecord.linked_id);
|
||||
if (contact) {
|
||||
recordName = contact.name || `${contact.first_name} ${contact.last_name}`.trim();
|
||||
}
|
||||
} else if (linkTypeStr === LINK_TYPES.PROJECT) {
|
||||
const projects = await fetchProjects(auth.secret_text);
|
||||
const project = projects.find((p: any) => p.id === linkedRecord.linked_id);
|
||||
if (project) {
|
||||
recordName = project.name;
|
||||
}
|
||||
} else if (linkTypeStr === LINK_TYPES.OPPORTUNITY) {
|
||||
const opportunities = await fetchOpportunities(auth.secret_text);
|
||||
const opportunity = opportunities.find((o: any) => o.id === linkedRecord.linked_id);
|
||||
if (opportunity) {
|
||||
recordName = opportunity.name;
|
||||
}
|
||||
}
|
||||
|
||||
requestBody.linked_to = [{
|
||||
id: linkedRecord.linked_id,
|
||||
type: propsValue.link_type,
|
||||
name: recordName
|
||||
}];
|
||||
} catch (error) {
|
||||
requestBody.linked_to = [{
|
||||
id: linkedRecord.linked_id,
|
||||
type: propsValue.link_type,
|
||||
name: `${propsValue.link_type} ${linkedRecord.linked_id}`
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
const customFieldsArray = (propsValue as any).custom_fields_array;
|
||||
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
|
||||
try {
|
||||
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.TASK);
|
||||
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
|
||||
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => {
|
||||
const fieldId = customFieldMap.get(field.custom_field);
|
||||
if (!fieldId) {
|
||||
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
|
||||
}
|
||||
return {
|
||||
id: fieldId,
|
||||
value: field.value
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Custom field')) {
|
||||
throw error;
|
||||
}
|
||||
console.warn('Could not fetch custom fields for validation:', error);
|
||||
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
|
||||
id: field.custom_field,
|
||||
value: field.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/tasks`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('create task', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,333 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchTags, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const findContact = createAction({
|
||||
name: 'find_contact',
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Find Contact',
|
||||
description: 'Locate a contact by name, email, phone, or advanced filters. Comprehensive contact search with dynamic filtering options.',
|
||||
props: {
|
||||
name: Property.ShortText({
|
||||
displayName: 'Name',
|
||||
description: 'Search by name (supports partial matches across prefix, first, middle, last, suffix, nickname, and full name for households/companies)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
email: Property.ShortText({
|
||||
displayName: 'Email Address',
|
||||
description: 'Search by email address',
|
||||
required: false
|
||||
}),
|
||||
|
||||
phone: Property.ShortText({
|
||||
displayName: 'Phone Number',
|
||||
description: 'Search by phone number (delimiters like -, (), will be stripped automatically)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
contact_id: Property.Number({
|
||||
displayName: 'Contact ID',
|
||||
description: 'Search by specific contact ID (most precise search)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
external_unique_id: Property.ShortText({
|
||||
displayName: 'External Unique ID',
|
||||
description: 'Search by external unique identifier',
|
||||
required: false
|
||||
}),
|
||||
|
||||
contact_type: Property.StaticDropdown({
|
||||
displayName: 'Contact Type',
|
||||
description: 'Filter by contact type',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Client', value: 'Client' },
|
||||
{ label: 'Past Client', value: 'Past Client' },
|
||||
{ label: 'Prospect', value: 'Prospect' },
|
||||
{ label: 'Vendor', value: 'Vendor' },
|
||||
{ label: 'Organization', value: 'Organization' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Contact Entity Type',
|
||||
description: 'Filter by entity type',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Person', value: 'person' },
|
||||
{ label: 'Household', value: 'household' },
|
||||
{ label: 'Organization', value: 'organization' },
|
||||
{ label: 'Trust', value: 'trust' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
household_title: Property.StaticDropdown({
|
||||
displayName: 'Household Title',
|
||||
description: 'Filter by household title (only applies to household members)',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Head', value: 'Head' },
|
||||
{ label: 'Spouse', value: 'Spouse' },
|
||||
{ label: 'Partner', value: 'Partner' },
|
||||
{ label: 'Child', value: 'Child' },
|
||||
{ label: 'Grandchild', value: 'Grandchild' },
|
||||
{ label: 'Parent', value: 'Parent' },
|
||||
{ label: 'Grandparent', value: 'Grandparent' },
|
||||
{ label: 'Sibling', value: 'Sibling' },
|
||||
{ label: 'Other', value: 'Other' },
|
||||
{ label: 'Dependent', value: 'Dependent' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
tags_filter: Property.MultiSelectDropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Tags Filter',
|
||||
description: 'Filter contacts by tags',
|
||||
required: false,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Connect your Wealthbox account first'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const availableTags = await fetchTags(auth.secret_text, 'Contact');
|
||||
const tagOptions = availableTags.map((tag: any) => ({
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}));
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: tagOptions,
|
||||
placeholder: tagOptions.length === 0 ? 'No tags available' : 'Select tags to filter by'
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: `Error loading tags: ${errorMessage}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
active: Property.StaticDropdown({
|
||||
displayName: 'Active Status',
|
||||
description: 'Filter by active status',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Active Only', value: 'true' },
|
||||
{ label: 'Inactive Only', value: 'false' },
|
||||
{ label: 'All Contacts', value: '' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
include_deleted: Property.Checkbox({
|
||||
displayName: 'Include Deleted Contacts',
|
||||
description: 'Include contacts that have been deleted',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
}),
|
||||
|
||||
updated_since: Property.DateTime({
|
||||
displayName: 'Updated Since',
|
||||
description: 'Only return contacts updated on or after this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
updated_before: Property.DateTime({
|
||||
displayName: 'Updated Before',
|
||||
description: 'Only return contacts updated on or before this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
order: Property.StaticDropdown({
|
||||
displayName: 'Sort Order',
|
||||
description: 'How to order the results',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Recent (newest first)', value: 'recent' },
|
||||
{ label: 'Created Date (newest first)', value: 'created' },
|
||||
{ label: 'Updated Date (newest first)', value: 'updated' },
|
||||
{ label: 'Ascending', value: 'asc' },
|
||||
{ label: 'Descending', value: 'desc' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
limit: Property.Number({
|
||||
displayName: 'Result Limit',
|
||||
description: 'Maximum number of contacts to return (default: 50, max: 1000)',
|
||||
required: false,
|
||||
defaultValue: 50
|
||||
}),
|
||||
|
||||
return_single_result: Property.Checkbox({
|
||||
displayName: 'Return Single Result Only',
|
||||
description: 'If checked, returns only the first matching contact. If unchecked, returns all matching contacts.',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
const hasSearchCriteria =
|
||||
propsValue.contact_id ||
|
||||
propsValue.name ||
|
||||
propsValue.email ||
|
||||
propsValue.phone ||
|
||||
propsValue.external_unique_id ||
|
||||
propsValue.contact_type ||
|
||||
propsValue.type ||
|
||||
propsValue.household_title ||
|
||||
(propsValue as any).tags?.tags_filter ||
|
||||
propsValue.active ||
|
||||
propsValue.include_deleted ||
|
||||
propsValue.updated_since ||
|
||||
propsValue.updated_before;
|
||||
|
||||
if (!hasSearchCriteria) {
|
||||
throw new Error('At least one search criteria must be provided (ID, name, email, phone, external ID, or filters)');
|
||||
}
|
||||
|
||||
if (propsValue.contact_id && !(propsValue.name || propsValue.email || propsValue.phone || propsValue.external_unique_id || propsValue.contact_type || propsValue.type || propsValue.household_title || (propsValue as any).tags?.tags_filter || propsValue.active || propsValue.include_deleted || propsValue.updated_since || propsValue.updated_before)) {
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/contacts/${propsValue.contact_id}`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('find contact by ID', response.status, response.body);
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
contact: response.body,
|
||||
contacts: [response.body],
|
||||
total_results: 1,
|
||||
search_criteria: { contact_id: propsValue.contact_id }
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('404')) {
|
||||
return {
|
||||
found: false,
|
||||
contact: null,
|
||||
contacts: [],
|
||||
total_results: 0,
|
||||
message: `No contact found with ID: ${propsValue.contact_id}`,
|
||||
search_criteria: { contact_id: propsValue.contact_id }
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (propsValue.name) searchParams.append('name', propsValue.name);
|
||||
if (propsValue.email) searchParams.append('email', propsValue.email);
|
||||
if (propsValue.phone) searchParams.append('phone', propsValue.phone);
|
||||
if (propsValue.contact_id) searchParams.append('id', propsValue.contact_id.toString());
|
||||
if (propsValue.external_unique_id) searchParams.append('external_unique_id', propsValue.external_unique_id);
|
||||
|
||||
if (propsValue.contact_type) searchParams.append('contact_type', propsValue.contact_type);
|
||||
if (propsValue.type) searchParams.append('type', propsValue.type);
|
||||
if (propsValue.household_title) searchParams.append('household_title', propsValue.household_title);
|
||||
|
||||
const tagsFilter = propsValue.tags_filter;
|
||||
if (tagsFilter && Array.isArray(tagsFilter) && tagsFilter.length > 0) {
|
||||
tagsFilter.forEach((tag: string) => {
|
||||
searchParams.append('tags[]', tag);
|
||||
});
|
||||
}
|
||||
|
||||
if (propsValue.active && propsValue.active !== '') {
|
||||
searchParams.append('active', propsValue.active);
|
||||
}
|
||||
if (propsValue.include_deleted) {
|
||||
searchParams.append('deleted', 'true');
|
||||
}
|
||||
|
||||
if (propsValue.updated_since) searchParams.append('updated_since', propsValue.updated_since);
|
||||
if (propsValue.updated_before) searchParams.append('updated_before', propsValue.updated_before);
|
||||
|
||||
if (propsValue.order) searchParams.append('order', propsValue.order);
|
||||
|
||||
const limit = Math.min(propsValue.limit || 50, 1000);
|
||||
searchParams.append('limit', limit.toString());
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = queryString ? `${WEALTHBOX_API_BASE}/contacts?${queryString}` : `${WEALTHBOX_API_BASE}/contacts`;
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: url,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('find contacts', response.status, response.body);
|
||||
}
|
||||
|
||||
const contacts = response.body.contacts || [];
|
||||
const totalResults = contacts.length;
|
||||
|
||||
if (propsValue.return_single_result || totalResults === 1) {
|
||||
return {
|
||||
found: totalResults > 0,
|
||||
contact: totalResults > 0 ? contacts[0] : null,
|
||||
contacts: contacts,
|
||||
total_results: totalResults,
|
||||
search_criteria: Object.fromEntries(searchParams),
|
||||
message: totalResults === 0 ? 'No contacts found matching the search criteria' : undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
found: totalResults > 0,
|
||||
contacts: contacts,
|
||||
total_results: totalResults,
|
||||
search_criteria: Object.fromEntries(searchParams),
|
||||
message: totalResults === 0 ? 'No contacts found matching the search criteria' : undefined
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to find contacts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,347 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchContacts, fetchProjects, fetchOpportunities, fetchUsers, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const findTask = createAction({
|
||||
auth: wealthboxAuth,
|
||||
name: 'find_task',
|
||||
displayName: 'Find Task',
|
||||
description: 'Finds existing tasks using comprehensive search filters. Search by assignment, resource, completion status, and date ranges.',
|
||||
props: {
|
||||
task_id: Property.Number({
|
||||
displayName: 'Task ID (Optional)',
|
||||
description: 'Search for a specific task by its unique ID. Leave empty to search using filters.',
|
||||
required: false
|
||||
}),
|
||||
|
||||
resource_type: Property.StaticDropdown({
|
||||
displayName: 'Linked Resource Type',
|
||||
description: 'Filter tasks by the type of resource they are linked to',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Contact', value: 'Contact' },
|
||||
{ label: 'Project', value: 'Project' },
|
||||
{ label: 'Opportunity', value: 'Opportunity' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
resource_record: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Linked Resource',
|
||||
description: 'Select the specific resource to filter tasks by',
|
||||
required: false,
|
||||
refreshers: ['resource_type'],
|
||||
props: async ({ auth, resource_type }) => {
|
||||
if (!auth || !resource_type) {
|
||||
return {
|
||||
resource_id: Property.Number({
|
||||
displayName: 'Resource ID',
|
||||
description: 'Enter the resource ID manually',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let records: any[] = [];
|
||||
let recordType = '';
|
||||
|
||||
const resourceTypeValue = resource_type as unknown as string;
|
||||
|
||||
switch (resourceTypeValue) {
|
||||
case 'Contact':
|
||||
records = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
recordType = 'Contact';
|
||||
break;
|
||||
case 'Project':
|
||||
records = await fetchProjects(auth.secret_text);
|
||||
recordType = 'Project';
|
||||
break;
|
||||
case 'Opportunity':
|
||||
records = await fetchOpportunities(auth.secret_text);
|
||||
recordType = 'Opportunity';
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
resource_id: Property.Number({
|
||||
displayName: 'Resource ID',
|
||||
description: 'Enter the resource ID manually',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const recordOptions = records.map((record: any) => ({
|
||||
label: record.name || record.title || `${recordType} ${record.id}`,
|
||||
value: record.id
|
||||
}));
|
||||
|
||||
return {
|
||||
resource_id: Property.StaticDropdown({
|
||||
displayName: `${recordType} Record`,
|
||||
description: `Select the ${recordType.toLowerCase()} to filter tasks by`,
|
||||
required: false,
|
||||
options: {
|
||||
options: recordOptions
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
resource_id: Property.Number({
|
||||
displayName: 'Resource ID',
|
||||
description: 'Enter the resource ID manually (API unavailable)',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
assigned_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Assigned To',
|
||||
description: 'Filter tasks by the user they are assigned to',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const users = await fetchUsers(auth.secret_text);
|
||||
return {
|
||||
options: users.map((user: any) => ({
|
||||
label: `${user.name} (${user.email})`,
|
||||
value: user.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load users. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
assigned_to_team: Property.Number({
|
||||
displayName: 'Assigned to Team ID',
|
||||
description: 'Filter tasks by the team they are assigned to',
|
||||
required: false
|
||||
}),
|
||||
|
||||
created_by: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Created By',
|
||||
description: 'Filter tasks by the user who created them',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const users = await fetchUsers(auth.secret_text);
|
||||
return {
|
||||
options: users.map((user: any) => ({
|
||||
label: `${user.name} (${user.email})`,
|
||||
value: user.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load users. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
completed: Property.StaticDropdown({
|
||||
displayName: 'Completion Status',
|
||||
description: 'Filter by task completion status',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'All Tasks', value: '' },
|
||||
{ label: 'Completed Only', value: 'true' },
|
||||
{ label: 'Incomplete Only', value: 'false' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
task_type: Property.StaticDropdown({
|
||||
displayName: 'Task Type',
|
||||
description: 'Filter by task type',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'All Tasks', value: 'all' },
|
||||
{ label: 'Parent Tasks Only', value: 'parents' },
|
||||
{ label: 'Subtasks Only', value: 'subtasks' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
updated_since: Property.DateTime({
|
||||
displayName: 'Updated Since',
|
||||
description: 'Only return tasks updated on or after this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
updated_before: Property.DateTime({
|
||||
displayName: 'Updated Before',
|
||||
description: 'Only return tasks updated on or before this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
limit: Property.Number({
|
||||
displayName: 'Result Limit',
|
||||
description: 'Maximum number of tasks to return (default: 50, max: 1000)',
|
||||
required: false,
|
||||
defaultValue: 50
|
||||
}),
|
||||
|
||||
return_single_result: Property.Checkbox({
|
||||
displayName: 'Return Single Result Only',
|
||||
description: 'If checked, returns only the first matching task. If unchecked, returns all matching tasks.',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
|
||||
|
||||
const hasSearchCriteria =
|
||||
propsValue.task_id ||
|
||||
propsValue.resource_type ||
|
||||
(propsValue as any).resource_record?.resource_id ||
|
||||
propsValue.assigned_to ||
|
||||
propsValue.assigned_to_team ||
|
||||
propsValue.created_by ||
|
||||
propsValue.completed ||
|
||||
propsValue.task_type ||
|
||||
propsValue.updated_since ||
|
||||
propsValue.updated_before;
|
||||
|
||||
if (!hasSearchCriteria) {
|
||||
throw new Error('At least one search criteria must be provided (Task ID, resource, assignment, status, or date filters)');
|
||||
}
|
||||
|
||||
if (propsValue.task_id && !(propsValue.resource_type || (propsValue as any).resource_record?.resource_id || propsValue.assigned_to || propsValue.assigned_to_team || propsValue.created_by || propsValue.completed || propsValue.task_type || propsValue.updated_since || propsValue.updated_before)) {
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/tasks/${propsValue.task_id}`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('find task by ID', response.status, response.body);
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
task: response.body,
|
||||
tasks: [response.body],
|
||||
total_results: 1,
|
||||
search_criteria: { task_id: propsValue.task_id }
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('404')) {
|
||||
return {
|
||||
found: false,
|
||||
task: null,
|
||||
tasks: [],
|
||||
total_results: 0,
|
||||
message: `No task found with ID: ${propsValue.task_id}`,
|
||||
search_criteria: { task_id: propsValue.task_id }
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (propsValue.task_id) searchParams.append('id', propsValue.task_id.toString());
|
||||
|
||||
if (propsValue.resource_type) searchParams.append('resource_type', propsValue.resource_type);
|
||||
const resourceRecord = (propsValue as any).resource_record;
|
||||
if (resourceRecord?.resource_id) {
|
||||
searchParams.append('resource_id', resourceRecord.resource_id.toString());
|
||||
}
|
||||
|
||||
if (propsValue.assigned_to) searchParams.append('assigned_to', propsValue.assigned_to);
|
||||
if (propsValue.assigned_to_team) searchParams.append('assigned_to_team', propsValue.assigned_to_team.toString());
|
||||
if (propsValue.created_by) searchParams.append('created_by', propsValue.created_by);
|
||||
|
||||
if (propsValue.completed && propsValue.completed !== '') {
|
||||
searchParams.append('completed', propsValue.completed);
|
||||
}
|
||||
if (propsValue.task_type && propsValue.task_type !== 'all') {
|
||||
searchParams.append('task_type', propsValue.task_type);
|
||||
}
|
||||
|
||||
if (propsValue.updated_since) searchParams.append('updated_since', propsValue.updated_since);
|
||||
if (propsValue.updated_before) searchParams.append('updated_before', propsValue.updated_before);
|
||||
|
||||
const limit = Math.min(propsValue.limit || 50, 1000);
|
||||
searchParams.append('limit', limit.toString());
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = queryString ? `${WEALTHBOX_API_BASE}/tasks?${queryString}` : `${WEALTHBOX_API_BASE}/tasks`;
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: url,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('find tasks', response.status, response.body);
|
||||
}
|
||||
|
||||
const tasks = response.body.tasks || [];
|
||||
const totalResults = tasks.length;
|
||||
|
||||
if (propsValue.return_single_result || totalResults === 1) {
|
||||
return {
|
||||
found: totalResults > 0,
|
||||
task: totalResults > 0 ? tasks[0] : null,
|
||||
tasks: tasks,
|
||||
total_results: totalResults,
|
||||
search_criteria: Object.fromEntries(searchParams),
|
||||
message: totalResults === 0 ? 'No tasks found matching the search criteria' : undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
found: totalResults > 0,
|
||||
tasks: tasks,
|
||||
total_results: totalResults,
|
||||
search_criteria: Object.fromEntries(searchParams),
|
||||
message: totalResults === 0 ? 'No tasks found matching the search criteria' : undefined
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to find tasks: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export { createContact } from './create-contact';
|
||||
export { createNote } from './create-note';
|
||||
export { createProject } from './create-project';
|
||||
export { addHouseholdMember } from './add-household-member';
|
||||
export { createHousehold } from './create-household';
|
||||
export { createEvent } from './create-event';
|
||||
export { createOpportunity } from './create-opportunity';
|
||||
export { createTask } from './create-task';
|
||||
export { startWorkflow } from './start-workflow';
|
||||
export { findContact } from './find-contact';
|
||||
export { findTask } from './find-task';
|
||||
@@ -0,0 +1,376 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fetchWorkflowTemplates, fetchContacts, fetchProjects, fetchOpportunities, fetchUserGroups, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
export const startWorkflow = createAction({
|
||||
name: 'start_workflow',
|
||||
displayName: 'Start Workflow',
|
||||
auth: wealthboxAuth,
|
||||
description: 'Triggers a workflow template on a contact/project/opportunity. Automate multi-step sequences based on CRM events.',
|
||||
props: {
|
||||
workflow_template: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Workflow Template',
|
||||
description: 'Select the workflow template to trigger',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const templates = await fetchWorkflowTemplates(auth.secret_text);
|
||||
return {
|
||||
options: templates.map((template: any) => ({
|
||||
label: template.name || template.label || `Template ${template.id}`,
|
||||
value: template.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load workflow templates. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
linked_type: Property.StaticDropdown({
|
||||
displayName: 'Link To',
|
||||
description: 'What type of record to link this workflow to',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Contact', value: 'Contact' },
|
||||
{ label: 'Project', value: 'Project' },
|
||||
{ label: 'Opportunity', value: 'Opportunity' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
linked_record: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Linked Record',
|
||||
description: 'Select the record to link this workflow to',
|
||||
required: true,
|
||||
refreshers: ['linked_type'],
|
||||
props: async ({ auth, linked_type }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
linked_id: Property.Number({
|
||||
displayName: 'Record ID',
|
||||
description: 'Enter the record ID manually',
|
||||
required: true
|
||||
}),
|
||||
linked_name: Property.ShortText({
|
||||
displayName: 'Record Name',
|
||||
description: 'Enter the record name for reference',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let records: any[] = [];
|
||||
let recordType = '';
|
||||
|
||||
const linkedTypeValue = linked_type as unknown as string;
|
||||
|
||||
switch (linkedTypeValue) {
|
||||
case 'Contact':
|
||||
records = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
|
||||
recordType = 'Contact';
|
||||
break;
|
||||
case 'Project':
|
||||
records = await fetchProjects(auth.secret_text);
|
||||
recordType = 'Project';
|
||||
break;
|
||||
case 'Opportunity':
|
||||
records = await fetchOpportunities(auth.secret_text);
|
||||
recordType = 'Opportunity';
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
linked_id: Property.Number({
|
||||
displayName: 'Record ID',
|
||||
description: 'Enter the record ID manually',
|
||||
required: true
|
||||
}),
|
||||
linked_name: Property.ShortText({
|
||||
displayName: 'Record Name',
|
||||
description: 'Enter the record name for reference',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const recordOptions = records.map((record: any) => ({
|
||||
label: record.name || record.title || record.label || `${recordType} ${record.id}`,
|
||||
value: record.id
|
||||
}));
|
||||
|
||||
return {
|
||||
linked_id: Property.StaticDropdown({
|
||||
displayName: `${recordType} Record`,
|
||||
description: `Select the ${recordType.toLowerCase()} to link this workflow to`,
|
||||
required: true,
|
||||
options: {
|
||||
options: recordOptions
|
||||
}
|
||||
}),
|
||||
linked_name: Property.ShortText({
|
||||
displayName: 'Record Name',
|
||||
description: 'The name will be automatically populated from the selected record',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
linked_id: Property.Number({
|
||||
displayName: 'Record ID',
|
||||
description: 'Enter the record ID manually (API unavailable)',
|
||||
required: true
|
||||
}),
|
||||
linked_name: Property.ShortText({
|
||||
displayName: 'Record Name',
|
||||
description: 'Enter the record name for reference',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
label: Property.ShortText({
|
||||
displayName: 'Workflow Label',
|
||||
description: 'A short name for the workflow (e.g., "Onboard a new client to the firm")',
|
||||
required: false
|
||||
}),
|
||||
|
||||
starts_at: Property.DateTime({
|
||||
displayName: 'Start Date & Time',
|
||||
description: 'When you want the workflow to start (optional, defaults to now)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
visible_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Visible To',
|
||||
description: 'Select who can view this workflow',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) return { options: [] };
|
||||
|
||||
try {
|
||||
const userGroups = await fetchUserGroups(auth.secret_text);
|
||||
return {
|
||||
options: userGroups.map((group: any) => ({
|
||||
label: group.name,
|
||||
value: group.name
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load user groups. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
workflow_milestones: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Workflow Milestones',
|
||||
description: 'Add milestones to this workflow',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
props: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
milestones_array: Property.Array({
|
||||
displayName: 'Milestones',
|
||||
description: 'Add workflow milestones',
|
||||
required: false,
|
||||
properties: {
|
||||
milestone_id: Property.ShortText({
|
||||
displayName: 'Milestone ID',
|
||||
description: 'The ID of the milestone',
|
||||
required: true
|
||||
}),
|
||||
milestone_name: Property.ShortText({
|
||||
displayName: 'Milestone Name',
|
||||
description: 'The name of the milestone',
|
||||
required: true
|
||||
}),
|
||||
milestone_date: Property.DateTime({
|
||||
displayName: 'Milestone Date',
|
||||
description: 'When this milestone should occur',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
milestones_array: Property.Array({
|
||||
displayName: 'Milestones',
|
||||
description: 'Add workflow milestones for this template',
|
||||
required: false,
|
||||
properties: {
|
||||
milestone_id: Property.ShortText({
|
||||
displayName: 'Milestone ID',
|
||||
description: 'The ID of the milestone (from template)',
|
||||
required: true
|
||||
}),
|
||||
milestone_name: Property.ShortText({
|
||||
displayName: 'Milestone Name',
|
||||
description: 'The name of the milestone',
|
||||
required: true
|
||||
}),
|
||||
milestone_date: Property.DateTime({
|
||||
displayName: 'Milestone Date',
|
||||
description: 'When this milestone should occur',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch milestones for validation:', error);
|
||||
return {
|
||||
milestones_array: Property.Array({
|
||||
displayName: 'Milestones',
|
||||
description: 'Add workflow milestones (API unavailable)',
|
||||
required: false,
|
||||
properties: {
|
||||
milestone_id: Property.ShortText({
|
||||
displayName: 'Milestone ID',
|
||||
description: 'The ID of the milestone',
|
||||
required: true
|
||||
}),
|
||||
milestone_name: Property.ShortText({
|
||||
displayName: 'Milestone Name',
|
||||
description: 'The name of the milestone',
|
||||
required: true
|
||||
}),
|
||||
milestone_date: Property.DateTime({
|
||||
displayName: 'Milestone Date',
|
||||
description: 'When this milestone should occur',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { auth, propsValue } = context;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
|
||||
|
||||
const requestBody: any = {
|
||||
workflow_template: propsValue.workflow_template
|
||||
};
|
||||
|
||||
const linkedRecord = (propsValue as any).linked_record;
|
||||
if (linkedRecord) {
|
||||
let linkedId: number;
|
||||
let linkedName: string;
|
||||
|
||||
if (linkedRecord.linked_id) {
|
||||
linkedId = linkedRecord.linked_id;
|
||||
linkedName = linkedRecord.linked_name || `${propsValue.linked_type} ${linkedId}`;
|
||||
} else {
|
||||
linkedId = linkedRecord.linked_id;
|
||||
linkedName = linkedRecord.linked_name || `${propsValue.linked_type} ${linkedId}`;
|
||||
}
|
||||
|
||||
try {
|
||||
let recordName = linkedName;
|
||||
|
||||
if (propsValue.linked_type === 'Contact' && linkedId) {
|
||||
const contacts = await fetchContacts(auth.secret_text, { active: true });
|
||||
const contact = contacts.find((c: any) => c.id === linkedId);
|
||||
if (contact) {
|
||||
recordName = contact.name || `${contact.first_name} ${contact.last_name}`.trim();
|
||||
}
|
||||
} else if (propsValue.linked_type === 'Project' && linkedId) {
|
||||
const projects = await fetchProjects(auth.secret_text);
|
||||
const project = projects.find((p: any) => p.id === linkedId);
|
||||
if (project) {
|
||||
recordName = project.name || project.title || `Project ${linkedId}`;
|
||||
}
|
||||
} else if (propsValue.linked_type === 'Opportunity' && linkedId) {
|
||||
const opportunities = await fetchOpportunities(auth.secret_text);
|
||||
const opportunity = opportunities.find((o: any) => o.id === linkedId);
|
||||
if (opportunity) {
|
||||
recordName = opportunity.name || `Opportunity ${linkedId}`;
|
||||
}
|
||||
}
|
||||
|
||||
requestBody.linked_to = {
|
||||
id: linkedId,
|
||||
type: propsValue.linked_type,
|
||||
name: recordName
|
||||
};
|
||||
} catch (error) {
|
||||
requestBody.linked_to = {
|
||||
id: linkedId,
|
||||
type: propsValue.linked_type,
|
||||
name: linkedName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (propsValue.label) {
|
||||
requestBody.label = propsValue.label;
|
||||
}
|
||||
|
||||
if (propsValue.starts_at) {
|
||||
requestBody.starts_at = propsValue.starts_at;
|
||||
}
|
||||
|
||||
if (propsValue.visible_to) {
|
||||
requestBody.visible_to = propsValue.visible_to;
|
||||
}
|
||||
|
||||
const milestonesArray = (propsValue as any).workflow_milestones?.milestones_array;
|
||||
if (milestonesArray && Array.isArray(milestonesArray) && milestonesArray.length > 0) {
|
||||
requestBody.workflow_milestones = milestonesArray.map((milestone: any) => ({
|
||||
id: milestone.milestone_id,
|
||||
name: milestone.milestone_name,
|
||||
...(milestone.milestone_date && { milestone_date: milestone.milestone_date })
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${WEALTHBOX_API_BASE}/workflows`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth.secret_text,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
handleApiError('start workflow', response.status, response.body);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to start workflow: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user