diff --git a/backend/Actions/Booknetic/BookneticController.php b/backend/Actions/Booknetic/BookneticController.php
new file mode 100644
index 000000000..0c1825f9e
--- /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..0ee52e63f
--- /dev/null
+++ b/backend/Actions/Booknetic/BookneticHelper.php
@@ -0,0 +1,146 @@
+ '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')->limit(self::OPTION_LIMIT)->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')->limit(self::OPTION_LIMIT)->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'], 'number' => self::OPTION_LIMIT]) 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..ce1d4973e
--- /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'),
+ ];
+
+ try {
+ 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_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;
+ }
+ } catch (Throwable $e) {
+ $response = ['success' => false, 'message' => $e->getMessage()];
+ }
+
+ $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 @@
+ ['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],
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/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',
diff --git a/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx b/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx
new file mode 100644
index 000000000..4c31f6b0d
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Booknetic/Booknetic.jsx
@@ -0,0 +1,133 @@
+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, withoutOptionLists } 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,
+ withoutOptionLists(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..a866857e4
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticActions.jsx
@@ -0,0 +1,135 @@
+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 { __, sprintf } 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] ?? []
+
+ 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 => (
+
handleToggle(e, item)}
+ className="wdt-200 mt-4 mr-2"
+ value={item.key}
+ title={item.title}
+ subTitle={
+ selectedLabels(item)
+ ? sprintf(
+ // translators: %s: selected option labels
+ __('Selected: %s', 'bit-integrations'),
+ selectedLabels(item)
+ )
+ : 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..2ef6eda91
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticCommonFunc.js
@@ -0,0 +1,78 @@
+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)
+ return 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 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.mapKey && isFieldMapped(bookneticConf, select.mapKey))
+ )
+
+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..22679442a
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticFieldMap.jsx
@@ -0,0 +1,106 @@
+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 { BookneticStaticData } from './staticData'
+import {
+ addFieldMap,
+ delFieldMap,
+ handleCustomValue,
+ handleFieldMapping
+} from '../GlobalIntegrationHelper'
+
+export default function BookneticFieldMap({ i, formFields, field, bookneticConf, setBookneticConf }) {
+ const btcbi = useRecoilValue($appConfigState)
+ const { isPro } = btcbi
+
+ const fieldDefs = BookneticStaticData[bookneticConf?.mainAction] ?? []
+ const requiredFlds = fieldDefs.filter(fld => fld.required === true)
+ const nonRequiredFlds = fieldDefs.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..70b554061
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Booknetic/BookneticIntegLayout.jsx
@@ -0,0 +1,209 @@
+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 { __, 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,
+ isFieldMapped,
+ refreshBookneticOptions,
+ selectsForAction
+} from './BookneticCommonFunc'
+import BookneticFieldMap from './BookneticFieldMap'
+import Note from '../../Utilities/Note'
+import { actionNotes, 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 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 => {
+ draftConf.mainAction = value
+ draftConf.field_map = generateMappedField(BookneticStaticData[value] ?? [])
+ draftConf.utilities = {}
+ requiredSelects.forEach(select => delete draftConf[select.key])
+ })
+ )
+
+ const sources = new Set(
+ selectsForAction(requiredSelects, value)
+ .map(select => select.source)
+ .filter(Boolean)
+ )
+ loadSources(sources)
+ }
+
+ const selectOptions = select => select.options ?? bookneticConf?.default?.[select.source] ?? []
+
+ const mapLabel = key =>
+ (BookneticStaticData[action] ?? []).find(fieldDef => fieldDef.key === key)?.label ?? key
+
+ 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 && (
+
+ )}
+
+ {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)
+ )}
+
+ )}
+
+ ))}
+
+ {isLoading && (
+
+ )}
+
+ {action && BookneticStaticData[action] && (
+
+
{__('Map Fields', 'bit-integrations')}
+
+
+
+ {__('Form Fields', 'bit-integrations')}
+
+
+ {__('Booknetic Fields', 'bit-integrations')}
+
+
+
+ {bookneticConf?.field_map?.map((itm, i) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+ {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
new file mode 100644
index 000000000..9a83e9dda
--- /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, withoutOptionLists } 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: withoutOptionLists(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..dd355c105
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Booknetic/staticData.js
@@ -0,0 +1,572 @@
+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_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('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'))
+ ],
+ 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('service_price', __('Service Price', 'bit-integrations'))
+ ],
+ complete_payment: [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('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: __(
+ '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'),
+ 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' },
+ { 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' }
+]
+
+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']
+ },
+ {
+ 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: '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',
+ mapKey: 'user_id',
+ 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/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 (