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'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { WEALTHBOX_API_BASE } from './constants';
|
||||
|
||||
export const fetchUserGroups = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/user_groups`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch user groups: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.user_groups || [];
|
||||
};
|
||||
|
||||
export const fetchContacts = async (auth: string, filters?: { active?: boolean; order?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.active !== undefined) params.append('active', filters.active.toString());
|
||||
if (filters?.order) params.append('order', filters.order);
|
||||
|
||||
const url = `${WEALTHBOX_API_BASE}/contacts${params.toString() ? '?' + params.toString() : ''}`;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch contacts: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.contacts || [];
|
||||
};
|
||||
|
||||
export const fetchTags = async (auth: string, documentType?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
if (documentType) params.append('document_type', documentType);
|
||||
|
||||
const url = `${WEALTHBOX_API_BASE}/categories/tags${params.toString() ? '?' + params.toString() : ''}`;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch tags: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.tags || [];
|
||||
};
|
||||
|
||||
export const fetchUsers = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/users`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch users: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.users || [];
|
||||
};
|
||||
|
||||
export const fetchCustomFields = async (auth: string, documentType?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
if (documentType) params.append('document_type', documentType);
|
||||
|
||||
const url = `${WEALTHBOX_API_BASE}/categories/custom_fields${params.toString() ? '?' + params.toString() : ''}`;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch custom fields: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.custom_fields || [];
|
||||
};
|
||||
|
||||
export const fetchEventCategories = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/event_categories`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch event categories: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.event_categories || [];
|
||||
};
|
||||
|
||||
export const fetchHouseholds = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/contacts?type=Household&active=true`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch households: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.contacts || [];
|
||||
};
|
||||
|
||||
export const fetchOpportunityStages = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/opportunity_stages`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch opportunity stages: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.opportunity_stages || [];
|
||||
};
|
||||
|
||||
export const fetchTaskCategories = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/task_categories`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch task categories: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.task_categories || [];
|
||||
};
|
||||
|
||||
export const fetchOpportunities = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/opportunities`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch opportunities: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.opportunities || [];
|
||||
};
|
||||
|
||||
export const fetchProjects = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/projects`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch projects: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.projects || [];
|
||||
};
|
||||
|
||||
export const fetchWorkflowTemplates = async (auth: string) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${WEALTHBOX_API_BASE}/workflow_templates`,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch workflow templates: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.workflow_templates || [];
|
||||
};
|
||||
|
||||
export const fetchTasks = async (auth: string, filters?: {
|
||||
resource_id?: number;
|
||||
resource_type?: string;
|
||||
assigned_to?: number;
|
||||
completed?: boolean;
|
||||
task_type?: string;
|
||||
updated_since?: string;
|
||||
updated_before?: string;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters?.resource_id) params.append('resource_id', filters.resource_id.toString());
|
||||
if (filters?.resource_type) params.append('resource_type', filters.resource_type);
|
||||
if (filters?.assigned_to) params.append('assigned_to', filters.assigned_to.toString());
|
||||
if (filters?.completed !== undefined) params.append('completed', filters.completed.toString());
|
||||
if (filters?.task_type) params.append('task_type', filters.task_type);
|
||||
if (filters?.updated_since) params.append('updated_since', filters.updated_since);
|
||||
if (filters?.updated_before) params.append('updated_before', filters.updated_before);
|
||||
if (filters?.limit) params.append('limit', filters.limit.toString());
|
||||
|
||||
const url = `${WEALTHBOX_API_BASE}/tasks${params.toString() ? '?' + params.toString() : ''}`;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url,
|
||||
headers: {
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Failed to fetch tasks: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.tasks || [];
|
||||
};
|
||||
|
||||
export const createApiHeaders = (auth: string) => ({
|
||||
'ACCESS_TOKEN': auth,
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
export const handleApiError = (operation: string, status: number, body?: any) => {
|
||||
throw new Error(`Wealthbox API error in ${operation}: ${status} - ${JSON.stringify(body)}`);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
export const WEALTHBOX_API_BASE = 'https://api.crmworkspace.com/v1';
|
||||
|
||||
export const DOCUMENT_TYPES = {
|
||||
CONTACT_NOTE: 'ContactNote',
|
||||
CONTACT: 'Contact',
|
||||
TASK: 'Task',
|
||||
EVENT: 'Event',
|
||||
OPPORTUNITY: 'Opportunity',
|
||||
PROJECT: 'Project'
|
||||
} as const;
|
||||
|
||||
export const EVENT_STATES = {
|
||||
UNCONFIRMED: 'unconfirmed',
|
||||
CONFIRMED: 'confirmed',
|
||||
TENTATIVE: 'tentative',
|
||||
COMPLETED: 'completed',
|
||||
CANCELLED: 'cancelled'
|
||||
} as const;
|
||||
|
||||
export const OPPORTUNITY_AMOUNT_KINDS = {
|
||||
FEE: 'Fee',
|
||||
COMMISSION: 'Commission',
|
||||
AUM: 'AUM',
|
||||
OTHER: 'Other'
|
||||
} as const;
|
||||
|
||||
export const CURRENCIES = {
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
CAD: 'C$',
|
||||
AUD: 'A$'
|
||||
} as const;
|
||||
|
||||
export const TASK_PRIORITIES = {
|
||||
LOW: 'Low',
|
||||
MEDIUM: 'Medium',
|
||||
HIGH: 'High'
|
||||
} as const;
|
||||
|
||||
export const LINK_TYPES = {
|
||||
CONTACT: 'Contact',
|
||||
PROJECT: 'Project',
|
||||
OPPORTUNITY: 'Opportunity'
|
||||
} as const;
|
||||
|
||||
export const CONTACT_TYPES = {
|
||||
PERSON: 'Person',
|
||||
HOUSEHOLD: 'Household',
|
||||
ORGANIZATION: 'Organization',
|
||||
TRUST: 'Trust'
|
||||
} as const;
|
||||
|
||||
export const CONTACT_CLASSIFICATIONS = {
|
||||
CLIENT: 'Client',
|
||||
PAST_CLIENT: 'Past Client',
|
||||
PROSPECT: 'Prospect',
|
||||
VENDOR: 'Vendor',
|
||||
ORGANIZATION: 'Organization'
|
||||
} as const;
|
||||
|
||||
export const ORDER_OPTIONS = {
|
||||
RECENT: 'recent',
|
||||
CREATED: 'created',
|
||||
UPDATED: 'updated',
|
||||
ASCENDING: 'asc',
|
||||
DESCENDING: 'desc'
|
||||
} as const;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './api-helpers';
|
||||
export * from './types';
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
export interface UserGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
user: number | null;
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
id: number;
|
||||
name?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
type?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
active?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
name: string;
|
||||
type?: string;
|
||||
document_type?: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
account: number;
|
||||
excluded_from_assignments: boolean;
|
||||
}
|
||||
|
||||
export interface CustomField {
|
||||
id: number;
|
||||
name: string;
|
||||
field_type?: string;
|
||||
document_type?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: number;
|
||||
content: string;
|
||||
creator: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
linked_to: LinkedResource[];
|
||||
visible_to?: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export interface LinkedResource {
|
||||
id: number;
|
||||
type: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface ContactFilters {
|
||||
active?: boolean;
|
||||
order?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
tags?: string[];
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface TagFilters {
|
||||
document_type?: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { newTask } from './new-task';
|
||||
export { newContact } from './new-contact';
|
||||
export { newEvent } from './new-event';
|
||||
export { newOpportunity } from './new-opportunity';
|
||||
@@ -0,0 +1,323 @@
|
||||
import {
|
||||
createTrigger,
|
||||
TriggerStrategy,
|
||||
Property
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
httpClient,
|
||||
HttpMethod
|
||||
} from '@activepieces/pieces-common';
|
||||
import {
|
||||
pollingHelper,
|
||||
DedupeStrategy,
|
||||
Polling
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchUsers, fetchTags, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
const polling: Polling<any, any> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ propsValue, lastFetchEpochMS, auth }) => {
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append('limit', '100');
|
||||
|
||||
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);
|
||||
if (propsValue.assigned_to) searchParams.append('assigned_to', propsValue.assigned_to);
|
||||
|
||||
const tagsFilter = propsValue.tags_filter;
|
||||
if (tagsFilter && Array.isArray(tagsFilter) && tagsFilter.length > 0) {
|
||||
tagsFilter.forEach((tag: string) => {
|
||||
searchParams.append('tags[]', tag);
|
||||
});
|
||||
}
|
||||
|
||||
if (propsValue.active !== undefined && propsValue.active !== '') {
|
||||
searchParams.append('active', propsValue.active);
|
||||
}
|
||||
if (propsValue.include_deleted) {
|
||||
searchParams.append('deleted', 'true');
|
||||
}
|
||||
|
||||
if (lastFetchEpochMS) {
|
||||
const lastFetchDate = dayjs(lastFetchEpochMS - 1000).toISOString();
|
||||
searchParams.append('updated_since', lastFetchDate);
|
||||
}
|
||||
|
||||
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('poll new contacts', response.status, response.body);
|
||||
}
|
||||
|
||||
const contacts = response.body.contacts || [];
|
||||
|
||||
const newContacts = contacts.filter((contact: any) => {
|
||||
if (!lastFetchEpochMS) return true;
|
||||
|
||||
const contactCreatedAt = dayjs(contact.created_at).valueOf();
|
||||
return contactCreatedAt > lastFetchEpochMS;
|
||||
});
|
||||
|
||||
return newContacts.map((contact: any) => ({
|
||||
epochMilliSeconds: dayjs(contact.created_at).valueOf(),
|
||||
data: contact
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to poll new contacts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const newContact = createTrigger({
|
||||
name: 'new_contact',
|
||||
displayName: 'New Contact',
|
||||
description: 'Fires when a new contact is created',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
contact_type: Property.StaticDropdown({
|
||||
displayName: 'Contact Type',
|
||||
description: 'Only trigger for contacts of this type (optional)',
|
||||
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: 'Entity Type',
|
||||
description: 'Only trigger for contacts of this entity type (optional)',
|
||||
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: 'Only trigger for contacts with this household title (optional)',
|
||||
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' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
assigned_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Assigned To',
|
||||
description: 'Only trigger for contacts assigned to this user (optional)',
|
||||
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.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
tags_filter: Property.MultiSelectDropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Tags Filter',
|
||||
description: 'Only trigger for contacts with one of these tags (optional)',
|
||||
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: 'All Contacts', value: '' },
|
||||
{ label: 'Active Only', value: 'true' },
|
||||
{ label: 'Inactive Only', value: 'false' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
include_deleted: Property.Checkbox({
|
||||
displayName: 'Include Deleted Contacts',
|
||||
description: 'Include contacts that have been deleted',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
})
|
||||
},
|
||||
sampleData: {
|
||||
id: 1,
|
||||
creator: 1,
|
||||
created_at: '2015-05-24 10:00 AM -0400',
|
||||
updated_at: '2015-10-12 11:30 PM -0400',
|
||||
prefix: 'Mr.',
|
||||
first_name: 'Kevin',
|
||||
middle_name: 'James',
|
||||
last_name: 'Anderson',
|
||||
suffix: 'M.D.',
|
||||
nickname: 'Kev',
|
||||
job_title: 'CEO',
|
||||
twitter_name: 'kev.anderson',
|
||||
linkedin_url: 'linkedin.com/in/kanderson',
|
||||
background_information: 'Met Kevin at a conference.',
|
||||
birth_date: '1975-10-27',
|
||||
anniversary: '1998-11-29',
|
||||
client_since: '2002-05-21',
|
||||
assigned_to: 1,
|
||||
referred_by: 1,
|
||||
type: 'Person',
|
||||
gender: 'Male',
|
||||
contact_source: 'Referral',
|
||||
contact_type: 'Client',
|
||||
status: 'Active',
|
||||
marital_status: 'Married',
|
||||
important_information: 'Has 3 kids in college',
|
||||
personal_interests: 'Skiing: Downhill, Traveling',
|
||||
investment_objective: 'Income',
|
||||
time_horizon: 'Intermediate',
|
||||
risk_tolerance: 'Moderate',
|
||||
company_name: 'Acme Co.',
|
||||
tags: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Clients'
|
||||
}
|
||||
],
|
||||
street_addresses: [
|
||||
{
|
||||
street_line_1: '155 12th Ave.',
|
||||
street_line_2: 'Apt 3B',
|
||||
city: 'New York',
|
||||
state: 'New York',
|
||||
zip_code: '10001',
|
||||
country: 'United States',
|
||||
principal: true,
|
||||
kind: 'Work',
|
||||
id: 1,
|
||||
address: '155 12th Ave., Apt 3B, New York, New York 10001, United States'
|
||||
}
|
||||
],
|
||||
email_addresses: [
|
||||
{
|
||||
id: 1,
|
||||
address: 'kevin.anderson@example.com',
|
||||
principal: true,
|
||||
kind: 'Work'
|
||||
}
|
||||
],
|
||||
phone_numbers: [
|
||||
{
|
||||
id: 1,
|
||||
address: '(555) 555-5555',
|
||||
principal: true,
|
||||
extension: '77',
|
||||
kind: 'Work'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onEnable: async (context) => {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
onDisable: async (context) => {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
run: async (context) => {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
|
||||
test: async (context) => {
|
||||
return await pollingHelper.test(polling, context);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
import {
|
||||
createTrigger,
|
||||
TriggerStrategy,
|
||||
Property
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
httpClient,
|
||||
HttpMethod
|
||||
} from '@activepieces/pieces-common';
|
||||
import {
|
||||
pollingHelper,
|
||||
DedupeStrategy,
|
||||
Polling
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchContacts, fetchProjects, fetchOpportunities, fetchEventCategories, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
const polling: Polling<any, any> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ propsValue, lastFetchEpochMS, auth }) => {
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append('limit', '100');
|
||||
|
||||
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.event_category) searchParams.append('event_category', propsValue.event_category);
|
||||
|
||||
if (propsValue.start_date_min) searchParams.append('start_date_min', dayjs(propsValue.start_date_min).toISOString());
|
||||
if (propsValue.start_date_max) searchParams.append('start_date_max', dayjs(propsValue.start_date_max).toISOString());
|
||||
|
||||
searchParams.append('order', 'created');
|
||||
|
||||
if (lastFetchEpochMS) {
|
||||
const lastFetchDate = dayjs(lastFetchEpochMS - 1000).toISOString();
|
||||
searchParams.append('updated_since', lastFetchDate);
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = queryString ? `${WEALTHBOX_API_BASE}/events?${queryString}` : `${WEALTHBOX_API_BASE}/events`;
|
||||
|
||||
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('poll new events', response.status, response.body);
|
||||
}
|
||||
|
||||
const events = response.body.events || [];
|
||||
|
||||
const newEvents = events.filter((event: any) => {
|
||||
if (!lastFetchEpochMS) return true;
|
||||
|
||||
const eventCreatedAt = dayjs(event.created_at).valueOf();
|
||||
return eventCreatedAt > lastFetchEpochMS;
|
||||
});
|
||||
|
||||
return newEvents.map((event: any) => ({
|
||||
epochMilliSeconds: dayjs(event.created_at).valueOf(),
|
||||
data: event
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to poll new events: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const newEvent = createTrigger({
|
||||
name: 'new_event',
|
||||
displayName: 'New Event',
|
||||
description: 'Fires when a new event is created',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
resource_type: Property.StaticDropdown({
|
||||
displayName: 'Linked Resource Type',
|
||||
description: 'Only trigger for events linked to this type of resource (optional)',
|
||||
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 events 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 events by`,
|
||||
required: false,
|
||||
options: {
|
||||
options: recordOptions
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch resource options for validation:', error);
|
||||
return {
|
||||
resource_id: Property.Number({
|
||||
displayName: 'Resource ID',
|
||||
description: 'Enter the resource ID manually (API unavailable)',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
event_category: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Event Category',
|
||||
description: 'Only trigger for events of this category (optional)',
|
||||
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 || `Category ${category.id}`,
|
||||
value: category.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load event categories. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
start_date_min: Property.DateTime({
|
||||
displayName: 'Start Date Minimum',
|
||||
description: 'Only trigger for events starting on or after this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
start_date_max: Property.DateTime({
|
||||
displayName: 'Start Date Maximum',
|
||||
description: 'Only trigger for events starting on or before this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
order: Property.StaticDropdown({
|
||||
displayName: 'Sort Order',
|
||||
description: 'How to order the events',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Recent (newest first)', value: 'recent' },
|
||||
{ label: 'Created Date (newest first)', value: 'created' },
|
||||
{ label: 'Start Date (ascending)', value: 'asc' },
|
||||
{ label: 'Start Date (descending)', value: 'desc' }
|
||||
]
|
||||
}
|
||||
})
|
||||
},
|
||||
sampleData: {
|
||||
id: 1,
|
||||
creator: 1,
|
||||
created_at: '2015-05-24 10:00 AM -0400',
|
||||
updated_at: '2015-10-12 11:30 PM -0400',
|
||||
title: 'Client Meeting',
|
||||
starts_at: '2015-05-24 10:00 AM -0400',
|
||||
ends_at: '2015-05-24 11:00 AM -0400',
|
||||
repeats: true,
|
||||
event_category: 2,
|
||||
all_day: true,
|
||||
location: 'Conference Room',
|
||||
description: 'Review meeting for Kevin...',
|
||||
state: 'confirmed',
|
||||
visible_to: 'Everyone',
|
||||
email_invitees: true,
|
||||
linked_to: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'Contact',
|
||||
name: 'Kevin Anderson'
|
||||
}
|
||||
],
|
||||
invitees: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'Contact',
|
||||
name: 'Kevin Anderson'
|
||||
}
|
||||
],
|
||||
custom_fields: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'My Field',
|
||||
value: '123456789',
|
||||
document_type: 'Contact',
|
||||
field_type: 'single_select'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onEnable: async (context) => {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
onDisable: async (context) => {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
run: async (context) => {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
|
||||
test: async (context) => {
|
||||
return await pollingHelper.test(polling, context);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,325 @@
|
||||
import {
|
||||
createTrigger,
|
||||
TriggerStrategy,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
httpClient,
|
||||
HttpMethod
|
||||
} from '@activepieces/pieces-common';
|
||||
import {
|
||||
pollingHelper,
|
||||
DedupeStrategy,
|
||||
Polling
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchUsers, fetchContacts, fetchProjects, fetchOpportunities, fetchOpportunityStages, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
const polling: Polling<any, any> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ propsValue, lastFetchEpochMS, auth }) => {
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append('limit', '100');
|
||||
|
||||
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.stage) searchParams.append('stage', propsValue.stage);
|
||||
if (propsValue.manager) searchParams.append('manager', propsValue.manager);
|
||||
|
||||
if (propsValue.include_closed) {
|
||||
searchParams.append('include_closed', 'true');
|
||||
}
|
||||
|
||||
if (propsValue.min_probability !== undefined) searchParams.append('min_probability', propsValue.min_probability.toString());
|
||||
if (propsValue.max_probability !== undefined) searchParams.append('max_probability', propsValue.max_probability.toString());
|
||||
|
||||
if (propsValue.target_close_after) searchParams.append('target_close_after', dayjs(propsValue.target_close_after).toISOString());
|
||||
if (propsValue.target_close_before) searchParams.append('target_close_before', dayjs(propsValue.target_close_before).toISOString());
|
||||
|
||||
searchParams.append('order', 'created');
|
||||
|
||||
if (lastFetchEpochMS) {
|
||||
const lastFetchDate = dayjs(lastFetchEpochMS - 1000).toISOString();
|
||||
searchParams.append('updated_since', lastFetchDate);
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = queryString ? `${WEALTHBOX_API_BASE}/opportunities?${queryString}` : `${WEALTHBOX_API_BASE}/opportunities`;
|
||||
|
||||
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('poll new opportunities', response.status, response.body);
|
||||
}
|
||||
|
||||
const opportunities = response.body.opportunities || [];
|
||||
|
||||
const newOpportunities = opportunities.filter((opportunity: any) => {
|
||||
if (!lastFetchEpochMS) return true;
|
||||
|
||||
const opportunityCreatedAt = dayjs(opportunity.created_at).valueOf();
|
||||
return opportunityCreatedAt > lastFetchEpochMS;
|
||||
});
|
||||
|
||||
return newOpportunities.map((opportunity: any) => ({
|
||||
epochMilliSeconds: dayjs(opportunity.created_at).valueOf(),
|
||||
data: opportunity
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to poll new opportunities: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const newOpportunity = createTrigger({
|
||||
name: 'new_opportunity',
|
||||
displayName: 'New Opportunity',
|
||||
description: 'Fires when a new opportunity is created',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
resource_type: Property.StaticDropdown({
|
||||
displayName: 'Linked Resource Type',
|
||||
description: 'Only trigger for opportunities linked to this type of resource (optional)',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Contact', value: 'Contact' },
|
||||
{ label: 'Project', value: 'Project' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
resource_record: Property.DynamicProperties({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Linked Resource',
|
||||
description: 'Select the specific resource to filter opportunities 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;
|
||||
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 opportunities by`,
|
||||
required: false,
|
||||
options: {
|
||||
options: recordOptions
|
||||
}
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading resource records:', error);
|
||||
return {
|
||||
resource_id: Property.Number({
|
||||
displayName: 'Resource ID',
|
||||
description: 'Enter the resource ID manually (API unavailable)',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
stage: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Opportunity Stage',
|
||||
description: 'Only trigger for opportunities in this stage (optional)',
|
||||
required: false,
|
||||
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 || `Stage ${stage.id}`,
|
||||
value: stage.id
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
options: [],
|
||||
error: 'Failed to load opportunity stages. Please check your authentication.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
manager: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Opportunity Manager',
|
||||
description: 'Only trigger for opportunities managed by this user (optional)',
|
||||
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.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
include_closed: Property.Checkbox({
|
||||
displayName: 'Include Closed Opportunities',
|
||||
description: 'Include won and lost opportunities in results',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
}),
|
||||
|
||||
min_probability: Property.Number({
|
||||
displayName: 'Minimum Probability (%)',
|
||||
description: 'Only trigger for opportunities with probability at or above this percentage (0-100)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
max_probability: Property.Number({
|
||||
displayName: 'Maximum Probability (%)',
|
||||
description: 'Only trigger for opportunities with probability at or below this percentage (0-100)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
target_close_after: Property.DateTime({
|
||||
displayName: 'Target Close After',
|
||||
description: 'Only trigger for opportunities with target close date on or after this date/time',
|
||||
required: false
|
||||
}),
|
||||
|
||||
target_close_before: Property.DateTime({
|
||||
displayName: 'Target Close Before',
|
||||
description: 'Only trigger for opportunities with target close date on or before this date/time',
|
||||
required: false
|
||||
})
|
||||
},
|
||||
sampleData: {
|
||||
id: 1,
|
||||
creator: 1,
|
||||
created_at: '2015-05-24 10:00 AM -0400',
|
||||
updated_at: '2015-10-12 11:30 PM -0400',
|
||||
name: 'Financial Plan',
|
||||
description: 'Opportunity to plan for...',
|
||||
target_close: '2015-11-12 11:00 AM -0500',
|
||||
probability: 70,
|
||||
stage: 1,
|
||||
manager: 1,
|
||||
amounts: [
|
||||
{
|
||||
amount: 56.76,
|
||||
currency: '$',
|
||||
kind: 'Fee'
|
||||
}
|
||||
],
|
||||
linked_to: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'Contact',
|
||||
name: 'Kevin Anderson'
|
||||
}
|
||||
],
|
||||
visible_to: 'Everyone',
|
||||
custom_fields: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'My Field',
|
||||
value: '123456789',
|
||||
document_type: 'Contact',
|
||||
field_type: 'single_select'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onEnable: async (context) => {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
onDisable: async (context) => {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
run: async (context) => {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
|
||||
test: async (context) => {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
import {
|
||||
createTrigger,
|
||||
TriggerStrategy,
|
||||
Property
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
httpClient,
|
||||
HttpMethod
|
||||
} from '@activepieces/pieces-common';
|
||||
import {
|
||||
pollingHelper,
|
||||
DedupeStrategy,
|
||||
Polling
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchUsers, fetchContacts, fetchProjects, fetchOpportunities, WEALTHBOX_API_BASE, handleApiError } from '../common';
|
||||
import { wealthboxAuth } from '../..';
|
||||
|
||||
const polling: Polling<any, any> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ propsValue, lastFetchEpochMS, auth }) => {
|
||||
if (!auth) {
|
||||
throw new Error('Authentication is required');
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append('limit', '100');
|
||||
|
||||
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.task_type && propsValue.task_type !== 'all') searchParams.append('task_type', propsValue.task_type);
|
||||
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 (lastFetchEpochMS) {
|
||||
const lastFetchDate = dayjs(lastFetchEpochMS - 1000).toISOString();
|
||||
searchParams.append('updated_since', lastFetchDate);
|
||||
}
|
||||
|
||||
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('poll new tasks', response.status, response.body);
|
||||
}
|
||||
|
||||
const tasks = response.body.tasks || [];
|
||||
|
||||
const newTasks = tasks.filter((task: any) => {
|
||||
if (!lastFetchEpochMS) return true;
|
||||
|
||||
const taskCreatedAt = dayjs(task.created_at).valueOf();
|
||||
return taskCreatedAt > lastFetchEpochMS;
|
||||
});
|
||||
|
||||
return newTasks.map((task: any) => ({
|
||||
epochMilliSeconds: dayjs(task.created_at).valueOf(),
|
||||
data: task
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to poll new tasks: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const newTask = createTrigger({
|
||||
name: 'new_task',
|
||||
displayName: 'New Task',
|
||||
description: 'Fires when a new task is created',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
assigned_to: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Assigned To',
|
||||
description: 'Only trigger for tasks assigned to this user (optional)',
|
||||
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: 'Only trigger for tasks assigned to this team (optional)',
|
||||
required: false
|
||||
}),
|
||||
|
||||
created_by: Property.Dropdown({
|
||||
auth: wealthboxAuth,
|
||||
displayName: 'Created By',
|
||||
description: 'Only trigger for tasks created by this user (optional)',
|
||||
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.'
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
task_type: Property.StaticDropdown({
|
||||
displayName: 'Task Type',
|
||||
description: 'Type of tasks to monitor',
|
||||
required: false,
|
||||
defaultValue: 'all',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'All Tasks', value: 'all' },
|
||||
{ label: 'Parent Tasks Only', value: 'parents' },
|
||||
{ label: 'Subtasks Only', value: 'subtasks' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
resource_type: Property.StaticDropdown({
|
||||
displayName: 'Linked Resource Type',
|
||||
description: 'Only trigger for tasks linked to this type of resource (optional)',
|
||||
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) {
|
||||
console.warn('Could not fetch resource options for validation:', error);
|
||||
return {
|
||||
resource_id: Property.Number({
|
||||
displayName: 'Resource ID',
|
||||
description: 'Enter the resource ID manually (API unavailable)',
|
||||
required: false
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
sampleData: {
|
||||
id: 1,
|
||||
creator: 1,
|
||||
created_at: '2015-05-24 10:00 AM -0400',
|
||||
updated_at: '2015-10-12 11:30 PM -0400',
|
||||
name: "Return Bill's call",
|
||||
due_date: '2015-05-24 11:00 AM -0400',
|
||||
complete: false,
|
||||
category: 1,
|
||||
linked_to: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'Contact',
|
||||
name: 'Kevin Anderson'
|
||||
}
|
||||
],
|
||||
priority: 'Medium',
|
||||
visible_to: 'Everyone',
|
||||
description: 'Follow up from message...',
|
||||
assigned_to: 1,
|
||||
assigned_to_team: 10
|
||||
},
|
||||
|
||||
onEnable: async (context) => {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
onDisable: async (context) => {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth
|
||||
});
|
||||
},
|
||||
|
||||
run: async (context) => {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
|
||||
test: async (context) => {
|
||||
return await pollingHelper.test(polling, context);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user