diff --git a/frontend-dev/src/components/AllIntegrations/EditInteg.jsx b/frontend-dev/src/components/AllIntegrations/EditInteg.jsx
index 4b1de084..7415811f 100644
--- a/frontend-dev/src/components/AllIntegrations/EditInteg.jsx
+++ b/frontend-dev/src/components/AllIntegrations/EditInteg.jsx
@@ -92,6 +92,7 @@ const EditMailup = lazy(() => import('./Mailup/EditMailup'))
const EditNotion = lazy(() => import('./Notion/EditNotion'))
const EditMailjet = lazy(() => import('./Mailjet/EditMailjet'))
const EditSendGrid = lazy(() => import('./SendGrid/EditSendGrid'))
+const EditFabman = lazy(() => import('./Fabman/EditFabman'))
const EditPCloud = lazy(() => import('./PCloud/EditPCloud'))
const EditEmailOctopus = lazy(() => import('./EmailOctopus/EditEmailOctopus'))
const EditCustomAction = lazy(() => import('./CustomAction/EditCustomAction'))
@@ -411,6 +412,8 @@ const IntegType = memo(({ allIntegURL, flow }) => {
return
case 'SendGrid':
return
+ case 'Fabman':
+ return
case 'PCloud':
return
case 'EmailOctopus':
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/EditFabman.jsx b/frontend-dev/src/components/AllIntegrations/Fabman/EditFabman.jsx
new file mode 100644
index 00000000..1ee09629
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/EditFabman.jsx
@@ -0,0 +1,157 @@
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-param-reassign */
+
+import { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useRecoilState, useRecoilValue } from 'recoil'
+import { $actionConf, $formFields, $newFlow } from '../../../GlobalStates'
+import { __ } from '../../../Utils/i18nwrap'
+import SnackMsg from '../../Utilities/SnackMsg'
+import EditFormInteg from '../EditFormInteg'
+import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents'
+import EditWebhookInteg from '../EditWebhookInteg'
+import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers'
+import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree'
+import { checkMappedFields, handleInput, isConfigInvalid } from './FabmanCommonFunc'
+import FabmanIntegLayout from './FabmanIntegLayout'
+import { checkValidEmail } from '../../../Utils/Helpers'
+
+function EditFabman({ allIntegURL }) {
+ const navigate = useNavigate()
+ const [flow, setFlow] = useRecoilState($newFlow)
+ const [fabmanConf, setFabmanConf] = useRecoilState($actionConf)
+ const [loading, setLoading] = useState({
+ list: false,
+ field: false,
+ auth: false
+ })
+ const [snack, setSnackbar] = useState({ show: false })
+ const formField = useRecoilValue($formFields)
+
+ const [localName, setLocalName] = useState(fabmanConf.name || '')
+
+ const getEmailMappingRow = () => {
+ const rows = Array.isArray(fabmanConf?.field_map) ? fabmanConf.field_map : []
+ return rows.find(r => r?.fabmanFormField === 'emailAddress')
+ }
+
+ const isEmailMappingInvalid = () => {
+ const emailRow = getEmailMappingRow()
+ if (!emailRow) return true
+ if (emailRow.formField === 'custom') {
+ const customValue = (emailRow.customValue || '').trim()
+ return !customValue || !checkValidEmail(customValue)
+ }
+ // Non-custom: a form field is selected. Consider valid if:
+ // - field type is email, OR
+ // - type is missing/unknown, OR
+ // - field name/label includes "email"
+ const selectedField = (formField || []).find(f => f.name === emailRow.formField)
+
+ if (!selectedField) return false
+
+ const hasEmailType = selectedField.type && String(selectedField.type).toLowerCase() === 'email'
+ const looksLikeEmailField =
+ /email/i.test(selectedField.name || '') || /email/i.test(selectedField.label || '')
+ return !(hasEmailType || !selectedField.type || looksLikeEmailField)
+ }
+
+ const saveConfig = () => {
+ if (!fabmanConf.actionName) {
+ setSnackbar({ show: true, msg: __('Please select an action', 'bit-integrations') })
+ return
+ }
+
+ if (['update_member', 'delete_member'].includes(fabmanConf.actionName)) {
+ if (!checkMappedFields(fabmanConf)) {
+ setSnackbar({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') })
+ return
+ }
+
+ if (isEmailMappingInvalid()) {
+ setSnackbar({ show: true, msg: __('Please map a valid email address', 'bit-integrations') })
+ return
+ }
+
+ saveActionConf({
+ flow,
+ allIntegURL,
+ conf: fabmanConf,
+ navigate,
+ edit: 1,
+ setLoading,
+ setSnackbar
+ })
+ return
+ }
+
+ if (!checkMappedFields(fabmanConf)) {
+ setSnackbar({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') })
+ return
+ }
+
+ const requiresWorkspaceSelection =
+ fabmanConf.actionName === 'update_spaces' || fabmanConf.actionName === 'delete_spaces'
+
+ if (requiresWorkspaceSelection && !fabmanConf.selectedWorkspace) {
+ setSnackbar({ show: true, msg: __('Please select a workspace', 'bit-integrations') })
+ return
+ }
+
+ saveActionConf({
+ flow,
+ allIntegURL,
+ conf: fabmanConf,
+ navigate,
+ edit: 1,
+ setLoading,
+ setSnackbar
+ })
+ }
+
+ const handleNameChange = e => {
+ setLocalName(e.target.value)
+ setFabmanConf(prev => ({ ...prev, name: e.target.value }))
+ }
+
+ return (
+
+
+
+ {__('Integration Name:', 'bit-integrations')}
+
+
+
+
+
handleInput(e, fabmanConf, setFabmanConf, setLoading, setSnackbar)}
+ fabmanConf={fabmanConf}
+ setFabmanConf={setFabmanConf}
+ loading={loading}
+ setLoading={setLoading}
+ setSnackbar={setSnackbar}
+ />
+
+
+
+ )
+}
+
+export default EditFabman
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/Fabman.jsx b/frontend-dev/src/components/AllIntegrations/Fabman/Fabman.jsx
new file mode 100644
index 00000000..30daf572
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/Fabman.jsx
@@ -0,0 +1,323 @@
+/* eslint-disable no-console */
+/* eslint-disable no-unused-expressions */
+import { useEffect, useState } from 'react'
+import 'react-multiple-select-dropdown-lite/dist/index.css'
+import { useNavigate } from 'react-router-dom'
+import toast from 'react-hot-toast'
+import { __ } from '../../../Utils/i18nwrap'
+import SnackMsg from '../../Utilities/SnackMsg'
+import Steps from '../../Utilities/Steps'
+import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers'
+import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree'
+import FabmanAuthorization from './FabmanAuthorization'
+import {
+ checkMappedFields,
+ hasEmailFieldMapped,
+ getEmailMappingRow,
+ isEmailMappingInvalid
+} from './FabmanCommonFunc'
+import FabmanIntegLayout from './FabmanIntegLayout'
+import { checkValidEmail } from '../../../Utils/Helpers'
+
+export default function Fabman({ formFields, setFlow, flow, allIntegURL }) {
+ const navigate = useNavigate()
+ const [isLoading, setIsLoading] = useState(false)
+ const [loading, setLoading] = useState({ auth: false, workspaces: false, members: false })
+ const [step, setStep] = useState(1)
+ const [snack, setSnack] = useState({ show: false })
+
+ const [fabmanConf, setFabmanConf] = useState({
+ name: 'Fabman',
+ type: 'Fabman',
+ field_map: [{ formField: '', fabmanFormField: '' }],
+ customFields: [],
+ actions: {},
+ condition: {
+ action_behavior: '',
+ actions: [{ field: '', action: 'value' }],
+ logics: [{ field: '', logic: '', val: '' }, 'or', { field: '', logic: '', val: '' }]
+ },
+ trigger_type: '',
+ pro_integ_v: '2.5.4',
+ fields: [],
+ accountId: '',
+ selectedLockVersion: '',
+ memberStaticFields,
+ spacesStaticFields
+ })
+
+ const isConfigInvalid = () => {
+ if (!fabmanConf.actionName) return true
+
+ if (
+ !['delete_member', 'delete_spaces'].includes(fabmanConf.actionName) &&
+ !checkMappedFields(fabmanConf)
+ )
+ return true
+
+ if (
+ ['update_member', 'delete_member'].includes(fabmanConf.actionName) &&
+ isEmailMappingInvalid(fabmanConf, formFields, checkValidEmail)
+ )
+ return true
+
+ if (
+ ['create_member', 'update_member', 'update_spaces', 'delete_spaces'].includes(
+ fabmanConf.actionName
+ ) &&
+ !fabmanConf.selectedWorkspace
+ )
+ return true
+
+ if (fabmanConf.actionName === 'delete_member') {
+ const hasEmailField = fabmanConf.field_map?.some(
+ field => field.fabmanFormField === 'emailAddress' && field.formField
+ )
+
+ if (!hasEmailField) {
+ return true
+ }
+ }
+ return false
+ }
+
+ const saveConfig = () => {
+ if (isConfigInvalid()) {
+ if (!fabmanConf.actionName) {
+ setSnack({ show: true, msg: __('Please select an action', 'bit-integrations') })
+ } else if (
+ !['delete_member', 'delete_spaces'].includes(fabmanConf.actionName) &&
+ !checkMappedFields(fabmanConf)
+ ) {
+ setSnack({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') })
+ } else if (
+ ['update_member', 'delete_member'].includes(fabmanConf.actionName) &&
+ isEmailMappingInvalid(fabmanConf, formFields, checkValidEmail)
+ ) {
+ setSnack({ show: true, msg: __('Please map a valid email address', 'bit-integrations') })
+ } else if (
+ ['create_member', 'update_member', 'update_spaces', 'delete_spaces'].includes(
+ fabmanConf.actionName
+ ) &&
+ !fabmanConf.selectedWorkspace
+ ) {
+ setSnack({ show: true, msg: __('Please select a workspace', 'bit-integrations') })
+ } else if (fabmanConf.actionName === 'delete_member') {
+ if (!hasEmailFieldMapped(fabmanConf)) {
+ setSnack({
+ show: true,
+ msg: __('Please map email field for member lookup', 'bit-integrations')
+ })
+ }
+ }
+ return
+ }
+ saveIntegConfig(flow, setFlow, allIntegURL, fabmanConf, navigate, '', '', setIsLoading)
+ }
+
+ const nextPage = () => {
+ if (isConfigInvalid()) {
+ if (!fabmanConf.actionName) {
+ setSnack({ show: true, msg: __('Please select an action', 'bit-integrations') })
+ } else if (
+ !['delete_member', 'delete_spaces'].includes(fabmanConf.actionName) &&
+ !checkMappedFields(fabmanConf)
+ ) {
+ setSnack({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') })
+ } else if (
+ ['update_member', 'delete_member'].includes(fabmanConf.actionName) &&
+ isEmailMappingInvalid(fabmanConf, formFields, checkValidEmail)
+ ) {
+ setSnack({ show: true, msg: __('Please map a valid email address', 'bit-integrations') })
+ } else if (
+ ['create_member', 'update_member', 'update_spaces', 'delete_spaces'].includes(
+ fabmanConf.actionName
+ ) &&
+ !fabmanConf.selectedWorkspace
+ ) {
+ setSnack({ show: true, msg: __('Please select a workspace', 'bit-integrations') })
+ } else if (fabmanConf.actionName === 'delete_member') {
+ if (!hasEmailFieldMapped(fabmanConf)) {
+ setSnack({
+ show: true,
+ msg: __('Please map email field for member lookup', 'bit-integrations')
+ })
+ }
+ }
+ return
+ }
+ setStep(3)
+ }
+
+ return (
+
+
+
+
+
+ {/* STEP 1 */}
+
+ {/* STEP 2 */}
+ {step === 2 && (
+
+
+
+
+ )}
+ {/* STEP 3 */}
+
+ saveIntegConfig(flow, setFlow, allIntegURL, fabmanConf, navigate, '', '', setIsLoading)
+ }
+ isLoading={isLoading}
+ dataConf={fabmanConf}
+ setDataConf={setFabmanConf}
+ formFields={formFields}
+ />
+
+ )
+}
+
+const memberStaticFields = [
+ { key: 'emailAddress', label: __('Email Address', 'bit-integrations'), required: false },
+ { key: 'firstName', label: __('First Name', 'bit-integrations'), required: true },
+ { key: 'lastName', label: __('Last Name', 'bit-integrations'), required: false },
+ { key: 'memberNumber', label: __('Member Number', 'bit-integrations'), required: false },
+ { key: 'gender', label: __('Gender', 'bit-integrations'), required: false },
+ { key: 'dateOfBirth', label: __('Date of Birth', 'bit-integrations'), required: false },
+ { key: 'company', label: __('Company', 'bit-integrations'), required: false },
+ { key: 'phone', label: __('Phone', 'bit-integrations'), required: false },
+ { key: 'address', label: __('Address', 'bit-integrations'), required: false },
+ { key: 'address2', label: __('Address 2', 'bit-integrations'), required: false },
+ { key: 'city', label: __('City', 'bit-integrations'), required: false },
+ { key: 'zip', label: __('ZIP Code', 'bit-integrations'), required: false },
+ { key: 'countryCode', label: __('Country Code', 'bit-integrations'), required: false },
+ { key: 'region', label: __('Region', 'bit-integrations'), required: false },
+ { key: 'notes', label: __('Notes', 'bit-integrations'), required: false },
+ { key: 'billingFirstName', label: __('Billing First Name', 'bit-integrations'), required: false },
+ { key: 'billingLastName', label: __('Billing Last Name', 'bit-integrations'), required: false },
+ { key: 'billingCompany', label: __('Billing Company', 'bit-integrations'), required: false },
+ { key: 'billingAddress', label: __('Billing Address', 'bit-integrations'), required: false },
+ { key: 'billingAddress2', label: __('Billing Address 2', 'bit-integrations'), required: false },
+ { key: 'billingCity', label: __('Billing City', 'bit-integrations'), required: false },
+ { key: 'billingZip', label: __('Billing ZIP Code', 'bit-integrations'), required: false },
+ {
+ key: 'billingCountryCode',
+ label: __('Billing Country Code', 'bit-integrations'),
+ required: false
+ },
+ { key: 'billingRegion', label: __('Billing Region', 'bit-integrations'), required: false },
+ {
+ key: 'billingInvoiceText',
+ label: __('Billing Invoice Text', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'billingEmailAddress',
+ label: __('Billing Email Address', 'bit-integrations'),
+ required: false
+ },
+ { key: 'language', label: __('Language', 'bit-integrations'), required: false },
+ { key: 'state', label: __('State', 'bit-integrations'), required: false },
+ { key: 'taxExempt', label: __('Tax Exempt', 'bit-integrations'), required: false },
+ {
+ key: 'hasBillingAddress',
+ label: __('Has Billing Address', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'requireUpfrontPayment',
+ label: __('Require Upfront Payment', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'upfrontMinimumBalance',
+ label: __('Upfront Minimum Balance', 'bit-integrations'),
+ required: false
+ }
+]
+
+const spacesStaticFields = [
+ { key: 'name', label: __('Name', 'bit-integrations'), required: true },
+ { key: 'shortName', label: __('Short Name', 'bit-integrations'), required: false },
+ { key: 'timezone', label: __('Timezone', 'bit-integrations'), required: true },
+ { key: 'emailAddress', label: __('Email Address', 'bit-integrations'), required: false },
+ { key: 'website', label: __('Website', 'bit-integrations'), required: false },
+ { key: 'phone', label: __('Phone', 'bit-integrations'), required: false },
+ { key: 'infoText', label: __('Info Text', 'bit-integrations'), required: false },
+ {
+ key: 'bookingTermsOfService',
+ label: __('Booking Terms Of Service', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingSlotsPerHour',
+ label: __('Booking Slots Per Hour', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingWindowMinHours',
+ label: __('Booking Window Min Hours', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingWindowMaxDays',
+ label: __('Booking Window Max Days', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingLockInHours',
+ label: __('Booking Lock-in Hours', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingMaxMinutesPerMemberDay',
+ label: __('Booking Max Minutes Per Member/Day', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingMaxMinutesPerMemberWeek',
+ label: __('Booking Max Minutes Per Member/Week', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingExclusiveMinutes',
+ label: __('Booking Exclusive Minutes', 'bit-integrations'),
+ required: false
+ },
+ {
+ key: 'bookingOpeningHours',
+ label: __('Booking Opening Hours', 'bit-integrations'),
+ required: false
+ },
+ { key: 'bookingRefundable', label: __('Booking Refundable', 'bit-integrations'), required: false },
+ {
+ key: 'bookingNamesPublic',
+ label: __('Booking Names Public', 'bit-integrations'),
+ required: false
+ }
+]
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/FabmanAuthorization.jsx b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanAuthorization.jsx
new file mode 100644
index 00000000..c22f82fe
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanAuthorization.jsx
@@ -0,0 +1,124 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+/* eslint-disable no-unused-expressions */
+import { useState, useCallback, useEffect } from 'react'
+import { __, sprintf } from '../../../Utils/i18nwrap'
+import LoaderSm from '../../Loaders/LoaderSm'
+import { fabmanAuthentication } from './FabmanCommonFunc'
+import Note from '../../Utilities/Note'
+import tutorialLinks from '../../../Utils/StaticData/tutorialLinks'
+import TutorialLink from '../../Utilities/TutorialLink'
+
+export default function FabmanAuthorization({
+ fabmanConf,
+ setFabmanConf,
+ step,
+ setStep,
+ loading,
+ setLoading,
+ isInfo
+}) {
+ const [isAuthorized, setIsAuthorized] = useState(false)
+ const [error, setError] = useState({ name: '', apiKey: '' })
+ const { fabman } = tutorialLinks
+
+ const nextPage = useCallback(() => {
+ setTimeout(() => {
+ document.getElementById('btcd-settings-wrp').scrollTop = 0
+ }, 300)
+ setStep(2)
+ }, [setStep])
+
+ const handleInput = useCallback(
+ e => {
+ const { name, value } = e.target
+ setFabmanConf(prev => ({ ...prev, [name]: value }))
+ setError(prev => ({ ...prev, [name]: '' }))
+ },
+ [setFabmanConf, setError]
+ )
+
+ const handleNameBlur = useCallback(() => {}, [setFabmanConf])
+
+ const styleStep1 = step === 1 ? { width: 900, height: 'auto' } : {}
+
+ return (
+
+ {fabman?.youTubeLink &&
}
+ {fabman?.docLink &&
}
+
+ {__('Integration Name:', 'bit-integrations')}
+
+
+
+ {__('API Key:', 'bit-integrations')}
+
+
+
+ {error.apiKey}
+
+
+ {!isInfo && (
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+const fabmanApiKeyNote = `${__('To get your Fabman API key:', 'bit-integrations')}
+
+ - ${sprintf(
+ __('Log in to your %s.', 'bit-integrations'),
+ 'Fabman account'
+ )}
+ - ${__('Go to "Configure" → "Integrations (API & Webhooks)".', 'bit-integrations')}
+ - ${__('Click "Create API key", add a title, and choose a member.', 'bit-integrations')}
+ - ${__('Save, then click "Reveal" to copy your API key.', 'bit-integrations')}
+
`
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/FabmanCommonFunc.js b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanCommonFunc.js
new file mode 100644
index 00000000..99385bfa
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanCommonFunc.js
@@ -0,0 +1,176 @@
+/* eslint-disable no-console */
+/* eslint-disable no-else-return */
+import toast from 'react-hot-toast'
+import bitsFetch from '../../../Utils/bitsFetch'
+import { __ } from '../../../Utils/i18nwrap'
+import { create } from 'mutative'
+
+export const handleInput = (e, fabmanConf, setFabmanConf) => {
+ const newConf = { ...fabmanConf }
+ const { name } = e.target
+
+ if (e.target.value !== '') {
+ newConf[name] = e.target.value
+ } else {
+ delete newConf[name]
+ }
+
+ setFabmanConf({ ...newConf })
+}
+
+export const generateMappedField = fabmanConf => {
+ // The double exclamation mark (!!) is used to convert any value to a boolean.
+ // This ensures only fields with a truthy 'required' property are included.
+ const requiredFlds = fabmanConf?.staticFields.filter(fld => !!fld.required)
+ return requiredFlds.length > 0
+ ? requiredFlds.map(field => ({ formField: '', fabmanFormField: field.key }))
+ : [{ formField: '', fabmanFormField: '' }]
+}
+
+export const checkMappedFields = fabmanConf => {
+ const rows = Array.isArray(fabmanConf?.field_map) ? fabmanConf.field_map : []
+ const mappedFieldPresent = rows.filter(r => {
+ const hasAnySide =
+ (r?.formField && r.formField !== '') || (r?.fabmanFormField && r.fabmanFormField !== '')
+
+ if (!hasAnySide) return false
+
+ if (!r.formField || !r.fabmanFormField) return true
+
+ if (r.formField === 'custom' && !r.customValue) return true
+ return false
+ })
+ return mappedFieldPresent.length === 0
+}
+
+export const fabmanAuthentication = (
+ confTmp,
+ setConf,
+ setError,
+ setIsAuthorized,
+ loading,
+ setLoading,
+ type
+) => {
+ if (!confTmp.apiKey) {
+ setError({ apiKey: !confTmp.apiKey ? __("API key can't be empty", 'bit-integrations') : '' })
+ return
+ }
+
+ setError({})
+
+ if (type === 'authentication') {
+ setLoading({ ...loading, auth: true })
+ }
+
+ const requestParams = { apiKey: confTmp.apiKey }
+
+ bitsFetch(requestParams, 'fabman_authorization').then(result => {
+ if (result && result.success) {
+ // Use mutative's produce for state update
+ const newConf = create(confTmp, draft => {
+ if (type === 'authentication' && result.data && result.data.accountId) {
+ draft.accountId = result.data.accountId
+ }
+ })
+
+ setIsAuthorized(true)
+
+ if (type === 'authentication') {
+ setConf(newConf)
+ setLoading({ ...loading, auth: false })
+ toast.success(__('Authorized Successfully', 'bit-integrations'))
+
+ fetchFabmanWorkspaces(newConf, setConf, loading, setLoading, 'fetch')
+ }
+ return
+ }
+ setLoading({ ...loading, auth: false })
+ toast.error(__('Authorization Failed', 'bit-integrations'))
+ })
+}
+
+export const fetchFabmanWorkspaces = (confTmp, setConf, loading, setLoading, type = 'fetch') => {
+ if (!confTmp.apiKey) {
+ toast.error(__('API key is required to fetch workspaces', 'bit-integrations'))
+ return
+ }
+
+ setLoading({ ...loading, workspaces: true })
+
+ const requestParams = { apiKey: confTmp.apiKey }
+
+ bitsFetch(requestParams, 'fabman_fetch_workspaces').then(result => {
+ setLoading({ ...loading, workspaces: false })
+
+ if (result && result.success) {
+ // Use mutative's produce for state update
+ const newConf = create(confTmp, draft => {
+ if (result.data && result.data.workspaces && Array.isArray(result.data.workspaces)) {
+ draft.workspaces = result.data.workspaces
+ if (result.data.workspaces.length === 1) {
+ draft.selectedWorkspace = result.data.workspaces[0].id
+ }
+ }
+ })
+
+ setConf(newConf)
+ toast.success(
+ type === 'refresh'
+ ? __('Workspaces refreshed successfully', 'bit-integrations')
+ : __('Workspaces fetched successfully', 'bit-integrations')
+ )
+ return
+ }
+ toast.error(__('Failed to fetch workspaces', 'bit-integrations'))
+ })
+}
+
+export const isConfigInvalid = (fabmanConf, formField) => {
+ if (!fabmanConf.actionName) return true
+
+ if (['update_member', 'delete_member'].includes(fabmanConf.actionName)) {
+ // isEmailMappingInvalid needs to be passed or imported
+ if (isEmailMappingInvalid(fabmanConf, formField)) return true
+ if (!checkMappedFields(fabmanConf)) return true
+ return false
+ }
+
+ if (
+ ['update_spaces', 'delete_spaces'].includes(fabmanConf.actionName) &&
+ !fabmanConf.selectedWorkspace
+ )
+ return true
+
+ if (!checkMappedFields(fabmanConf)) return true
+ return false
+}
+
+export const hasEmailFieldMapped = fabmanConf => {
+ return fabmanConf.field_map?.some(field => field.fabmanFormField === 'emailAddress' && field.formField)
+}
+
+export const getEmailMappingRow = fabmanConf => {
+ const rows = Array.isArray(fabmanConf?.field_map) ? fabmanConf.field_map : []
+ return rows.find(r => r?.fabmanFormField === 'emailAddress')
+}
+
+export const isEmailMappingInvalid = (fabmanConf, formFields, checkValidEmail) => {
+ const emailRow = getEmailMappingRow(fabmanConf)
+
+ if (!emailRow) return true
+
+ if (emailRow.formField === 'custom') {
+ const customValue = (emailRow.customValue || '').trim()
+ return !customValue || !checkValidEmail(customValue)
+ }
+
+ const selectedField = (formFields || []).find(f => f.name === emailRow.formField)
+
+ if (!selectedField) return false
+
+ const hasEmailType = selectedField.type && String(selectedField.type).toLowerCase() === 'email'
+ const looksLikeEmailField =
+ /email/i.test(selectedField.name || '') || /email/i.test(selectedField.label || '')
+ return !(hasEmailType || !selectedField.type || looksLikeEmailField)
+}
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/FabmanFieldMap.jsx b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanFieldMap.jsx
new file mode 100644
index 00000000..0347d00b
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanFieldMap.jsx
@@ -0,0 +1,131 @@
+/* eslint-disable no-console */
+import { useRecoilValue } from 'recoil'
+import { useMemo, useCallback } from 'react'
+import { __, sprintf } from '../../../Utils/i18nwrap'
+import { addFieldMap, delFieldMap, handleFieldMapping } from './IntegrationHelpers'
+import { SmartTagField } from '../../../Utils/StaticData/SmartTagField'
+import { $btcbi } from '../../../GlobalStates'
+import { generateMappedField } from './FabmanCommonFunc'
+import TagifyInput from '../../Utilities/TagifyInput'
+import { handleCustomValue } from '../IntegrationHelpers/IntegrationHelpers'
+
+export default function FabmanFieldMap({ i, formFields, field, fabmanConf, setFabmanConf }) {
+ const requiredFields = useMemo(
+ () => (fabmanConf?.staticFields ? fabmanConf.staticFields.filter(fld => !!fld.required) : []),
+ [fabmanConf?.staticFields]
+ )
+ const nonRequriedFields = useMemo(
+ () => (fabmanConf?.staticFields ? fabmanConf.staticFields.filter(fld => !fld.required) : []),
+ [fabmanConf?.staticFields]
+ )
+ const customFields = useMemo(
+ () => (Array.isArray(fabmanConf.customFields) ? fabmanConf.customFields : []),
+ [fabmanConf.customFields]
+ )
+ const allNonrequriedFields = useMemo(
+ () => [...nonRequriedFields, ...customFields],
+ [nonRequriedFields, customFields]
+ )
+
+ const btcbi = useRecoilValue($btcbi)
+ const { isPro } = btcbi
+
+ const onFieldMapping = useCallback(
+ ev => handleFieldMapping(ev, i, fabmanConf, setFabmanConf),
+ [i, fabmanConf, setFabmanConf]
+ )
+ const onCustomValue = useCallback(
+ e => handleCustomValue(e, i, fabmanConf, setFabmanConf),
+ [i, fabmanConf, setFabmanConf]
+ )
+ const onAddFieldMap = useCallback(
+ () => addFieldMap(i, fabmanConf, setFabmanConf),
+ [i, fabmanConf, setFabmanConf]
+ )
+ const onDelFieldMap = useCallback(
+ () => delFieldMap(i, fabmanConf, setFabmanConf),
+ [i, fabmanConf, setFabmanConf]
+ )
+
+ return (
+
+
+
+
+
+ {field.formField === 'custom' && (
+
+ )}
+
+
+
+ {i >= requiredFields.length && (
+ <>
+
+
+ >
+ )}
+
+
+ )
+}
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/FabmanIntegLayout.jsx b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanIntegLayout.jsx
new file mode 100644
index 00000000..c2eabbcb
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/FabmanIntegLayout.jsx
@@ -0,0 +1,314 @@
+/* eslint-disable no-unused-vars */
+/* eslint-disable react-hooks/exhaustive-deps */
+import { __ } from '../../../Utils/i18nwrap'
+import FabmanFieldMap from './FabmanFieldMap'
+import { addFieldMap } from './IntegrationHelpers'
+import { fetchFabmanWorkspaces, generateMappedField } from './FabmanCommonFunc'
+import Loader from '../../Loaders/Loader'
+import { useEffect, useMemo, useCallback, useRef } from 'react'
+import Note from '../../Utilities/Note'
+
+const fabmanActionsList = [
+ { label: __('Create Member', 'bit-integrations'), value: 'create_member' },
+ { label: __('Update Member', 'bit-integrations'), value: 'update_member' },
+ { label: __('Delete Member', 'bit-integrations'), value: 'delete_member' },
+ { label: __('Create Spaces', 'bit-integrations'), value: 'create_spaces' },
+ { label: __('Update Spaces', 'bit-integrations'), value: 'update_spaces' }
+]
+
+export default function FabmanIntegLayout({
+ formFields,
+ fabmanConf,
+ setFabmanConf,
+ loading,
+ setLoading,
+ setSnackbar
+}) {
+ const getActiveStaticFields = useCallback(conf => {
+ if (conf.actionName === 'create_spaces' || conf.actionName === 'update_spaces') {
+ const fields = Array.isArray(conf.spacesStaticFields)
+ ? conf.spacesStaticFields.map(f => ({ ...f }))
+ : []
+ const isCreate = conf.actionName === 'create_spaces'
+ const nameIdx = fields.findIndex(f => String(f.key) === 'name')
+
+ if (nameIdx > -1) fields[nameIdx].required = true
+
+ const tzIdx = fields.findIndex(f => String(f.key) === 'timezone')
+
+ if (tzIdx > -1) fields[tzIdx].required = isCreate
+ return fields
+ }
+
+ if (conf.actionName === 'update_member' || conf.actionName === 'delete_member') {
+ const fields = Array.isArray(conf.memberStaticFields)
+ ? conf.memberStaticFields.map(f => ({ ...f }))
+ : []
+ const emailIdx = fields.findIndex(f => String(f.key) === 'emailAddress')
+
+ if (emailIdx > -1) fields[emailIdx].required = true
+
+ const firstNameIdx = fields.findIndex(f => String(f.key) === 'firstName')
+
+ if (firstNameIdx > -1) fields[firstNameIdx].required = false
+ return fields
+ }
+
+ return conf.memberStaticFields
+ }, [])
+
+ useEffect(() => {
+ const staticFields = getActiveStaticFields(fabmanConf) || []
+ const requiredFields = staticFields.filter(f => !!f.required)
+
+ if (!Array.isArray(fabmanConf.field_map)) return
+
+ let changed = false
+ const newFieldMap = [...fabmanConf.field_map]
+
+ for (let i = 0; i < requiredFields.length; i += 1) {
+ if (!newFieldMap[i]) {
+ newFieldMap[i] = { formField: '', fabmanFormField: requiredFields[i].key }
+ changed = true
+ } else if (newFieldMap[i].fabmanFormField !== requiredFields[i].key) {
+ newFieldMap[i] = { ...newFieldMap[i], fabmanFormField: requiredFields[i].key }
+ changed = true
+ }
+ }
+
+ if (changed) {
+ const newConf = { ...fabmanConf, field_map: newFieldMap }
+ setFabmanConf(newConf)
+ }
+ }, [fabmanConf.actionName, fabmanConf.selectedWorkspace, fabmanConf.field_map, getActiveStaticFields])
+
+ const handleActionChange = useCallback(
+ e => {
+ const newConf = { ...fabmanConf }
+ const value = e.target.value
+ newConf.actionName = value
+
+ if (value === 'create_spaces' || value === 'update_spaces') {
+ delete newConf.selectedMember
+ delete newConf.selectedLockVersion
+ }
+
+ newConf.field_map = [{ formField: '', fabmanFormField: '' }]
+
+ setFabmanConf(newConf)
+ },
+ [fabmanConf, setFabmanConf, loading, setLoading]
+ )
+
+ const handleWorkspaceChange = useCallback(
+ e => {
+ const newConf = { ...fabmanConf }
+ newConf.selectedWorkspace = e.target.value
+
+ const ws = (fabmanConf?.workspaces || []).find(w => String(w.id) === String(e.target.value))
+
+ if (ws && typeof ws.lockVersion !== 'undefined') {
+ newConf.selectedLockVersion = ws.lockVersion
+ }
+
+ setFabmanConf(newConf)
+ },
+ [fabmanConf, setFabmanConf, loading, setLoading]
+ )
+
+ const handleMemberChange = useCallback(
+ e => {
+ const newConf = { ...fabmanConf }
+ const selectedValue = e.target.value
+
+ if (selectedValue) {
+ const [memberId, lockVersion] = selectedValue.split('|')
+ newConf.selectedMember = memberId
+ newConf.selectedLockVersion = lockVersion
+ } else {
+ delete newConf.selectedMember
+ delete newConf.selectedLockVersion
+ }
+
+ setFabmanConf(newConf)
+ },
+ [fabmanConf, setFabmanConf]
+ )
+
+ const handleRefreshWorkspaces = useCallback(() => {
+ fetchFabmanWorkspaces(fabmanConf, setFabmanConf, loading, setLoading, 'refresh')
+ }, [fabmanConf, setFabmanConf, loading, setLoading])
+
+ const isSpaceAction = useMemo(
+ () => fabmanConf.actionName === 'create_spaces' || fabmanConf.actionName === 'update_spaces',
+ [fabmanConf.actionName]
+ )
+
+ const isDeleteMember = useMemo(
+ () => fabmanConf.actionName === 'delete_member',
+ [fabmanConf.actionName]
+ )
+
+ const activeStaticFields = useMemo(
+ () => getActiveStaticFields(fabmanConf),
+ [fabmanConf, getActiveStaticFields]
+ )
+
+ return (
+
+
+
+ {__('Action:', 'bit-integrations')}
+
+
+
+ {fabmanConf.actionName &&
+ !isDeleteMember &&
+ (!isSpaceAction || fabmanConf.actionName === 'update_spaces') && (
+ <>
+
+ {__('Select Workspace:', 'bit-integrations')}
+
+
+
+ {loading.workspaces && (
+
+ )}
+
+
+ >
+ )}
+
+ {isDeleteMember && (
+ <>
+
+ {__('Field Map', 'bit-integrations')}
+
+
+
+
+
+ {__('Form Fields', 'bit-integrations')}
+
+
+ {__('Fabman Fields', 'bit-integrations')}
+
+
+
+ >
+ )}
+
+ {fabmanConf.actionName &&
+ fabmanConf.actionName !== 'delete_member' &&
+ (!isSpaceAction ||
+ (isSpaceAction &&
+ (fabmanConf.actionName !== 'update_spaces' || fabmanConf.selectedWorkspace))) && (
+ <>
+ {fabmanConf.actionName === 'create_spaces' &&
}
+
+ {__('Field Map', 'bit-integrations')}
+
+
+
+
+
+ {__('Form Fields', 'bit-integrations')}
+
+
+ {__('Fabman Fields', 'bit-integrations')}
+
+
+ {fabmanConf?.field_map.map((itm, i) => (
+
+ ))}
+
+
+
+
+
+
+
+ {__('Utilities', 'bit-integrations')}
+
+
+
+ >
+ )}
+
+ )
+}
+
+const fabmanWorkspaceNote = `${__(
+ 'Please select workspace for Create Member, Update Member, and Update Spaces.',
+ 'bit-integrations'
+)}
`
+
+const fabmanTimezoneNote = `${__(
+ 'For Create Spaces, Timezone must be like Asia/Dhaka (IANA timezone format).',
+ 'bit-integrations'
+)}
`
diff --git a/frontend-dev/src/components/AllIntegrations/Fabman/IntegrationHelpers.jsx b/frontend-dev/src/components/AllIntegrations/Fabman/IntegrationHelpers.jsx
new file mode 100644
index 00000000..c4e635e6
--- /dev/null
+++ b/frontend-dev/src/components/AllIntegrations/Fabman/IntegrationHelpers.jsx
@@ -0,0 +1,40 @@
+import { create } from 'mutative'
+
+export const addFieldMap = (i, confTmp, setConf) => {
+ const newConf = create(confTmp, draft => {
+ const fieldMap = Array.isArray(draft.field_map) ? [...draft.field_map] : []
+ fieldMap.splice(i, 0, {})
+ draft.field_map = fieldMap
+ })
+
+ setConf(newConf)
+}
+
+export const delFieldMap = (i, confTmp, setConf) => {
+ const newConf = create(confTmp, draft => {
+ const fieldMap = Array.isArray(draft.field_map) ? [...draft.field_map] : []
+
+ if (fieldMap.length > 1) {
+ fieldMap.splice(i, 1)
+ }
+ draft.field_map = fieldMap
+ })
+
+ setConf(newConf)
+}
+
+export const handleFieldMapping = (event, index, conftTmp, setConf) => {
+ const newConf = create(conftTmp, draft => {
+ const fieldMap = Array.isArray(draft.field_map) ? [...draft.field_map] : []
+
+ if (!fieldMap[index]) fieldMap[index] = {}
+ fieldMap[index][event.target.name] = event.target.value
+
+ if (event.target.value === 'custom') {
+ fieldMap[index].customValue = ''
+ }
+ draft.field_map = fieldMap
+ })
+
+ setConf(newConf)
+}
diff --git a/frontend-dev/src/components/AllIntegrations/IntegInfo.jsx b/frontend-dev/src/components/AllIntegrations/IntegInfo.jsx
index 5d203d9a..b26da0b3 100644
--- a/frontend-dev/src/components/AllIntegrations/IntegInfo.jsx
+++ b/frontend-dev/src/components/AllIntegrations/IntegInfo.jsx
@@ -87,6 +87,7 @@ const MailupAuthentication = lazy(() => import('./Mailup/MailupAuthorization'))
const NotionAuthorization = lazy(() => import('./Notion/NotionAuthorization'))
const MailjetAuthorization = lazy(() => import('./Mailjet/MailjetAuthorization'))
const SendGridAuthorization = lazy(() => import('./SendGrid/SendGridAuthorization'))
+const FabmanAuthorization = lazy(() => import('./Fabman/FabmanAuthorization'))
const PCloudAuthorization = lazy(() => import('./PCloud/PCloudAuthorization'))
const EmailOctopusAuthorization = lazy(() => import('./EmailOctopus/EmailOctopusAuthorization'))
const CustomAction = lazy(() => import('./CustomAction/CustomFuncEditor'))
@@ -426,6 +427,8 @@ export default function IntegInfo() {
return
case 'SendGrid':
return
+ case 'Fabman':
+ return
case 'PCloud':
return
case 'EmailOctopus':
diff --git a/frontend-dev/src/components/AllIntegrations/NewInteg.jsx b/frontend-dev/src/components/AllIntegrations/NewInteg.jsx
index c06d03d3..f82d0d4d 100644
--- a/frontend-dev/src/components/AllIntegrations/NewInteg.jsx
+++ b/frontend-dev/src/components/AllIntegrations/NewInteg.jsx
@@ -93,6 +93,7 @@ const Mailup = lazy(() => import('./Mailup/Mailup'))
const Notion = lazy(() => import('./Notion/Notion'))
const Mailjet = lazy(() => import('./Mailjet/Mailjet'))
const SendGrid = lazy(() => import('./SendGrid/SendGrid'))
+const Fabman = lazy(() => import('./Fabman/Fabman'))
const PCloud = lazy(() => import('./PCloud/PCloud'))
const EmailOctopus = lazy(() => import('./EmailOctopus/EmailOctopus'))
const Smaily = lazy(() => import('./Smaily/Smaily'))
@@ -904,6 +905,15 @@ export default function NewInteg({ allIntegURL }) {
setFlow={setFlow}
/>
)
+ case 'Fabman':
+ return (
+
+ )
case 'PCloud':
return (
apiKey)) {
+ wp_send_json_error(__('API Key is required', 'bit-integrations'), 400);
+ }
+
+ $header = [
+ 'Authorization' => 'Bearer ' . $requestParams->apiKey,
+ 'Content-Type' => 'application/json'
+ ];
+
+ $apiEndpoint = 'https://fabman.io/api/v1/accounts';
+ $apiResponse = HttpHelper::get($apiEndpoint, null, $header);
+
+ if (is_wp_error($apiResponse)) {
+ wp_send_json_error($apiResponse->get_error_message(), 400);
+ }
+
+ if (empty($apiResponse) || isset($apiResponse->error) || !\is_array($apiResponse) || !isset($apiResponse[0]->id)) {
+ wp_send_json_error(isset($apiResponse->error) ? $apiResponse->error : __('Invalid API credentials', 'bit-integrations'), 400);
+ }
+
+ $accountId = $apiResponse[0]->id;
+ wp_send_json_success([
+ 'accountId' => $accountId
+ ], 200);
+ }
+
+ public static function fetchWorkspaces($requestParams)
+ {
+ if (empty($requestParams->apiKey)) {
+ wp_send_json_error(__('API Key is required', 'bit-integrations'), 400);
+ }
+
+ $header = [
+ 'Authorization' => 'Bearer ' . $requestParams->apiKey,
+ 'Content-Type' => 'application/json'
+ ];
+
+ $apiEndpoint = 'https://fabman.io/api/v1/spaces';
+ $apiResponse = HttpHelper::get($apiEndpoint, null, $header);
+
+ if (is_wp_error($apiResponse)) {
+ wp_send_json_error($apiResponse->get_error_message(), 400);
+ }
+
+ if (empty($apiResponse) || isset($apiResponse->error) || !\is_array($apiResponse)) {
+ wp_send_json_error(isset($apiResponse->error) ? $apiResponse->error : __('Failed to fetch workspaces', 'bit-integrations'), 400);
+ }
+
+ wp_send_json_success([
+ 'workspaces' => $apiResponse
+ ], 200);
+ }
+
+ public static function execute($integrationData, $fieldValues)
+ {
+ $integrationDetails = $integrationData->flow_details;
+ $apiKey = $integrationDetails->apiKey;
+ $selectedWorkspace = $integrationDetails->selectedWorkspace ?? null;
+ $accountId = $integrationDetails->accountId ?? null;
+ $actionName = $integrationDetails->actionName;
+ $integId = $integrationData->id;
+ $memberId = $integrationDetails->selectedMember ?? null;
+ $lockVersion = $integrationDetails->selectedLockVersion ?? null;
+
+ if (\in_array($actionName, ['update_member', 'delete_member'])) {
+ if ($actionName === 'delete_member' || empty($memberId)) {
+ $email = self::getMappedValue($integrationDetails->field_map, 'emailAddress', $fieldValues);
+ if ($email) {
+ $memberData = self::fetchMemberByEmailInternal($apiKey, $email);
+ if ($memberData) {
+ $memberId = $memberData['memberId'];
+ $lockVersion = $memberData['lockVersion'];
+ }
+ }
+ }
+ }
+
+ $recordApiHelper = new RecordApiHelper(
+ $apiKey,
+ $selectedWorkspace,
+ $accountId,
+ $integId,
+ $memberId,
+ $lockVersion
+ );
+
+ return $recordApiHelper->execute($actionName, $fieldValues, $integrationDetails);
+ }
+
+ private static function getMappedValue($fieldMap, $targetField, $fieldValues)
+ {
+ if (empty($fieldMap)) {
+ return;
+ }
+
+ foreach ($fieldMap as $map) {
+ if (!empty($map->fabmanFormField) && $map->fabmanFormField === $targetField) {
+ if ($map->formField === 'custom') {
+ return $map->customValue;
+ }
+
+ return $fieldValues[$map->formField] ?? null;
+ }
+ }
+ }
+
+ private static function fetchMemberByEmailInternal($apiKey, $email)
+ {
+ $header = [
+ 'Authorization' => 'Bearer ' . $apiKey,
+ 'Content-Type' => 'application/json'
+ ];
+
+ $apiEndpoint = 'https://fabman.io/api/v1/members?q=' . urlencode($email);
+ $apiResponse = HttpHelper::get($apiEndpoint, null, $header);
+
+ if (is_wp_error($apiResponse) || empty($apiResponse) || isset($apiResponse->error) || !\is_array($apiResponse)) {
+ return;
+ }
+
+ if (isset($apiResponse[0])) {
+ return [
+ 'memberId' => $apiResponse[0]->id,
+ 'lockVersion' => $apiResponse[0]->lockVersion
+ ];
+ }
+ }
+}
diff --git a/includes/Actions/Fabman/RecordApiHelper.php b/includes/Actions/Fabman/RecordApiHelper.php
new file mode 100644
index 00000000..3f88b48e
--- /dev/null
+++ b/includes/Actions/Fabman/RecordApiHelper.php
@@ -0,0 +1,177 @@
+integrationID = $integrationID;
+ $this->apiKey = $apiKey;
+ $this->workspaceId = $workspaceId;
+ $this->accountId = $accountId;
+ $this->memberId = $memberId;
+ $this->lockVersion = $lockVersion;
+ $this->apiEndpoint = 'https://fabman.io/api/v1';
+ }
+
+ public function execute($actionName, $fieldValues, $integrationDetails)
+ {
+ $finalData = [];
+ $finalData['account'] = $this->accountId;
+
+ if ($this->workspaceId) {
+ $finalData['space'] = $this->workspaceId;
+ }
+
+ if ($this->memberId) {
+ $finalData['memberId'] = $this->memberId;
+ }
+
+ if (!empty($integrationDetails->field_map)) {
+ foreach ($integrationDetails->field_map as $fieldMap) {
+ if (!empty($fieldMap->formField) && !empty($fieldMap->fabmanFormField)) {
+ $finalData[$fieldMap->fabmanFormField] = $fieldMap->formField === 'custom'
+ ? $fieldMap->customValue
+ : ($fieldValues[$fieldMap->formField] ?? null);
+ }
+ }
+ }
+
+ switch ($actionName) {
+ case 'create_member':
+ $apiResponse = $this->createMember($finalData);
+
+ break;
+ case 'update_member':
+ $apiResponse = $this->updateMember($finalData);
+
+ break;
+ case 'delete_member':
+ $apiResponse = $this->deleteMember($finalData);
+
+ break;
+ case 'create_spaces':
+ $apiResponse = $this->createSpace($finalData);
+
+ break;
+ case 'update_spaces':
+ $apiResponse = $this->updateSpace($finalData);
+
+ break;
+ default:
+ $apiResponse = new WP_Error(
+ 'INVALID_ACTION',
+ __('Invalid action name', 'bit-integrations')
+ );
+ }
+
+ if (is_wp_error($apiResponse) || (\is_object($apiResponse) && isset($apiResponse->error))) {
+ $status = 'error';
+ } else {
+ $status = \in_array(HttpHelper::$responseCode, [200, 201, 204]) ? 'success' : 'error';
+ }
+
+ LogHandler::save($this->integrationID, ['type' => 'record', 'type_name' => $actionName], $status, $apiResponse);
+
+ return $apiResponse;
+ }
+
+ private function createMember($data)
+ {
+ unset($data['memberId']);
+ $apiEndpoint = $this->apiEndpoint . '/members';
+ $header = $this->setHeaders();
+ $apiResponse = HttpHelper::post($apiEndpoint, json_encode($data), $header);
+
+ if (\is_wp_error($apiResponse)) {
+ return $apiResponse;
+ }
+
+ if (empty($apiResponse) || isset($apiResponse->error)) {
+ return new WP_Error('API_ERROR', isset($apiResponse->error) ? $apiResponse->error : \__('Failed to create member', 'bit-integrations'));
+ }
+
+ return HttpHelper::$responseCode === 201 ? 'Member Created Successfully' : 'Failed';
+ }
+
+ private function updateMember($data)
+ {
+ $data['lockVersion'] = $this->lockVersion;
+
+ if (empty($this->memberId)) {
+ return new WP_Error('MISSING_MEMBER_ID', __('The email provided did not match any existing Fabman member.', 'bit-integrations'));
+ }
+
+ $response = \apply_filters('btcbi_fabman_update_member', false, json_encode($data), $this->setHeaders(), $this->apiEndpoint, $this->memberId);
+
+ return $this->handleFilterResponse($response);
+ }
+
+ private function deleteMember()
+ {
+ if (empty($this->memberId)) {
+ return new WP_Error('MISSING_MEMBER_ID', __('The email provided did not match any existing Fabman member.', 'bit-integrations'));
+ }
+
+ $response = \apply_filters('btcbi_fabman_delete_member', false, $this->setHeaders(), $this->apiEndpoint, $this->memberId);
+
+ return $this->handleFilterResponse($response);
+ }
+
+ private function createSpace($data)
+ {
+ unset($data['space']);
+ $response = \apply_filters('btcbi_fabman_create_space', false, json_encode($data), $this->setHeaders(), $this->apiEndpoint);
+
+ return $this->handleFilterResponse($response);
+ }
+
+ private function updateSpace($data)
+ {
+ $data['lockVersion'] = $this->lockVersion;
+
+ if (empty($this->workspaceId)) {
+ return new WP_Error('MISSING_SPACE_ID', __('Please select a space to update.', 'bit-integrations'));
+ }
+
+ $response = \apply_filters('btcbi_fabman_update_space', false, json_encode($data), $this->setHeaders(), $this->apiEndpoint, $this->workspaceId);
+
+ return $this->handleFilterResponse($response);
+ }
+
+ private function handleFilterResponse($response)
+ {
+ if (empty($response)) {
+ return (object) ['error' => \wp_sprintf(\__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integration Pro')];
+ }
+
+ return $response;
+ }
+
+ private function setHeaders(): array
+ {
+ return [
+ 'Authorization' => 'Bearer ' . $this->apiKey,
+ 'Content-Type' => 'application/json'
+ ];
+ }
+}
diff --git a/includes/Actions/Fabman/Routes.php b/includes/Actions/Fabman/Routes.php
new file mode 100644
index 00000000..cc3118b5
--- /dev/null
+++ b/includes/Actions/Fabman/Routes.php
@@ -0,0 +1,11 @@
+