From 97a944bac8a76df38c5fc6e74bd0e73ccaeb1b72 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 09:56:45 +0600 Subject: [PATCH 01/10] feat(booknetic): add the Booknetic action backend Controller runs the flow, Helper serves the dropdown lists through one refresh_booknetic_options route, and Service fires one Pro filter per action and logs every result. --- .../Actions/Booknetic/BookneticController.php | 22 ++ backend/Actions/Booknetic/BookneticHelper.php | 144 +++++++++ .../Actions/Booknetic/BookneticService.php | 277 ++++++++++++++++++ backend/Actions/Booknetic/Routes.php | 10 + 4 files changed, 453 insertions(+) create mode 100644 backend/Actions/Booknetic/BookneticController.php create mode 100644 backend/Actions/Booknetic/BookneticHelper.php create mode 100644 backend/Actions/Booknetic/BookneticService.php create mode 100644 backend/Actions/Booknetic/Routes.php diff --git a/backend/Actions/Booknetic/BookneticController.php b/backend/Actions/Booknetic/BookneticController.php new file mode 100644 index 000000000..836221838 --- /dev/null +++ b/backend/Actions/Booknetic/BookneticController.php @@ -0,0 +1,22 @@ +flow_details; + $integId = $integrationData->id; + $fieldMap = $integrationDetails->field_map; + $utilities = $integrationDetails->utilities ?? []; + + if (empty($fieldMap)) { + return new WP_Error('field_map_empty', __('Field map is empty', 'bit-integrations')); + } + + return (new BookneticService($integrationDetails, $integId))->execute($fieldValues, $fieldMap, $utilities); + } +} diff --git a/backend/Actions/Booknetic/BookneticHelper.php b/backend/Actions/Booknetic/BookneticHelper.php new file mode 100644 index 000000000..e65dd0372 --- /dev/null +++ b/backend/Actions/Booknetic/BookneticHelper.php @@ -0,0 +1,144 @@ + 'BookneticApp\Models\Location', + 'services' => 'BookneticApp\Models\Service', + 'staff' => 'BookneticApp\Models\Staff', + 'customer_categories' => 'BookneticApp\Models\CustomerCategory', + 'location_categories' => 'BookneticApp\Models\LocationCategory', + 'service_categories' => 'BookneticApp\Models\ServiceCategory', + 'extra_categories' => 'BookneticApp\Models\ExtraCategory', + ]; + + public static function isExists() + { + if (!class_exists('BookneticApp\Providers\Core\Bootstrap')) { + wp_send_json_error(__('Booknetic is not activated or not installed', 'bit-integrations'), 400); + } + } + + public function refreshOptions($requestParams) + { + self::isExists(); + + $type = sanitize_key($requestParams->type ?? ''); + + switch ($type) { + case 'customers': + $options = self::customerOptions(); + + break; + case 'statuses': + $options = self::statusOptions(); + + break; + case 'payment_gateways': + $options = self::paymentGatewayOptions(); + + break; + case 'users': + $options = self::userOptions(); + + break; + default: + if (!isset(self::MODEL_OPTIONS[$type])) { + wp_send_json_error(__('The option type is not valid.', 'bit-integrations'), 400); + } + + $options = self::modelOptions(self::MODEL_OPTIONS[$type]); + + break; + } + + wp_send_json_success(['options' => $options], 200); + } + + private static function modelOptions($modelClass) + { + if (!class_exists($modelClass)) { + return []; + } + + $options = []; + + foreach ($modelClass::query()->orderBy('id DESC')->fetchAll() as $record) { + $label = (string) ($record->name ?? ''); + $options[] = [ + 'value' => (string) $record->id, + 'label' => $label !== '' ? $label : '#' . (int) $record->id, + ]; + } + + return $options; + } + + private static function customerOptions() + { + if (!class_exists('BookneticApp\Models\Customer')) { + return []; + } + + $options = []; + + foreach (\BookneticApp\Models\Customer::query()->orderBy('id DESC')->fetchAll() as $customer) { + $name = trim($customer->first_name . ' ' . $customer->last_name); + $label = $name !== '' ? $name : '#' . (int) $customer->id; + + if (!empty($customer->email)) { + $label .= ' (' . $customer->email . ')'; + } + + $options[] = ['value' => (string) $customer->id, 'label' => $label]; + } + + return $options; + } + + private static function statusOptions() + { + if (!class_exists('BookneticApp\Providers\Helpers\Helper')) { + return []; + } + + $options = []; + + foreach (\BookneticApp\Providers\Helpers\Helper::getAppointmentStatuses() as $value => $status) { + $options[] = ['value' => (string) $value, 'label' => $status['title'] ?? $value]; + } + + return $options; + } + + private static function paymentGatewayOptions() + { + if (!class_exists('BookneticApp\Providers\Common\PaymentGatewayService')) { + return []; + } + + $options = []; + + foreach (\BookneticApp\Providers\Common\PaymentGatewayService::getGateways() as $value => $gateway) { + $options[] = ['value' => (string) $value, 'label' => $gateway->getTitle()]; + } + + return $options; + } + + private static function userOptions() + { + $options = []; + + foreach (get_users(['fields' => ['ID', 'display_name', 'user_email']]) as $user) { + $options[] = [ + 'value' => (string) $user->ID, + 'label' => ($user->display_name ?: $user->user_email) . ' (' . $user->user_email . ')', + ]; + } + + return $options; + } +} diff --git a/backend/Actions/Booknetic/BookneticService.php b/backend/Actions/Booknetic/BookneticService.php new file mode 100644 index 000000000..f99a4c59d --- /dev/null +++ b/backend/Actions/Booknetic/BookneticService.php @@ -0,0 +1,277 @@ +_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + public function execute($fieldValues, $fieldMap, $utilities) + { + if (!class_exists('BookneticApp\Providers\Core\Bootstrap')) { + return [ + 'success' => false, + 'message' => __('Booknetic is not installed or activated', 'bit-integrations'), + ]; + } + + $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); + $mainAction = $this->_integrationDetails->mainAction ?? ''; + $integrationDetails = $this->_integrationDetails; + $default = [ + 'success' => false, + // translators: %s: Plugin name + 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro'), + ]; + + switch ($mainAction) { + case 'create_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_create_appointment'), $default, $fieldData, $utilities, $integrationDetails); + + break; + + case 'update_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_update_appointment'), $default, $fieldData, $utilities); + + break; + + case 'delete_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_appointment'), $default, $fieldData); + + break; + + case 'reschedule_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_reschedule_appointment'), $default, $fieldData, $utilities); + + break; + + case 'change_appointment_status': + $response = Hooks::apply(Config::withPrefix('booknetic_change_appointment_status'), $default, $fieldData, $integrationDetails); + + break; + + case 'update_payment': + $response = Hooks::apply(Config::withPrefix('booknetic_update_payment'), $default, $fieldData, $integrationDetails); + + break; + + case 'complete_payment': + $response = Hooks::apply(Config::withPrefix('booknetic_complete_payment'), $default, $fieldData); + + break; + + case 'create_payment_link': + $response = Hooks::apply(Config::withPrefix('booknetic_create_payment_link'), $default, $fieldData, $integrationDetails); + + break; + + case 'create_customer': + $response = Hooks::apply(Config::withPrefix('booknetic_create_customer'), $default, $fieldData, $utilities); + + break; + + case 'update_customer': + $response = Hooks::apply(Config::withPrefix('booknetic_update_customer'), $default, $fieldData, $utilities); + + break; + + case 'delete_customer': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_customer'), $default, $fieldData); + + break; + + case 'assign_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_assign_customer_category'), $default, $fieldData, $integrationDetails); + + break; + + case 'create_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_create_staff'), $default, $fieldData, $utilities, $integrationDetails); + + break; + + case 'update_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_update_staff'), $default, $fieldData, $utilities); + + break; + + case 'delete_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_staff'), $default, $fieldData); + + break; + + case 'duplicate_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_staff'), $default, $fieldData); + + break; + + case 'set_staff_status': + $response = Hooks::apply(Config::withPrefix('booknetic_set_staff_status'), $default, $fieldData, $integrationDetails); + + break; + + case 'create_location': + $response = Hooks::apply(Config::withPrefix('booknetic_create_location'), $default, $fieldData, $utilities); + + break; + + case 'update_location': + $response = Hooks::apply(Config::withPrefix('booknetic_update_location'), $default, $fieldData, $utilities); + + break; + + case 'delete_location': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_location'), $default, $fieldData); + + break; + + case 'duplicate_location': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_location'), $default, $fieldData); + + break; + + case 'set_location_status': + $response = Hooks::apply(Config::withPrefix('booknetic_set_location_status'), $default, $fieldData, $integrationDetails); + + break; + + case 'create_service': + $response = Hooks::apply(Config::withPrefix('booknetic_create_service'), $default, $fieldData, $utilities, $integrationDetails); + + break; + + case 'update_service': + $response = Hooks::apply(Config::withPrefix('booknetic_update_service'), $default, $fieldData, $utilities); + + break; + + case 'delete_service': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_service'), $default, $fieldData); + + break; + + case 'duplicate_service': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_service'), $default, $fieldData); + + break; + + case 'set_service_status': + $response = Hooks::apply(Config::withPrefix('booknetic_set_service_status'), $default, $fieldData, $integrationDetails); + + break; + + case 'create_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_create_service_extra'), $default, $fieldData, $utilities); + + break; + + case 'update_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_update_service_extra'), $default, $fieldData, $utilities); + + break; + + case 'delete_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_service_extra'), $default, $fieldData); + + break; + + case 'duplicate_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_service_extra'), $default, $fieldData, $utilities); + + break; + + case 'create_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_create_customer_category'), $default, $fieldData, $utilities); + + break; + + case 'update_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_update_customer_category'), $default, $fieldData, $utilities); + + break; + + case 'delete_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_customer_category'), $default, $fieldData); + + break; + + case 'create_location_category': + $response = Hooks::apply(Config::withPrefix('booknetic_create_location_category'), $default, $fieldData); + + break; + + case 'update_location_category': + $response = Hooks::apply(Config::withPrefix('booknetic_update_location_category'), $default, $fieldData); + + break; + + case 'delete_location_category': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_location_category'), $default, $fieldData); + + break; + + case 'create_service_category': + $response = Hooks::apply(Config::withPrefix('booknetic_create_service_category'), $default, $fieldData, $utilities); + + break; + + case 'update_service_category': + $response = Hooks::apply(Config::withPrefix('booknetic_update_service_category'), $default, $fieldData, $utilities); + + break; + + case 'delete_service_category': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_service_category'), $default, $fieldData); + + break; + + case 'create_in_app_notification': + $response = Hooks::apply(Config::withPrefix('booknetic_create_in_app_notification'), $default, $fieldData, $integrationDetails); + + break; + + default: + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations'), + ]; + + break; + } + + $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; + LogHandler::save($this->_integrationID, ['type' => 'Booknetic', 'type_name' => $mainAction], $responseType, $response); + + return $response; + } + + private static function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $data = []; + + foreach ($fieldMap as $map) { + if (empty($map->formField) || empty($map->bookneticField)) { + continue; + } + + $data[$map->bookneticField] = $map->formField === 'custom' + ? Common::replaceFieldWithValue($map->customValue ?? '', $fieldValues) + : ($fieldValues[$map->formField] ?? ''); + } + + return $data; + } +} diff --git a/backend/Actions/Booknetic/Routes.php b/backend/Actions/Booknetic/Routes.php new file mode 100644 index 000000000..fb982170e --- /dev/null +++ b/backend/Actions/Booknetic/Routes.php @@ -0,0 +1,10 @@ + Date: Sun, 18 Oct 2026 09:56:45 +0600 Subject: [PATCH 02/10] feat(booknetic): list Booknetic as a Pro trigger --- backend/Core/Util/AllTriggersName.php | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/Core/Util/AllTriggersName.php b/backend/Core/Util/AllTriggersName.php index 9e0bf9840..4ae206192 100644 --- a/backend/Core/Util/AllTriggersName.php +++ b/backend/Core/Util/AllTriggersName.php @@ -36,6 +36,7 @@ public static function allTriggersName() 'BookingCalendarContactForm' => ['name' => 'Booking Calendar Contact Form', 'isPro' => true, 'is_active' => false], 'BookingCalendar' => ['name' => 'Booking Calendar', 'isPro' => true, 'is_active' => false], 'BookingPress' => ['name' => 'BookingPress', 'isPro' => true, 'is_active' => false], + 'Booknetic' => ['name' => 'Booknetic', 'isPro' => true, 'is_active' => false], 'CalculatedFieldsForm' => ['name' => 'Calculated Fields Form Pro', 'isPro' => true, 'is_active' => false], 'CartFlow' => ['name' => 'CartFlow', 'isPro' => true, 'is_active' => false], 'CartAbandonmentRecovery' => ['name' => 'Cart Abandonment Recovery', 'isPro' => true, 'is_active' => false], From b66fd52e700915fc57d42c6a0b28564634002c56 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 09:56:45 +0600 Subject: [PATCH 03/10] feat(booknetic): add the Booknetic action config UI Required options render as selects, optional options sit under Utilities, and record IDs stay in the field map so flows can map them. --- .../AllIntegrations/Booknetic/Booknetic.jsx | 124 ++++ .../Booknetic/BookneticActions.jsx | 116 ++++ .../Booknetic/BookneticAuthorization.jsx | 39 ++ .../Booknetic/BookneticCommonFunc.js | 69 +++ .../Booknetic/BookneticFieldMap.jsx | 104 ++++ .../Booknetic/BookneticIntegLayout.jsx | 158 +++++ .../Booknetic/EditBooknetic.jsx | 74 +++ .../AllIntegrations/Booknetic/staticData.js | 538 ++++++++++++++++++ .../src/resource/img/integ/booknetic.webp | Bin 0 -> 1016 bytes 9 files changed, 1222 insertions(+) create mode 100644 frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx create mode 100644 frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx create mode 100644 frontend/src/components/AllIntegrations/Booknetic/BookneticAuthorization.jsx create mode 100644 frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js create mode 100644 frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx create mode 100644 frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx create mode 100644 frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx create mode 100644 frontend/src/components/AllIntegrations/Booknetic/staticData.js create mode 100644 frontend/src/resource/img/integ/booknetic.webp diff --git a/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx b/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx new file mode 100644 index 000000000..2ccbdf102 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx @@ -0,0 +1,124 @@ +import { useState } from 'react' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { useNavigate, useParams } from 'react-router' +import BackIcn from '../../../Icons/BackIcn' +import { __, sprintf } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import BookneticAuthorization from './BookneticAuthorization' +import { checkMappedFields, missingRequiredSelect } from './BookneticCommonFunc' +import BookneticIntegLayout from './BookneticIntegLayout' + +export default function Booknetic({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const { formID } = useParams() + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + const [bookneticConf, setBookneticConf] = useState({ + name: 'Booknetic', + type: 'Booknetic', + field_map: [{ formField: '', bookneticField: '' }], + utilities: {}, + mainAction: '' + }) + + const nextPage = val => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (val !== 3) { + setStep(val) + return + } + + if (!bookneticConf.mainAction) { + setSnackbar({ + show: true, + msg: __('No action is selected. Select an action.', 'bit-integrations') + }) + return + } + + const missingSelect = missingRequiredSelect(bookneticConf) + + if (missingSelect) { + setSnackbar({ + show: true, + // translators: %s: field label + msg: sprintf(__('%s is required. Select a value.', 'bit-integrations'), missingSelect.label) + }) + return + } + + if (!checkMappedFields(bookneticConf)) { + setSnackbar({ + show: true, + msg: __('A required field is not mapped. Map all required fields.', 'bit-integrations') + }) + return + } + + if (bookneticConf.name !== '' && bookneticConf.field_map.length > 0) { + setStep(val) + } + } + + return ( +
+ +
+ + + +
+ +
+
+
+ +
+ + + saveIntegConfig(flow, setFlow, allIntegURL, bookneticConf, navigate, '', '', setIsLoading) + } + isLoading={isLoading} + dataConf={bookneticConf} + setDataConf={setBookneticConf} + formFields={formFields} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx new file mode 100644 index 000000000..4081b041d --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx @@ -0,0 +1,116 @@ +import { useState } from 'react' +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import ConfirmModal from '../../Utilities/ConfirmModal' +import TableCheckBox from '../../Utilities/TableCheckBox' +import { refreshBookneticOptions, selectsForAction } from './BookneticCommonFunc' +import { utilities } from './staticData' + +export default function BookneticActions({ bookneticConf, setBookneticConf }) { + const [isLoading, setIsLoading] = useState(false) + const [openKey, setOpenKey] = useState(null) + + const actionUtilities = selectsForAction(utilities, bookneticConf?.mainAction) + const openUtility = actionUtilities.find(item => item.key === openKey) + + const setUtility = (key, val) => { + setBookneticConf(prevConf => + create(prevConf, draftConf => { + if (!draftConf.utilities) { + draftConf.utilities = {} + } + + if (val) { + draftConf.utilities[key] = val + } else { + delete draftConf.utilities[key] + } + }) + ) + } + + const handleToggle = (e, item) => { + if (!e.target.checked) { + setUtility(item.key, '') + return + } + + setOpenKey(item.key) + + if (item.source && !bookneticConf?.default?.[item.source]) { + refreshBookneticOptions(item.source, setBookneticConf, setIsLoading) + } + } + + const closeModal = () => setOpenKey(null) + + const options = item => item.options ?? bookneticConf?.default?.[item.source] ?? [] + + return ( +
+ {actionUtilities.map(item => ( + handleToggle(e, item)} + className="wdt-200 mt-4 mr-2" + value={item.key} + title={item.title} + subTitle={item.subTitle} + /> + ))} + + +
+ {isLoading ? ( + + ) : ( + openUtility && ( +
+ setUtility(openUtility.key, val)} + /> + {openUtility.source && ( + + )} +
+ ) + )} + +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticAuthorization.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticAuthorization.jsx new file mode 100644 index 000000000..053843020 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticAuthorization.jsx @@ -0,0 +1,39 @@ +import { useCallback } from 'react' +import { AUTH_TYPES } from '../../../Utils/connectionAuth' +import { __ } from '../../../Utils/i18nwrap' +import tutorialLinks from '../../../Utils/StaticData/tutorialLinks' +import Authorization from '../../Connections/Authorization' + +export default function BookneticAuthorization({ + bookneticConf, + setBookneticConf, + step, + nextPage, + isInfo +}) { + const setStep = useCallback(value => nextPage(value), [nextPage]) + return ( + + ) +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js b/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js new file mode 100644 index 000000000..8990a85ae --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js @@ -0,0 +1,69 @@ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' +import { requiredSelects } from './staticData' + +export const handleInput = (e, bookneticConf, setBookneticConf) => { + const { name, value } = e.target + + setBookneticConf(prevConf => + create(prevConf, draftConf => { + draftConf[name] = value + }) + ) +} + +export const refreshBookneticOptions = (type, setBookneticConf, setIsLoading) => { + setIsLoading(true) + bitsFetch({ type }, 'refresh_booknetic_options') + .then(result => { + setIsLoading(false) + + if (result?.success && Array.isArray(result?.data?.options)) { + setBookneticConf(prevConf => + create(prevConf, draftConf => { + if (!draftConf.default) { + draftConf.default = {} + } + draftConf.default[type] = result.data.options + }) + ) + return + } + + toast.error(result?.data || __('Booknetic did not send the list. Try again.', 'bit-integrations')) + }) + .catch(() => setIsLoading(false)) +} + +export const selectsForAction = (selects, action) => selects.filter(s => s.actions.includes(action)) + +export const missingRequiredSelect = bookneticConf => + selectsForAction(requiredSelects, bookneticConf?.mainAction).find( + select => !bookneticConf?.[select.key] + ) + +export const checkMappedFields = bookneticConf => { + if (!bookneticConf?.mainAction || missingRequiredSelect(bookneticConf)) { + return false + } + + const unmappedFields = bookneticConf?.field_map + ? bookneticConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.bookneticField || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + + return unmappedFields.length === 0 +} + +export const generateMappedField = fields => { + const requiredFlds = fields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ formField: '', bookneticField: field.key })) + : [{ formField: '', bookneticField: '' }] +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx new file mode 100644 index 000000000..14d382e20 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx @@ -0,0 +1,104 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import TagifyInput from '../../Utilities/TagifyInput' +import { + addFieldMap, + delFieldMap, + handleCustomValue, + handleFieldMapping +} from '../GlobalIntegrationHelper' + +export default function BookneticFieldMap({ i, formFields, field, bookneticConf, setBookneticConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = bookneticConf?.bookneticFields?.filter(fld => fld.required === true) || [] + const nonRequiredFlds = bookneticConf?.bookneticFields?.filter(fld => fld.required === false) || [] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, bookneticConf, setBookneticConf)} + label={__('Custom Value', 'bit-integrations')} + className="mr-2" + type="text" + value={field.customValue} + placeholder={__('Custom Value', 'bit-integrations')} + formFields={formFields} + /> + )} + + +
+ {i >= requiredFlds.length && ( + <> + + + + )} +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx new file mode 100644 index 000000000..60f1ced8e --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx @@ -0,0 +1,158 @@ +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' +import BookneticActions from './BookneticActions' +import { generateMappedField, refreshBookneticOptions, selectsForAction } from './BookneticCommonFunc' +import BookneticFieldMap from './BookneticFieldMap' +import { BookneticStaticData, modules, requiredSelects, utilities } from './staticData' + +export default function BookneticIntegLayout({ + formFields, + bookneticConf, + setBookneticConf, + isLoading, + setIsLoading +}) { + const { isPro } = useRecoilValue($appConfigState) + const action = bookneticConf?.mainAction + const actionSelects = selectsForAction(requiredSelects, action) + + const handleMainAction = value => { + setBookneticConf(prevConf => + create(prevConf, draftConf => { + draftConf.mainAction = value + draftConf.bookneticFields = BookneticStaticData[value] ?? [] + draftConf.field_map = generateMappedField(draftConf.bookneticFields) + draftConf.utilities = {} + requiredSelects.forEach(select => delete draftConf[select.key]) + }) + ) + + const sources = new Set( + selectsForAction(requiredSelects, value) + .map(select => select.source) + .filter(Boolean) + ) + sources.forEach(source => refreshBookneticOptions(source, setBookneticConf, setIsLoading)) + } + + const setSelect = (key, val) => { + setBookneticConf(prevConf => + create(prevConf, draftConf => { + draftConf[key] = val + }) + ) + } + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={modules.map(module => ({ + label: checkIsPro(isPro, module.is_pro) ? module.label : getProLabel(module.label), + value: module.name, + disabled: !checkIsPro(isPro, module.is_pro) + }))} + singleSelect + closeOnSelect + /> +
+ + {actionSelects.map(select => ( +
+
+
+ {select.label}: + setSelect(select.key, val)} + singleSelect={!select.multi} + closeOnSelect={!select.multi} + /> + {select.source && ( + + )} +
+
+ ))} + + {isLoading && ( + + )} + + {action && bookneticConf.bookneticFields && ( +
+ {__('Map Fields', 'bit-integrations')} +
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('Booknetic Fields', 'bit-integrations')} +
+
+ + {bookneticConf?.field_map?.map((itm, i) => ( + + ))} +
+ +
+
+
+ )} + + {selectsForAction(utilities, action).length > 0 && ( +
+ {__('Utilities', 'bit-integrations')} +
+ +
+ )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx b/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx new file mode 100644 index 000000000..6aa62ed90 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx @@ -0,0 +1,74 @@ +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router' +import { useRecoilState, useRecoilValue } from 'recoil' +import { $actionConf, $formFields, $newFlow } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' +import { checkMappedFields, handleInput } from './BookneticCommonFunc' +import BookneticIntegLayout from './BookneticIntegLayout' + +export default function EditBooknetic({ allIntegURL }) { + const navigate = useNavigate() + const { id } = useParams() + + const [bookneticConf, setBookneticConf] = useRecoilState($actionConf) + const [flow, setFlow] = useRecoilState($newFlow) + const formFields = useRecoilValue($formFields) + const [isLoading, setIsLoading] = useState(false) + const [snack, setSnackbar] = useState({ show: false }) + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} + handleInput(e, bookneticConf, setBookneticConf)} + name="name" + value={bookneticConf.name} + type="text" + placeholder={__('Integration Name...', 'bit-integrations')} + /> +
+
+ + + + + + + saveActionConf({ + flow, + setFlow, + allIntegURL, + conf: bookneticConf, + navigate, + id, + edit: 1, + setIsLoading, + setSnackbar + }) + } + disabled={!checkMappedFields(bookneticConf)} + isLoading={isLoading} + dataConf={bookneticConf} + setDataConf={setBookneticConf} + formFields={formFields} + /> +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Booknetic/staticData.js b/frontend/src/components/AllIntegrations/Booknetic/staticData.js new file mode 100644 index 000000000..e75facf0e --- /dev/null +++ b/frontend/src/components/AllIntegrations/Booknetic/staticData.js @@ -0,0 +1,538 @@ +import { __ } from '../../../Utils/i18nwrap' + +export const modules = [ + { name: 'create_appointment', label: __('Create Appointment', 'bit-integrations'), is_pro: true }, + { name: 'update_appointment', label: __('Update Appointment', 'bit-integrations'), is_pro: true }, + { name: 'delete_appointment', label: __('Delete Appointment', 'bit-integrations'), is_pro: true }, + { + name: 'reschedule_appointment', + label: __('Reschedule Appointment', 'bit-integrations'), + is_pro: true + }, + { + name: 'change_appointment_status', + label: __('Change Appointment Status', 'bit-integrations'), + is_pro: true + }, + { name: 'update_payment', label: __('Update Payment', 'bit-integrations'), is_pro: true }, + { name: 'complete_payment', label: __('Complete Payment', 'bit-integrations'), is_pro: true }, + { name: 'create_payment_link', label: __('Create Payment Link', 'bit-integrations'), is_pro: true }, + { name: 'create_customer', label: __('Create Customer', 'bit-integrations'), is_pro: true }, + { name: 'update_customer', label: __('Update Customer', 'bit-integrations'), is_pro: true }, + { name: 'delete_customer', label: __('Delete Customer', 'bit-integrations'), is_pro: true }, + { + name: 'assign_customer_category', + label: __('Assign Customer Category', 'bit-integrations'), + is_pro: true + }, + { name: 'create_staff', label: __('Create Staff', 'bit-integrations'), is_pro: true }, + { name: 'update_staff', label: __('Update Staff', 'bit-integrations'), is_pro: true }, + { name: 'delete_staff', label: __('Delete Staff', 'bit-integrations'), is_pro: true }, + { name: 'duplicate_staff', label: __('Duplicate Staff', 'bit-integrations'), is_pro: true }, + { name: 'set_staff_status', label: __('Set Staff Status', 'bit-integrations'), is_pro: true }, + { name: 'create_location', label: __('Create Location', 'bit-integrations'), is_pro: true }, + { name: 'update_location', label: __('Update Location', 'bit-integrations'), is_pro: true }, + { name: 'delete_location', label: __('Delete Location', 'bit-integrations'), is_pro: true }, + { name: 'duplicate_location', label: __('Duplicate Location', 'bit-integrations'), is_pro: true }, + { name: 'set_location_status', label: __('Set Location Status', 'bit-integrations'), is_pro: true }, + { name: 'create_service', label: __('Create Service', 'bit-integrations'), is_pro: true }, + { name: 'update_service', label: __('Update Service', 'bit-integrations'), is_pro: true }, + { name: 'delete_service', label: __('Delete Service', 'bit-integrations'), is_pro: true }, + { name: 'duplicate_service', label: __('Duplicate Service', 'bit-integrations'), is_pro: true }, + { name: 'set_service_status', label: __('Set Service Status', 'bit-integrations'), is_pro: true }, + { + name: 'create_service_extra', + label: __('Create Service Extra', 'bit-integrations'), + is_pro: true + }, + { + name: 'update_service_extra', + label: __('Update Service Extra', 'bit-integrations'), + is_pro: true + }, + { + name: 'delete_service_extra', + label: __('Delete Service Extra', 'bit-integrations'), + is_pro: true + }, + { + name: 'duplicate_service_extra', + label: __('Duplicate Service Extra', 'bit-integrations'), + is_pro: true + }, + { + name: 'create_customer_category', + label: __('Create Customer Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'update_customer_category', + label: __('Update Customer Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'delete_customer_category', + label: __('Delete Customer Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'create_location_category', + label: __('Create Location Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'update_location_category', + label: __('Update Location Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'delete_location_category', + label: __('Delete Location Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'create_service_category', + label: __('Create Service Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'update_service_category', + label: __('Update Service Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'delete_service_category', + label: __('Delete Service Category', 'bit-integrations'), + is_pro: true + }, + { + name: 'create_in_app_notification', + label: __('Create In-App Notification', 'bit-integrations'), + is_pro: true + } +] + +const field = (key, label, required = false) => ({ key, label, required }) + +const appointmentId = field('appointment_id', __('Appointment ID', 'bit-integrations'), true) +const customerId = field('customer_id', __('Customer ID', 'bit-integrations'), true) +const staffId = field('staff_id', __('Staff ID', 'bit-integrations'), true) +const locationId = field('location_id', __('Location ID', 'bit-integrations'), true) +const serviceId = field('service_id', __('Service ID', 'bit-integrations'), true) +const serviceExtraId = field('service_extra_id', __('Service Extra ID', 'bit-integrations'), true) + +const customerFields = required => [ + field('first_name', __('First Name', 'bit-integrations'), required), + field('last_name', __('Last Name', 'bit-integrations'), required), + field('email', __('Email', 'bit-integrations'), required), + field('phone_number', __('Phone Number', 'bit-integrations')), + field('birthdate', __('Birth Date (YYYY-MM-DD)', 'bit-integrations')), + field('notes', __('Notes', 'bit-integrations')) +] + +const staffFields = required => [ + field('name', __('Name', 'bit-integrations'), required), + field('email', __('Email', 'bit-integrations'), required), + field('phone_number', __('Phone Number', 'bit-integrations')), + field('profession', __('Profession', 'bit-integrations')), + field('about', __('About', 'bit-integrations')) +] + +const locationFields = required => [ + field('name', __('Name', 'bit-integrations'), required), + field('address', __('Address', 'bit-integrations')), + field('phone_number', __('Phone Number', 'bit-integrations')), + field('latitude', __('Latitude', 'bit-integrations')), + field('longitude', __('Longitude', 'bit-integrations')), + field('address_components', __('Address Components', 'bit-integrations')), + field('notes', __('Notes', 'bit-integrations')) +] + +const serviceFields = required => [ + field('name', __('Name', 'bit-integrations'), required), + field('price', __('Price', 'bit-integrations'), required), + field('duration', __('Duration in Minutes', 'bit-integrations'), required), + field('timeslot_length', __('Timeslot Length in Minutes', 'bit-integrations')), + field('buffer_before', __('Buffer Before in Minutes', 'bit-integrations')), + field('buffer_after', __('Buffer After in Minutes', 'bit-integrations')), + field('max_capacity', __('Maximum Capacity', 'bit-integrations')), + field('color', __('Color', 'bit-integrations')), + field('deposit', __('Deposit', 'bit-integrations')), + field('notes', __('Notes', 'bit-integrations')) +] + +const serviceExtraFields = required => [ + field('name', __('Name', 'bit-integrations'), required), + field('price', __('Price', 'bit-integrations'), required), + field('min_quantity', __('Minimum Quantity', 'bit-integrations'), required), + field('max_quantity', __('Maximum Quantity', 'bit-integrations'), required), + field('duration', __('Duration in Minutes', 'bit-integrations')), + field('notes', __('Notes', 'bit-integrations')) +] + +const customerCategoryFields = required => [ + field('name', __('Name', 'bit-integrations'), required), + field('color', __('Color', 'bit-integrations')), + field('icon', __('Icon', 'bit-integrations')), + field('note', __('Note', 'bit-integrations')) +] + +const nameField = field('name', __('Name', 'bit-integrations'), true) + +export const BookneticStaticData = { + create_appointment: [ + field('starts_at', __('Starts At (YYYY-MM-DD HH:MM)', 'bit-integrations'), true), + field('weight', __('Number of Customers', 'bit-integrations')), + field('paid_amount', __('Paid Amount', 'bit-integrations')), + field('note', __('Note', 'bit-integrations')) + ], + update_appointment: [ + appointmentId, + field('starts_at', __('Starts At (YYYY-MM-DD HH:MM)', 'bit-integrations')), + field('weight', __('Number of Customers', 'bit-integrations')), + field('note', __('Note', 'bit-integrations')) + ], + delete_appointment: [appointmentId], + reschedule_appointment: [ + appointmentId, + field('date', __('Date (YYYY-MM-DD)', 'bit-integrations'), true), + field('time', __('Time (HH:MM)', 'bit-integrations'), true) + ], + change_appointment_status: [appointmentId], + update_payment: [ + appointmentId, + field('paid_amount', __('Paid Amount', 'bit-integrations')), + field('prices', __('Prices (JSON)', 'bit-integrations')) + ], + complete_payment: [appointmentId], + create_payment_link: [appointmentId], + create_customer: customerFields(true), + update_customer: [customerId, ...customerFields(false)], + delete_customer: [customerId], + assign_customer_category: [customerId], + create_staff: staffFields(true), + update_staff: [staffId, ...staffFields(false)], + delete_staff: [staffId], + duplicate_staff: [staffId], + set_staff_status: [staffId], + create_location: locationFields(true), + update_location: [locationId, ...locationFields(false)], + delete_location: [locationId], + duplicate_location: [locationId], + set_location_status: [locationId], + create_service: serviceFields(true), + update_service: [serviceId, ...serviceFields(false)], + delete_service: [serviceId], + duplicate_service: [serviceId], + set_service_status: [serviceId], + create_service_extra: serviceExtraFields(true), + update_service_extra: [serviceExtraId, ...serviceExtraFields(false)], + delete_service_extra: [serviceExtraId], + duplicate_service_extra: [serviceExtraId], + create_customer_category: customerCategoryFields(true), + update_customer_category: [ + field('customer_category_id', __('Customer Category ID', 'bit-integrations'), true), + ...customerCategoryFields(false) + ], + delete_customer_category: [ + field('customer_category_id', __('Customer Category ID', 'bit-integrations'), true) + ], + create_location_category: [nameField], + update_location_category: [ + field('location_category_id', __('Location Category ID', 'bit-integrations'), true), + nameField + ], + delete_location_category: [ + field('location_category_id', __('Location Category ID', 'bit-integrations'), true) + ], + create_service_category: [nameField], + update_service_category: [ + field('service_category_id', __('Service Category ID', 'bit-integrations'), true), + nameField + ], + delete_service_category: [ + field('service_category_id', __('Service Category ID', 'bit-integrations'), true) + ], + create_in_app_notification: [ + field('title', __('Title', 'bit-integrations'), true), + field('message', __('Message', 'bit-integrations'), true), + field('action_type', __('Action Type', 'bit-integrations')), + field('action_data', __('Action Data', 'bit-integrations')) + ] +} + +export const paymentStatusOptions = [ + { label: __('Not Paid', 'bit-integrations'), value: 'not_paid' }, + { label: __('Paid', 'bit-integrations'), value: 'paid' }, + { label: __('Pending', 'bit-integrations'), value: 'pending' }, + { label: __('Canceled', 'bit-integrations'), value: 'canceled' } +] + +export const activeStatusOptions = [ + { label: __('Active', 'bit-integrations'), value: '1' }, + { label: __('Inactive', 'bit-integrations'), value: '0' } +] + +export const yesNoOptions = [ + { label: __('Yes', 'bit-integrations'), value: '1' }, + { label: __('No', 'bit-integrations'), value: '0' } +] + +export const genderOptions = [ + { label: __('Male', 'bit-integrations'), value: 'male' }, + { label: __('Female', 'bit-integrations'), value: 'female' } +] + +export const depositTypeOptions = [ + { label: __('Percent', 'bit-integrations'), value: 'percent' }, + { label: __('Fixed Amount', 'bit-integrations'), value: 'price' } +] + +// `source` names a list that refresh_booknetic_options returns; `options` is a fixed list. +export const requiredSelects = [ + { + key: 'selectedLocation', + label: __('Location', 'bit-integrations'), + source: 'locations', + actions: ['create_appointment'] + }, + { + key: 'selectedService', + label: __('Service', 'bit-integrations'), + source: 'services', + actions: ['create_appointment'] + }, + { + key: 'selectedStaff', + label: __('Staff', 'bit-integrations'), + source: 'staff', + actions: ['create_appointment'] + }, + { + key: 'selectedCustomer', + label: __('Customer', 'bit-integrations'), + source: 'customers', + actions: ['create_appointment'] + }, + { + key: 'selectedStatus', + label: __('Status', 'bit-integrations'), + source: 'statuses', + actions: ['create_appointment', 'change_appointment_status'] + }, + { + key: 'selectedPaymentStatus', + label: __('Payment Status', 'bit-integrations'), + options: paymentStatusOptions, + actions: ['update_payment'] + }, + { + key: 'selectedPaymentGateway', + label: __('Payment Gateway', 'bit-integrations'), + source: 'payment_gateways', + actions: ['create_payment_link'] + }, + { + key: 'selectedCustomerCategory', + label: __('Customer Category', 'bit-integrations'), + source: 'customer_categories', + actions: ['assign_customer_category'] + }, + { + key: 'selectedLocations', + label: __('Locations', 'bit-integrations'), + source: 'locations', + multi: true, + actions: ['create_staff'] + }, + { + key: 'selectedServiceCategory', + label: __('Service Category', 'bit-integrations'), + source: 'service_categories', + actions: ['create_service'] + }, + { + key: 'selectedActiveStatus', + label: __('Status', 'bit-integrations'), + options: activeStatusOptions, + actions: ['set_staff_status', 'set_location_status', 'set_service_status'] + }, + { + key: 'selectedUser', + label: __('WordPress User', 'bit-integrations'), + source: 'users', + actions: ['create_in_app_notification'] + } +] + +const utility = (key, title, subTitle, actions, extra = {}) => ({ + key, + title, + subTitle, + actions, + ...extra +}) + +export const utilities = [ + utility( + 'selected_payment_method', + __('Payment Method', 'bit-integrations'), + __('Set the payment method', 'bit-integrations'), + ['create_appointment'], + { source: 'payment_gateways' } + ), + utility( + 'selected_payment_status', + __('Payment Status', 'bit-integrations'), + __('Set the payment status', 'bit-integrations'), + ['create_appointment'], + { options: paymentStatusOptions } + ), + utility( + 'selected_location', + __('Location', 'bit-integrations'), + __('Change the location', 'bit-integrations'), + ['update_appointment'], + { source: 'locations' } + ), + utility( + 'selected_service', + __('Service', 'bit-integrations'), + __('Change the service', 'bit-integrations'), + ['update_appointment'], + { source: 'services' } + ), + utility( + 'selected_staff', + __('Staff', 'bit-integrations'), + __('Change the staff member', 'bit-integrations'), + ['update_appointment', 'reschedule_appointment'], + { source: 'staff' } + ), + utility( + 'selected_customer', + __('Customer', 'bit-integrations'), + __('Change the customer', 'bit-integrations'), + ['update_appointment'], + { source: 'customers' } + ), + utility( + 'selected_status', + __('Status', 'bit-integrations'), + __('Change the status', 'bit-integrations'), + ['update_appointment'], + { source: 'statuses' } + ), + utility( + 'selected_reset_status', + __('Reset Status', 'bit-integrations'), + __('Set the status back to the default', 'bit-integrations'), + ['reschedule_appointment'], + { options: yesNoOptions } + ), + utility( + 'selected_gender', + __('Gender', 'bit-integrations'), + __('Set the gender', 'bit-integrations'), + ['create_customer', 'update_customer'], + { options: genderOptions } + ), + utility( + 'selected_customer_category', + __('Customer Category', 'bit-integrations'), + __('Set the customer category', 'bit-integrations'), + ['create_customer', 'update_customer'], + { source: 'customer_categories' } + ), + utility( + 'selected_locations', + __('Locations', 'bit-integrations'), + __('Set the locations', 'bit-integrations'), + ['update_staff'], + { source: 'locations', multi: true } + ), + utility( + 'selected_services', + __('Services', 'bit-integrations'), + __('Set the services', 'bit-integrations'), + ['create_staff', 'update_staff'], + { source: 'services', multi: true } + ), + utility( + 'selected_user', + __('WordPress User', 'bit-integrations'), + __('Link a WordPress user', 'bit-integrations'), + ['create_customer', 'update_customer', 'create_staff', 'update_staff'], + { source: 'users' } + ), + utility( + 'selected_location_category', + __('Location Category', 'bit-integrations'), + __('Set the location category', 'bit-integrations'), + ['create_location', 'update_location'], + { source: 'location_categories' } + ), + utility( + 'selected_staff_members', + __('Staff', 'bit-integrations'), + __('Set the staff members', 'bit-integrations'), + ['create_service'], + { source: 'staff', multi: true } + ), + utility( + 'selected_service_category', + __('Service Category', 'bit-integrations'), + __('Change the service category', 'bit-integrations'), + ['update_service'], + { source: 'service_categories' } + ), + utility( + 'selected_deposit_type', + __('Deposit Type', 'bit-integrations'), + __('Set the deposit type', 'bit-integrations'), + ['create_service', 'update_service'], + { options: depositTypeOptions } + ), + utility( + 'selected_service', + __('Service', 'bit-integrations'), + __('Set the service', 'bit-integrations'), + ['create_service_extra', 'update_service_extra'], + { source: 'services' } + ), + utility( + 'selected_service', + __('Destination Service', 'bit-integrations'), + __('Add the copy to this service', 'bit-integrations'), + ['duplicate_service_extra'], + { source: 'services' } + ), + utility( + 'selected_extra_category', + __('Extra Category', 'bit-integrations'), + __('Set the service extra category', 'bit-integrations'), + ['create_service_extra', 'update_service_extra'], + { source: 'extra_categories' } + ), + utility( + 'selected_active_status', + __('Status', 'bit-integrations'), + __('Set the status', 'bit-integrations'), + ['update_service_extra'], + { options: activeStatusOptions } + ), + utility( + 'selected_is_default', + __('Default Category', 'bit-integrations'), + __('Make this the default category', 'bit-integrations'), + ['create_customer_category', 'update_customer_category'], + { options: yesNoOptions } + ), + utility( + 'selected_parent_category', + __('Parent Category', 'bit-integrations'), + __('Set the parent category', 'bit-integrations'), + ['create_service_category', 'update_service_category'], + { source: 'service_categories' } + ) +] diff --git a/frontend/src/resource/img/integ/booknetic.webp b/frontend/src/resource/img/integ/booknetic.webp new file mode 100644 index 0000000000000000000000000000000000000000..7b696babb9157921f592171c9f1172a0936cfd8d GIT binary patch literal 1016 zcmV)f$D3ZKrq)O%QtDa3tZc;fo=M~>d!Chco@*UXm`rzAb=%P@E|NrDz#4jX zjv`3?ARRbla$GS|Ut&F-t5Qo*FI*Gmor#1PVQOPck0Qf5);GAV47W7mm&HX0(uq$b zlEBo;(E%NwNr2AQ933BygD$iZIvxpcdlYmJQlJYijn3+djz_>74R0L0N${q@8-UJM z9i3ZN?JV6c#nX;;U*n2yIDvE2LFbth-L-`1Hbz1>hQPTM1!oL*1$Q(NVe>9W*vx5> z!8y;YpE0<3<|w#6YrNjpD}O9xpL!h~k^vht-|w3Y92*x~$Hm5MJ5b358?$HUDBw7F zLTqsCSds!9TL-d$V?L4sowXxYXHwk$I#pW(J7kK08j^ej$Lz@C2lD!Cg+R48ObU%ej!=@dm%K^f3G%?3vo4VA9B> zy-V>id)<_axU;%%`83h!zls1>P&goT0RR9n4FH`1Du4ih06vjAnMkFhBB3pv2@tRn z2~FFp$;CUpVT|AofE&mjfE^{@x!GF9?Pob?Gb9K+aI&=Nqk#-IA%!}%MWT|kdK>8z zYk5k$&mVXQ0&TEJ8l?g%+0_CRfX}=@$6+ORxuFvHJdNfY0092`K_UNHvXTEe-={RA zgZ3ABst5la{tf^C7J7ZequO#y)9>~OtIuWF6#v9&8ou>o-i3~mEs35Yf9@}bKWAqD zp_Knn2+;kLq&~m%8Uv^OAj!YP?LX!_U@oaL%`Hh<%sDSvTjp=~i2m!b-<^1`M3sl0 zju8HCxT8rw#IH^u0T0ayxqtz3njYO7YHIWM;&j0vf~RPv)(VZ!tu@6A%c|T1X$@ZB z2l;lHFxX?J59@+f@ m56%D1r=O1Dck{(SsZs4<`5}rxO{q*7s6ND$*G^gsNB{sqSm{0h literal 0 HcmV?d00001 From 518b4dd13c5601a367000a90fb4c6a07e6148ea3 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 09:56:45 +0600 Subject: [PATCH 04/10] feat(booknetic): register Booknetic in the action lists --- frontend/src/Utils/StaticData/tutorialLinks.js | 4 ++++ frontend/src/components/AllIntegrations/EditInteg.jsx | 3 +++ frontend/src/components/AllIntegrations/IntegInfo.jsx | 3 +++ frontend/src/components/AllIntegrations/NewInteg.jsx | 10 ++++++++++ frontend/src/components/Flow/New/SelectAction.jsx | 1 + 5 files changed, 21 insertions(+) diff --git a/frontend/src/Utils/StaticData/tutorialLinks.js b/frontend/src/Utils/StaticData/tutorialLinks.js index b263affda..418b779b6 100644 --- a/frontend/src/Utils/StaticData/tutorialLinks.js +++ b/frontend/src/Utils/StaticData/tutorialLinks.js @@ -795,6 +795,10 @@ const tutorialLinks = { youTubeLink: '', docLink: 'https://bit-integrations.com/wp-docs/actions/bookly-integration-as-an-action/' }, + booknetic: { + youTubeLink: '', + docLink: 'https://bit-integrations.com/wp-docs/actions/booknetic-integration-as-an-action/' + }, fluentCart: { youTubeLink: '', docLink: 'https://bit-integrations.com/wp-docs/actions/fluentcart-integration-as-an-action/' diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index 23de76679..c62cdca84 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -187,6 +187,7 @@ const EditSureContact = lazy(() => import('./SureContact/EditSureContact')) const EditBrilliantDirectories = lazy(() => import('./BrilliantDirectories/EditBrilliantDirectories')) const EditFlodesk = lazy(() => import('./Flodesk/EditFlodesk')) const EditFluentCart = lazy(() => import('./FluentCart/EditFluentCart')) +const EditBooknetic = lazy(() => import('./Booknetic/EditBooknetic')) const EditBookingsAndAppointmentsForWoocommerce = lazy( () => import('./BookingsAndAppointmentsForWoocommerce/EditBookingsAndAppointmentsForWoocommerce') ) @@ -691,6 +692,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'FluentCart': return + case 'Booknetic': + return case 'RoxAppointmentBooking': return case 'Bookings And Appointments For Woocommerce': diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index 48a6f5923..8c2c607b0 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -191,6 +191,7 @@ const BrilliantDirectoriesAuthorization = lazy( ) const FlodeskAuthorization = lazy(() => import('./Flodesk/FlodeskAuthorization')) const FluentCartAuthorization = lazy(() => import('./FluentCart/FluentCartAuthorization')) +const BookneticAuthorization = lazy(() => import('./Booknetic/BookneticAuthorization')) const BookingsAndAppointmentsForWoocommerceAuthorization = lazy( () => import('./BookingsAndAppointmentsForWoocommerce/BookingsAndAppointmentsForWoocommerceAuthorization') @@ -716,6 +717,8 @@ const IntegrationInfo = memo(({ integrationConf, location, editUrl }) => { return case 'FluentCart': return + case 'Booknetic': + return case 'Bookings And Appointments For Woocommerce': return ( import('./SureContact/SureContact')) const BrilliantDirectories = lazy(() => import('./BrilliantDirectories/BrilliantDirectories')) const Flodesk = lazy(() => import('./Flodesk/Flodesk')) const FluentCart = lazy(() => import('./FluentCart/FluentCart')) +const Booknetic = lazy(() => import('./Booknetic/Booknetic')) const BookingsAndAppointmentsForWoocommerce = lazy( () => import('./BookingsAndAppointmentsForWoocommerce/BookingsAndAppointmentsForWoocommerce') ) @@ -1834,6 +1835,15 @@ const NewIntegs = memo(({ integUrlName, allIntegURL, flow, setFlow }) => { setFlow={setFlow} /> ) + case 'Booknetic': + return ( + + ) case 'Bookings And Appointments For Woocommerce': return ( Date: Sun, 18 Oct 2026 09:57:25 +0600 Subject: [PATCH 05/10] fix(booknetic): let flows with a Booknetic trigger open in edit mode SetEditIntegComponents only loads the custom form trigger fields for entities in customFormIntegrations. --- frontend/src/Utils/StaticData/webhookIntegrations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Utils/StaticData/webhookIntegrations.js b/frontend/src/Utils/StaticData/webhookIntegrations.js index c2654f740..1d205b333 100644 --- a/frontend/src/Utils/StaticData/webhookIntegrations.js +++ b/frontend/src/Utils/StaticData/webhookIntegrations.js @@ -107,6 +107,7 @@ export const customFormIntegrations = [ 'Wsms', 'WebbaBooking', 'WpSimpleBookingCalendar', + 'Booknetic', 'B2BKing', 'FormyChat', 'MainWP', From 44617cfe8e4d117bf9eb14821c027ac587fa5084 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 10:09:56 +0600 Subject: [PATCH 06/10] fix(booknetic): log Booknetic errors and cap the option lists Booknetic throws on limits and invalid slots. The flow now logs the message instead of failing the request that fired the trigger. Option lists stop at 500 rows. --- .../Actions/Booknetic/BookneticController.php | 2 +- backend/Actions/Booknetic/BookneticHelper.php | 8 +- .../Actions/Booknetic/BookneticService.php | 265 +++++++++--------- 3 files changed, 141 insertions(+), 134 deletions(-) diff --git a/backend/Actions/Booknetic/BookneticController.php b/backend/Actions/Booknetic/BookneticController.php index 836221838..0c1825f9e 100644 --- a/backend/Actions/Booknetic/BookneticController.php +++ b/backend/Actions/Booknetic/BookneticController.php @@ -10,7 +10,7 @@ public function execute($integrationData, $fieldValues) { $integrationDetails = $integrationData->flow_details; $integId = $integrationData->id; - $fieldMap = $integrationDetails->field_map; + $fieldMap = $integrationDetails->field_map ?? []; $utilities = $integrationDetails->utilities ?? []; if (empty($fieldMap)) { diff --git a/backend/Actions/Booknetic/BookneticHelper.php b/backend/Actions/Booknetic/BookneticHelper.php index e65dd0372..0ee52e63f 100644 --- a/backend/Actions/Booknetic/BookneticHelper.php +++ b/backend/Actions/Booknetic/BookneticHelper.php @@ -4,6 +4,8 @@ class BookneticHelper { + private const OPTION_LIMIT = 500; + private const MODEL_OPTIONS = [ 'locations' => 'BookneticApp\Models\Location', 'services' => 'BookneticApp\Models\Service', @@ -65,7 +67,7 @@ private static function modelOptions($modelClass) $options = []; - foreach ($modelClass::query()->orderBy('id DESC')->fetchAll() as $record) { + foreach ($modelClass::query()->orderBy('id DESC')->limit(self::OPTION_LIMIT)->fetchAll() as $record) { $label = (string) ($record->name ?? ''); $options[] = [ 'value' => (string) $record->id, @@ -84,7 +86,7 @@ private static function customerOptions() $options = []; - foreach (\BookneticApp\Models\Customer::query()->orderBy('id DESC')->fetchAll() as $customer) { + foreach (\BookneticApp\Models\Customer::query()->orderBy('id DESC')->limit(self::OPTION_LIMIT)->fetchAll() as $customer) { $name = trim($customer->first_name . ' ' . $customer->last_name); $label = $name !== '' ? $name : '#' . (int) $customer->id; @@ -132,7 +134,7 @@ private static function userOptions() { $options = []; - foreach (get_users(['fields' => ['ID', 'display_name', 'user_email']]) as $user) { + foreach (get_users(['fields' => ['ID', 'display_name', 'user_email'], 'number' => self::OPTION_LIMIT]) as $user) { $options[] = [ 'value' => (string) $user->ID, 'label' => ($user->display_name ?: $user->user_email) . ' (' . $user->user_email . ')', diff --git a/backend/Actions/Booknetic/BookneticService.php b/backend/Actions/Booknetic/BookneticService.php index f99a4c59d..3acae6fcf 100644 --- a/backend/Actions/Booknetic/BookneticService.php +++ b/backend/Actions/Booknetic/BookneticService.php @@ -6,6 +6,7 @@ use BitApps\Integrations\Core\Util\Common; use BitApps\Integrations\Core\Util\Hooks; use BitApps\Integrations\Log\LogHandler; +use Throwable; class BookneticService { @@ -37,219 +38,223 @@ public function execute($fieldValues, $fieldMap, $utilities) 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro'), ]; - switch ($mainAction) { - case 'create_appointment': - $response = Hooks::apply(Config::withPrefix('booknetic_create_appointment'), $default, $fieldData, $utilities, $integrationDetails); + try { + switch ($mainAction) { + case 'create_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_create_appointment'), $default, $fieldData, $utilities, $integrationDetails); - break; + break; - case 'update_appointment': - $response = Hooks::apply(Config::withPrefix('booknetic_update_appointment'), $default, $fieldData, $utilities); + case 'update_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_update_appointment'), $default, $fieldData, $utilities); - break; + break; - case 'delete_appointment': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_appointment'), $default, $fieldData); + case 'delete_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_appointment'), $default, $fieldData); - break; + break; - case 'reschedule_appointment': - $response = Hooks::apply(Config::withPrefix('booknetic_reschedule_appointment'), $default, $fieldData, $utilities); + case 'reschedule_appointment': + $response = Hooks::apply(Config::withPrefix('booknetic_reschedule_appointment'), $default, $fieldData, $utilities); - break; + break; - case 'change_appointment_status': - $response = Hooks::apply(Config::withPrefix('booknetic_change_appointment_status'), $default, $fieldData, $integrationDetails); + case 'change_appointment_status': + $response = Hooks::apply(Config::withPrefix('booknetic_change_appointment_status'), $default, $fieldData, $integrationDetails); - break; + break; - case 'update_payment': - $response = Hooks::apply(Config::withPrefix('booknetic_update_payment'), $default, $fieldData, $integrationDetails); + case 'update_payment': + $response = Hooks::apply(Config::withPrefix('booknetic_update_payment'), $default, $fieldData, $integrationDetails); - break; + break; - case 'complete_payment': - $response = Hooks::apply(Config::withPrefix('booknetic_complete_payment'), $default, $fieldData); + case 'complete_payment': + $response = Hooks::apply(Config::withPrefix('booknetic_complete_payment'), $default, $fieldData); - break; + break; - case 'create_payment_link': - $response = Hooks::apply(Config::withPrefix('booknetic_create_payment_link'), $default, $fieldData, $integrationDetails); + case 'create_payment_link': + $response = Hooks::apply(Config::withPrefix('booknetic_create_payment_link'), $default, $fieldData, $integrationDetails); - break; + break; - case 'create_customer': - $response = Hooks::apply(Config::withPrefix('booknetic_create_customer'), $default, $fieldData, $utilities); + case 'create_customer': + $response = Hooks::apply(Config::withPrefix('booknetic_create_customer'), $default, $fieldData, $utilities); - break; + break; - case 'update_customer': - $response = Hooks::apply(Config::withPrefix('booknetic_update_customer'), $default, $fieldData, $utilities); + case 'update_customer': + $response = Hooks::apply(Config::withPrefix('booknetic_update_customer'), $default, $fieldData, $utilities); - break; + break; - case 'delete_customer': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_customer'), $default, $fieldData); + case 'delete_customer': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_customer'), $default, $fieldData); - break; + break; - case 'assign_customer_category': - $response = Hooks::apply(Config::withPrefix('booknetic_assign_customer_category'), $default, $fieldData, $integrationDetails); + case 'assign_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_assign_customer_category'), $default, $fieldData, $integrationDetails); - break; + break; - case 'create_staff': - $response = Hooks::apply(Config::withPrefix('booknetic_create_staff'), $default, $fieldData, $utilities, $integrationDetails); + case 'create_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_create_staff'), $default, $fieldData, $utilities, $integrationDetails); - break; + break; - case 'update_staff': - $response = Hooks::apply(Config::withPrefix('booknetic_update_staff'), $default, $fieldData, $utilities); + case 'update_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_update_staff'), $default, $fieldData, $utilities); - break; + break; - case 'delete_staff': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_staff'), $default, $fieldData); + case 'delete_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_staff'), $default, $fieldData); - break; + break; - case 'duplicate_staff': - $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_staff'), $default, $fieldData); + case 'duplicate_staff': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_staff'), $default, $fieldData); - break; + break; - case 'set_staff_status': - $response = Hooks::apply(Config::withPrefix('booknetic_set_staff_status'), $default, $fieldData, $integrationDetails); + case 'set_staff_status': + $response = Hooks::apply(Config::withPrefix('booknetic_set_staff_status'), $default, $fieldData, $integrationDetails); - break; + break; - case 'create_location': - $response = Hooks::apply(Config::withPrefix('booknetic_create_location'), $default, $fieldData, $utilities); + case 'create_location': + $response = Hooks::apply(Config::withPrefix('booknetic_create_location'), $default, $fieldData, $utilities); - break; + break; - case 'update_location': - $response = Hooks::apply(Config::withPrefix('booknetic_update_location'), $default, $fieldData, $utilities); + case 'update_location': + $response = Hooks::apply(Config::withPrefix('booknetic_update_location'), $default, $fieldData, $utilities); - break; + break; - case 'delete_location': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_location'), $default, $fieldData); + case 'delete_location': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_location'), $default, $fieldData); - break; + break; - case 'duplicate_location': - $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_location'), $default, $fieldData); + case 'duplicate_location': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_location'), $default, $fieldData); - break; + break; - case 'set_location_status': - $response = Hooks::apply(Config::withPrefix('booknetic_set_location_status'), $default, $fieldData, $integrationDetails); + case 'set_location_status': + $response = Hooks::apply(Config::withPrefix('booknetic_set_location_status'), $default, $fieldData, $integrationDetails); - break; + break; - case 'create_service': - $response = Hooks::apply(Config::withPrefix('booknetic_create_service'), $default, $fieldData, $utilities, $integrationDetails); + case 'create_service': + $response = Hooks::apply(Config::withPrefix('booknetic_create_service'), $default, $fieldData, $utilities, $integrationDetails); - break; + break; - case 'update_service': - $response = Hooks::apply(Config::withPrefix('booknetic_update_service'), $default, $fieldData, $utilities); + case 'update_service': + $response = Hooks::apply(Config::withPrefix('booknetic_update_service'), $default, $fieldData, $utilities); - break; + break; - case 'delete_service': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_service'), $default, $fieldData); + case 'delete_service': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_service'), $default, $fieldData); - break; + break; - case 'duplicate_service': - $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_service'), $default, $fieldData); + case 'duplicate_service': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_service'), $default, $fieldData); - break; + break; - case 'set_service_status': - $response = Hooks::apply(Config::withPrefix('booknetic_set_service_status'), $default, $fieldData, $integrationDetails); + case 'set_service_status': + $response = Hooks::apply(Config::withPrefix('booknetic_set_service_status'), $default, $fieldData, $integrationDetails); - break; + break; - case 'create_service_extra': - $response = Hooks::apply(Config::withPrefix('booknetic_create_service_extra'), $default, $fieldData, $utilities); + case 'create_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_create_service_extra'), $default, $fieldData, $utilities); - break; + break; - case 'update_service_extra': - $response = Hooks::apply(Config::withPrefix('booknetic_update_service_extra'), $default, $fieldData, $utilities); + case 'update_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_update_service_extra'), $default, $fieldData, $utilities); - break; + break; - case 'delete_service_extra': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_service_extra'), $default, $fieldData); + case 'delete_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_service_extra'), $default, $fieldData); - break; + break; - case 'duplicate_service_extra': - $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_service_extra'), $default, $fieldData, $utilities); + case 'duplicate_service_extra': + $response = Hooks::apply(Config::withPrefix('booknetic_duplicate_service_extra'), $default, $fieldData, $utilities); - break; + break; - case 'create_customer_category': - $response = Hooks::apply(Config::withPrefix('booknetic_create_customer_category'), $default, $fieldData, $utilities); + case 'create_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_create_customer_category'), $default, $fieldData, $utilities); - break; + break; - case 'update_customer_category': - $response = Hooks::apply(Config::withPrefix('booknetic_update_customer_category'), $default, $fieldData, $utilities); + case 'update_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_update_customer_category'), $default, $fieldData, $utilities); - break; + break; - case 'delete_customer_category': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_customer_category'), $default, $fieldData); + case 'delete_customer_category': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_customer_category'), $default, $fieldData); - break; + break; - case 'create_location_category': - $response = Hooks::apply(Config::withPrefix('booknetic_create_location_category'), $default, $fieldData); + case 'create_location_category': + $response = Hooks::apply(Config::withPrefix('booknetic_create_location_category'), $default, $fieldData); - break; + break; - case 'update_location_category': - $response = Hooks::apply(Config::withPrefix('booknetic_update_location_category'), $default, $fieldData); + case 'update_location_category': + $response = Hooks::apply(Config::withPrefix('booknetic_update_location_category'), $default, $fieldData); - break; + break; - case 'delete_location_category': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_location_category'), $default, $fieldData); + case 'delete_location_category': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_location_category'), $default, $fieldData); - break; + break; - case 'create_service_category': - $response = Hooks::apply(Config::withPrefix('booknetic_create_service_category'), $default, $fieldData, $utilities); + case 'create_service_category': + $response = Hooks::apply(Config::withPrefix('booknetic_create_service_category'), $default, $fieldData, $utilities); - break; + break; - case 'update_service_category': - $response = Hooks::apply(Config::withPrefix('booknetic_update_service_category'), $default, $fieldData, $utilities); + case 'update_service_category': + $response = Hooks::apply(Config::withPrefix('booknetic_update_service_category'), $default, $fieldData, $utilities); - break; + break; - case 'delete_service_category': - $response = Hooks::apply(Config::withPrefix('booknetic_delete_service_category'), $default, $fieldData); + case 'delete_service_category': + $response = Hooks::apply(Config::withPrefix('booknetic_delete_service_category'), $default, $fieldData); - break; + break; - case 'create_in_app_notification': - $response = Hooks::apply(Config::withPrefix('booknetic_create_in_app_notification'), $default, $fieldData, $integrationDetails); + case 'create_in_app_notification': + $response = Hooks::apply(Config::withPrefix('booknetic_create_in_app_notification'), $default, $fieldData, $integrationDetails); - break; + break; - default: - $response = [ - 'success' => false, - 'message' => __('Invalid action', 'bit-integrations'), - ]; + default: + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations'), + ]; - break; + break; + } + } catch (Throwable $e) { + $response = ['success' => false, 'message' => $e->getMessage()]; } $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; From 4429e6b5d950c24aef38adf9ac4d0b09048456d5 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 10:09:56 +0600 Subject: [PATCH 07/10] feat(booknetic): let flows map the customer, staff and other IDs Create Appointment and Create In-App Notification accept a mapped ID in place of the list choice, and each action shows a short note on formats and limits. Option lists load on demand and are no longer saved in the flow. Parallel list loads share one loader. --- .../AllIntegrations/Booknetic/Booknetic.jsx | 13 ++++- .../Booknetic/BookneticCommonFunc.js | 13 ++++- .../Booknetic/BookneticIntegLayout.jsx | 57 +++++++++++++++++-- .../Booknetic/EditBooknetic.jsx | 4 +- .../AllIntegrations/Booknetic/staticData.js | 48 +++++++++++++++- 5 files changed, 123 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx b/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx index 2ccbdf102..4c31f6b0d 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx @@ -7,7 +7,7 @@ import SnackMsg from '../../Utilities/SnackMsg' import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' import BookneticAuthorization from './BookneticAuthorization' -import { checkMappedFields, missingRequiredSelect } from './BookneticCommonFunc' +import { checkMappedFields, missingRequiredSelect, withoutOptionLists } from './BookneticCommonFunc' import BookneticIntegLayout from './BookneticIntegLayout' export default function Booknetic({ formFields, setFlow, flow, allIntegURL }) { @@ -112,7 +112,16 @@ export default function Booknetic({ formFields, setFlow, flow, allIntegURL }) { - saveIntegConfig(flow, setFlow, allIntegURL, bookneticConf, navigate, '', '', setIsLoading) + saveIntegConfig( + flow, + setFlow, + allIntegURL, + withoutOptionLists(bookneticConf), + navigate, + '', + '', + setIsLoading + ) } isLoading={isLoading} dataConf={bookneticConf} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js b/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js index 8990a85ae..2ef6eda91 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js @@ -16,7 +16,7 @@ export const handleInput = (e, bookneticConf, setBookneticConf) => { export const refreshBookneticOptions = (type, setBookneticConf, setIsLoading) => { setIsLoading(true) - bitsFetch({ type }, 'refresh_booknetic_options') + return bitsFetch({ type }, 'refresh_booknetic_options') .then(result => { setIsLoading(false) @@ -37,11 +37,20 @@ export const refreshBookneticOptions = (type, setBookneticConf, setIsLoading) => .catch(() => setIsLoading(false)) } +export const withoutOptionLists = bookneticConf => + create(bookneticConf, draftConf => { + delete draftConf.default + }) + export const selectsForAction = (selects, action) => selects.filter(s => s.actions.includes(action)) +export const isFieldMapped = (bookneticConf, key) => + !!bookneticConf?.field_map?.some(map => map.bookneticField === key && map.formField) + export const missingRequiredSelect = bookneticConf => selectsForAction(requiredSelects, bookneticConf?.mainAction).find( - select => !bookneticConf?.[select.key] + select => + !bookneticConf?.[select.key] && !(select.mapKey && isFieldMapped(bookneticConf, select.mapKey)) ) export const checkMappedFields = bookneticConf => { diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx index 60f1ced8e..bd06eaa4a 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx @@ -1,15 +1,22 @@ +import { useEffect } from 'react' import { create } from 'mutative' import MultiSelect from 'react-multiple-select-dropdown-lite' import { useRecoilValue } from 'recoil' import { $appConfigState } from '../../../GlobalStates' -import { __ } from '../../../Utils/i18nwrap' +import { __, sprintf } from '../../../Utils/i18nwrap' import Loader from '../../Loaders/Loader' import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' import BookneticActions from './BookneticActions' -import { generateMappedField, refreshBookneticOptions, selectsForAction } from './BookneticCommonFunc' +import { + generateMappedField, + isFieldMapped, + refreshBookneticOptions, + selectsForAction +} from './BookneticCommonFunc' import BookneticFieldMap from './BookneticFieldMap' -import { BookneticStaticData, modules, requiredSelects, utilities } from './staticData' +import Note from '../../Utilities/Note' +import { actionNotes, BookneticStaticData, modules, requiredSelects, utilities } from './staticData' export default function BookneticIntegLayout({ formFields, @@ -22,6 +29,28 @@ export default function BookneticIntegLayout({ const action = bookneticConf?.mainAction const actionSelects = selectsForAction(requiredSelects, action) + const loadSources = sources => { + if (!sources.size) { + return + } + + setIsLoading(true) + Promise.all( + [...sources].map(source => refreshBookneticOptions(source, setBookneticConf, () => {})) + ).finally(() => setIsLoading(false)) + } + + useEffect(() => { + loadSources( + new Set( + actionSelects + .map(select => select.source) + .filter(source => source && !bookneticConf?.default?.[source]) + ) + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + const handleMainAction = value => { setBookneticConf(prevConf => create(prevConf, draftConf => { @@ -38,9 +67,12 @@ export default function BookneticIntegLayout({ .map(select => select.source) .filter(Boolean) ) - sources.forEach(source => refreshBookneticOptions(source, setBookneticConf, setIsLoading)) + loadSources(sources) } + const mapLabel = key => + (BookneticStaticData[action] ?? []).find(fieldDef => fieldDef.key === key)?.label ?? key + const setSelect = (key, val) => { setBookneticConf(prevConf => create(prevConf, draftConf => { @@ -94,6 +126,21 @@ export default function BookneticIntegLayout({ )}
+ {select.mapKey && ( + + {isFieldMapped(bookneticConf, select.mapKey) + ? sprintf( + // translators: %s: field label + __('The mapped %s field is used.', 'bit-integrations'), + mapLabel(select.mapKey) + ) + : sprintf( + // translators: %s: field label + __('Or map the %s field below to use a value from the trigger.', 'bit-integrations'), + mapLabel(select.mapKey) + )} + + )}
))} @@ -146,6 +193,8 @@ export default function BookneticIntegLayout({
)} + {actionNotes[action] && } + {selectsForAction(utilities, action).length > 0 && (
{__('Utilities', 'bit-integrations')} diff --git a/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx b/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx index 6aa62ed90..9a83e9dda 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/EditBooknetic.jsx @@ -7,7 +7,7 @@ import SnackMsg from '../../Utilities/SnackMsg' import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' -import { checkMappedFields, handleInput } from './BookneticCommonFunc' +import { checkMappedFields, handleInput, withoutOptionLists } from './BookneticCommonFunc' import BookneticIntegLayout from './BookneticIntegLayout' export default function EditBooknetic({ allIntegURL }) { @@ -54,7 +54,7 @@ export default function EditBooknetic({ allIntegURL }) { flow, setFlow, allIntegURL, - conf: bookneticConf, + conf: withoutOptionLists(bookneticConf), navigate, id, edit: 1, diff --git a/frontend/src/components/AllIntegrations/Booknetic/staticData.js b/frontend/src/components/AllIntegrations/Booknetic/staticData.js index e75facf0e..5f4fc56b8 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/staticData.js +++ b/frontend/src/components/AllIntegrations/Booknetic/staticData.js @@ -182,6 +182,10 @@ const nameField = field('name', __('Name', 'bit-integrations'), true) export const BookneticStaticData = { create_appointment: [ field('starts_at', __('Starts At (YYYY-MM-DD HH:MM)', 'bit-integrations'), true), + field('customer_id', __('Customer ID', 'bit-integrations')), + field('location_id', __('Location ID', 'bit-integrations')), + field('service_id', __('Service ID', 'bit-integrations')), + field('staff_id', __('Staff ID', 'bit-integrations')), field('weight', __('Number of Customers', 'bit-integrations')), field('paid_amount', __('Paid Amount', 'bit-integrations')), field('note', __('Note', 'bit-integrations')) @@ -256,11 +260,45 @@ export const BookneticStaticData = { create_in_app_notification: [ field('title', __('Title', 'bit-integrations'), true), field('message', __('Message', 'bit-integrations'), true), - field('action_type', __('Action Type', 'bit-integrations')), - field('action_data', __('Action Data', 'bit-integrations')) + field('user_id', __('WordPress User ID', 'bit-integrations')) ] } +export const actionNotes = { + create_appointment: __( + 'Map Starts At as YYYY-MM-DD HH:MM. The staff member must work at the location, give the service and be free at that time.', + 'bit-integrations' + ), + update_appointment: __( + 'Map only the fields to change. Map Starts At as YYYY-MM-DD HH:MM.', + 'bit-integrations' + ), + reschedule_appointment: __( + 'Map Date as YYYY-MM-DD and Time as HH:MM. The staff member must be free at that time.', + 'bit-integrations' + ), + update_payment: __( + 'Prices is a JSON object with one value for each price key of the appointment, for example {"service_price": 50}.', + 'bit-integrations' + ), + update_customer: __('Map only the fields to change.', 'bit-integrations'), + update_staff: __('Map only the fields to change.', 'bit-integrations'), + update_location: __('Map only the fields to change.', 'bit-integrations'), + update_service: __('Map only the fields to change.', 'bit-integrations'), + update_service_extra: __('Map only the fields to change.', 'bit-integrations'), + update_customer_category: __('Map only the fields to change.', 'bit-integrations'), + delete_customer: __('Booknetic does not delete a customer that has appointments.', 'bit-integrations'), + delete_staff: __( + 'Booknetic does not delete a staff member that has appointments.', + 'bit-integrations' + ), + delete_location: __( + 'Booknetic does not delete a location that has staff members or appointments.', + 'bit-integrations' + ), + delete_service: __('Booknetic does not delete a service that has appointments.', 'bit-integrations') +} + export const paymentStatusOptions = [ { label: __('Not Paid', 'bit-integrations'), value: 'not_paid' }, { label: __('Paid', 'bit-integrations'), value: 'paid' }, @@ -289,27 +327,32 @@ export const depositTypeOptions = [ ] // `source` names a list that refresh_booknetic_options returns; `options` is a fixed list. +// A select with `mapKey` is optional when the field map maps that key instead. export const requiredSelects = [ { key: 'selectedLocation', + mapKey: 'location_id', label: __('Location', 'bit-integrations'), source: 'locations', actions: ['create_appointment'] }, { key: 'selectedService', + mapKey: 'service_id', label: __('Service', 'bit-integrations'), source: 'services', actions: ['create_appointment'] }, { key: 'selectedStaff', + mapKey: 'staff_id', label: __('Staff', 'bit-integrations'), source: 'staff', actions: ['create_appointment'] }, { key: 'selectedCustomer', + mapKey: 'customer_id', label: __('Customer', 'bit-integrations'), source: 'customers', actions: ['create_appointment'] @@ -359,6 +402,7 @@ export const requiredSelects = [ }, { key: 'selectedUser', + mapKey: 'user_id', label: __('WordPress User', 'bit-integrations'), source: 'users', actions: ['create_in_app_notification'] From 2a8409afd7aa021485482cf74dea9d778862ace3 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 10:24:00 +0600 Subject: [PATCH 08/10] fix(booknetic): drop Create Payment Link Core Booknetic gateways return no link, so the action always failed. Only paid add-on gateways create links. --- backend/Actions/Booknetic/BookneticService.php | 5 ----- .../components/AllIntegrations/Booknetic/staticData.js | 8 -------- 2 files changed, 13 deletions(-) diff --git a/backend/Actions/Booknetic/BookneticService.php b/backend/Actions/Booknetic/BookneticService.php index 3acae6fcf..ce1d4973e 100644 --- a/backend/Actions/Booknetic/BookneticService.php +++ b/backend/Actions/Booknetic/BookneticService.php @@ -75,11 +75,6 @@ public function execute($fieldValues, $fieldMap, $utilities) break; - case 'create_payment_link': - $response = Hooks::apply(Config::withPrefix('booknetic_create_payment_link'), $default, $fieldData, $integrationDetails); - - break; - case 'create_customer': $response = Hooks::apply(Config::withPrefix('booknetic_create_customer'), $default, $fieldData, $utilities); diff --git a/frontend/src/components/AllIntegrations/Booknetic/staticData.js b/frontend/src/components/AllIntegrations/Booknetic/staticData.js index 5f4fc56b8..cf0c6bd1c 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/staticData.js +++ b/frontend/src/components/AllIntegrations/Booknetic/staticData.js @@ -16,7 +16,6 @@ export const modules = [ }, { name: 'update_payment', label: __('Update Payment', 'bit-integrations'), is_pro: true }, { name: 'complete_payment', label: __('Complete Payment', 'bit-integrations'), is_pro: true }, - { name: 'create_payment_link', label: __('Create Payment Link', 'bit-integrations'), is_pro: true }, { name: 'create_customer', label: __('Create Customer', 'bit-integrations'), is_pro: true }, { name: 'update_customer', label: __('Update Customer', 'bit-integrations'), is_pro: true }, { name: 'delete_customer', label: __('Delete Customer', 'bit-integrations'), is_pro: true }, @@ -209,7 +208,6 @@ export const BookneticStaticData = { field('prices', __('Prices (JSON)', 'bit-integrations')) ], complete_payment: [appointmentId], - create_payment_link: [appointmentId], create_customer: customerFields(true), update_customer: [customerId, ...customerFields(false)], delete_customer: [customerId], @@ -369,12 +367,6 @@ export const requiredSelects = [ options: paymentStatusOptions, actions: ['update_payment'] }, - { - key: 'selectedPaymentGateway', - label: __('Payment Gateway', 'bit-integrations'), - source: 'payment_gateways', - actions: ['create_payment_link'] - }, { key: 'selectedCustomerCategory', label: __('Customer Category', 'bit-integrations'), From fce04ffa87cb92a918e9ffc26bc9cecdfa9dbc5d Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 10:34:12 +0600 Subject: [PATCH 09/10] fix(booknetic): show saved choices and the field map when editing a flow The select reads its value only on mount, so it now remounts after its options load. Field definitions come from staticData instead of the copy saved in the flow. Utilities show the chosen value under each option. --- .../Booknetic/BookneticActions.jsx | 23 +++++++++++++++++-- .../Booknetic/BookneticFieldMap.jsx | 6 +++-- .../Booknetic/BookneticIntegLayout.jsx | 11 +++++---- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx index 4081b041d..a866857e4 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { create } from 'mutative' import MultiSelect from 'react-multiple-select-dropdown-lite' import 'react-multiple-select-dropdown-lite/dist/index.css' -import { __ } from '../../../Utils/i18nwrap' +import { __, sprintf } from '../../../Utils/i18nwrap' import Loader from '../../Loaders/Loader' import ConfirmModal from '../../Utilities/ConfirmModal' import TableCheckBox from '../../Utilities/TableCheckBox' @@ -49,6 +49,17 @@ export default function BookneticActions({ bookneticConf, setBookneticConf }) { const options = item => item.options ?? bookneticConf?.default?.[item.source] ?? [] + const selectedLabels = item => { + const values = String(bookneticConf?.utilities?.[item.key] ?? '') + .split(',') + .filter(Boolean) + const labels = values + .map(value => options(item).find(option => option.value === value)?.label) + .filter(Boolean) + + return labels.length === values.length && labels.length ? labels.join(', ') : '' + } + return (
{actionUtilities.map(item => ( @@ -59,7 +70,15 @@ export default function BookneticActions({ bookneticConf, setBookneticConf }) { className="wdt-200 mt-4 mr-2" value={item.key} title={item.title} - subTitle={item.subTitle} + subTitle={ + selectedLabels(item) + ? sprintf( + // translators: %s: selected option labels + __('Selected: %s', 'bit-integrations'), + selectedLabels(item) + ) + : item.subTitle + } /> ))} diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx index 14d382e20..22679442a 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx @@ -3,6 +3,7 @@ import { $appConfigState } from '../../../GlobalStates' import { __, sprintf } from '../../../Utils/i18nwrap' import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' import TagifyInput from '../../Utilities/TagifyInput' +import { BookneticStaticData } from './staticData' import { addFieldMap, delFieldMap, @@ -14,8 +15,9 @@ export default function BookneticFieldMap({ i, formFields, field, bookneticConf, const btcbi = useRecoilValue($appConfigState) const { isPro } = btcbi - const requiredFlds = bookneticConf?.bookneticFields?.filter(fld => fld.required === true) || [] - const nonRequiredFlds = bookneticConf?.bookneticFields?.filter(fld => fld.required === false) || [] + const fieldDefs = BookneticStaticData[bookneticConf?.mainAction] ?? [] + const requiredFlds = fieldDefs.filter(fld => fld.required === true) + const nonRequiredFlds = fieldDefs.filter(fld => fld.required === false) return (
diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx index bd06eaa4a..4c8195c1d 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx @@ -55,8 +55,7 @@ export default function BookneticIntegLayout({ setBookneticConf(prevConf => create(prevConf, draftConf => { draftConf.mainAction = value - draftConf.bookneticFields = BookneticStaticData[value] ?? [] - draftConf.field_map = generateMappedField(draftConf.bookneticFields) + draftConf.field_map = generateMappedField(BookneticStaticData[value] ?? []) draftConf.utilities = {} requiredSelects.forEach(select => delete draftConf[select.key]) }) @@ -70,6 +69,9 @@ export default function BookneticIntegLayout({ loadSources(sources) } + // The select reads defaultValue only on mount, so it remounts once its options arrive. + const selectOptions = select => select.options ?? bookneticConf?.default?.[select.source] ?? [] + const mapLabel = key => (BookneticStaticData[action] ?? []).find(fieldDef => fieldDef.key === key)?.label ?? key @@ -110,7 +112,8 @@ export default function BookneticIntegLayout({ title={select.key} defaultValue={bookneticConf?.[select.key] ?? null} className="btcd-paper-drpdwn w-5" - options={select.options ?? bookneticConf?.default?.[select.source] ?? []} + key={selectOptions(select).length} + options={selectOptions(select)} onChange={val => setSelect(select.key, val)} singleSelect={!select.multi} closeOnSelect={!select.multi} @@ -156,7 +159,7 @@ export default function BookneticIntegLayout({ /> )} - {action && bookneticConf.bookneticFields && ( + {action && BookneticStaticData[action] && (
{__('Map Fields', 'bit-integrations')}
From 48f23f42b90ebf190357bc27416fd5ef5dfeed79 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sun, 18 Oct 2026 11:10:31 +0600 Subject: [PATCH 10/10] fix(booknetic): replace the Prices JSON field with Service Price Update Payment asked for a JSON object of every price key, which a flow cannot build. It now takes a Service Price number. Also removes explanatory comments from the Booknetic files. --- .../AllIntegrations/Booknetic/BookneticIntegLayout.jsx | 1 - .../src/components/AllIntegrations/Booknetic/staticData.js | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx index 4c8195c1d..70b554061 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx @@ -69,7 +69,6 @@ export default function BookneticIntegLayout({ loadSources(sources) } - // The select reads defaultValue only on mount, so it remounts once its options arrive. const selectOptions = select => select.options ?? bookneticConf?.default?.[select.source] ?? [] const mapLabel = key => diff --git a/frontend/src/components/AllIntegrations/Booknetic/staticData.js b/frontend/src/components/AllIntegrations/Booknetic/staticData.js index cf0c6bd1c..dd355c105 100644 --- a/frontend/src/components/AllIntegrations/Booknetic/staticData.js +++ b/frontend/src/components/AllIntegrations/Booknetic/staticData.js @@ -205,7 +205,7 @@ export const BookneticStaticData = { update_payment: [ appointmentId, field('paid_amount', __('Paid Amount', 'bit-integrations')), - field('prices', __('Prices (JSON)', 'bit-integrations')) + field('service_price', __('Service Price', 'bit-integrations')) ], complete_payment: [appointmentId], create_customer: customerFields(true), @@ -276,7 +276,7 @@ export const actionNotes = { 'bit-integrations' ), update_payment: __( - 'Prices is a JSON object with one value for each price key of the appointment, for example {"service_price": 50}.', + 'Service Price is the total for all customers of the appointment. Other prices stay the same.', 'bit-integrations' ), update_customer: __('Map only the fields to change.', 'bit-integrations'), @@ -324,8 +324,6 @@ export const depositTypeOptions = [ { label: __('Fixed Amount', 'bit-integrations'), value: 'price' } ] -// `source` names a list that refresh_booknetic_options returns; `options` is a fixed list. -// A select with `mapKey` is optional when the field map maps that key instead. export const requiredSelects = [ { key: 'selectedLocation',