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 (