diff --git a/backend/Actions/BookingPress/BookingPressController.php b/backend/Actions/BookingPress/BookingPressController.php new file mode 100644 index 000000000..6ceef1895 --- /dev/null +++ b/backend/Actions/BookingPress/BookingPressController.php @@ -0,0 +1,37 @@ +flow_details; + $integId = $integrationData->id; + $fieldMap = $integrationDetails->field_map; + + if (empty($fieldMap)) { + return new WP_Error('field_map_empty', __('Field map is empty', 'bit-integrations')); + } + + return (new RecordApiHelper($integrationDetails, $integId))->execute($fieldValues, $fieldMap); + } +} diff --git a/backend/Actions/BookingPress/RecordApiHelper.php b/backend/Actions/BookingPress/RecordApiHelper.php new file mode 100644 index 000000000..0d0147085 --- /dev/null +++ b/backend/Actions/BookingPress/RecordApiHelper.php @@ -0,0 +1,118 @@ +_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + public function execute($fieldValues, $fieldMap) + { + if (!class_exists('BookingPress')) { + return [ + 'success' => false, + 'message' => __('BookingPress is not installed or activated', 'bit-integrations'), + ]; + } + + $fieldData = $this->generateReqDataFromFieldMap($fieldMap, $fieldValues); + $mainAction = $this->_integrationDetails->mainAction ?? 'cancel_appointment'; + + $defaultResponse = [ + 'success' => false, + // translators: %s: Plugin name + 'message' => wp_sprintf(__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integrations Pro'), + ]; + + switch ($mainAction) { + case 'cancel_appointment': + $response = Hooks::apply(Config::withPrefix('bookingpress_cancel_appointment'), $defaultResponse, $fieldData); + $type = 'appointment'; + $actionType = 'cancel_appointment'; + + break; + + case 'update_appointment_status': + $response = Hooks::apply(Config::withPrefix('bookingpress_update_appointment_status'), $defaultResponse, $fieldData); + $type = 'appointment'; + $actionType = 'update_appointment_status'; + + break; + + case 'create_customer': + $response = Hooks::apply(Config::withPrefix('bookingpress_create_customer'), $defaultResponse, $fieldData); + $type = 'customer'; + $actionType = 'create_customer'; + + break; + + case 'update_customer': + $response = Hooks::apply(Config::withPrefix('bookingpress_update_customer'), $defaultResponse, $fieldData); + $type = 'customer'; + $actionType = 'update_customer'; + + break; + + case 'delete_appointment': + $response = Hooks::apply(Config::withPrefix('bookingpress_delete_appointment'), $defaultResponse, $fieldData); + $type = 'appointment'; + $actionType = 'delete_appointment'; + + break; + + case 'delete_customer': + $response = Hooks::apply(Config::withPrefix('bookingpress_delete_customer'), $defaultResponse, $fieldData); + $type = 'customer'; + $actionType = 'delete_customer'; + + break; + + default: + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations'), + ]; + $type = 'BookingPress'; + $actionType = 'unknown'; + + break; + } + + $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; + LogHandler::save($this->_integrationID, ['type' => $type, 'type_name' => $actionType], $responseType, $response); + + return $response; + } + + private function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $dataFinal = []; + foreach ($fieldMap as $item) { + if (empty($item->formField) || empty($item->bookingPressField)) { + continue; + } + + $triggerValue = $item->formField; + $actionValue = $item->bookingPressField; + + $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) + ? Common::replaceFieldWithValue($item->customValue, $fieldValues) + : $fieldValues[$triggerValue] ?? ''; + } + + return $dataFinal; + } +} diff --git a/backend/Actions/BookingPress/Routes.php b/backend/Actions/BookingPress/Routes.php new file mode 100644 index 000000000..e4e17c50e --- /dev/null +++ b/backend/Actions/BookingPress/Routes.php @@ -0,0 +1,10 @@ + ['name' => 'Buddypress', 'isPro' => true, 'is_active' => false], 'BbPress' => ['name' => 'bbPress', 'isPro' => true, 'is_active' => false], 'BookingCalendarContactForm' => ['name' => 'Booking Calendar Contact Form', 'isPro' => true, 'is_active' => false], + 'BookingPress' => ['name' => 'BookingPress', 'isPro' => true, 'is_active' => false], 'CalculatedFieldsForm' => ['name' => 'Calculated Fields Form Pro', 'isPro' => true, 'is_active' => false], 'CartFlow' => ['name' => 'CartFlow', 'isPro' => true, 'is_active' => false], 'CustomTrigger' => ['name' => 'Custom Trigger', 'isPro' => true, 'is_active' => false], diff --git a/frontend/src/Utils/StaticData/tutorialLinks.js b/frontend/src/Utils/StaticData/tutorialLinks.js index 97d2258b4..26d7e34e2 100644 --- a/frontend/src/Utils/StaticData/tutorialLinks.js +++ b/frontend/src/Utils/StaticData/tutorialLinks.js @@ -670,6 +670,10 @@ const tutorialLinks = { userRegistrationMembership: { youTubeLink: '', docLink: 'https://bit-integrations.com/wp-docs/actions/user-registration-and-membership-as-action/' + }, + bookingPress: { + youTubeLink: '', + docLink: '' } } export default tutorialLinks diff --git a/frontend/src/Utils/StaticData/webhookIntegrations.js b/frontend/src/Utils/StaticData/webhookIntegrations.js index 69b3101d6..ff9562767 100644 --- a/frontend/src/Utils/StaticData/webhookIntegrations.js +++ b/frontend/src/Utils/StaticData/webhookIntegrations.js @@ -94,6 +94,7 @@ export const customFormIntegrations = [ 'WeDocs', 'UserRegistrationMembership', 'UltimateAffiliatePro', + 'BookingPress', ] export const actionHookIntegrations = ['ActionHook'] diff --git a/frontend/src/components/AllIntegrations/BookingPress/BookingPress.jsx b/frontend/src/components/AllIntegrations/BookingPress/BookingPress.jsx new file mode 100644 index 000000000..5287ab414 --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/BookingPress.jsx @@ -0,0 +1,102 @@ +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 { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import BookingPressAuthorization from './BookingPressAuthorization' +import { checkMappedFields } from './BookingPressCommonFunc' +import BookingPressIntegLayout from './BookingPressIntegLayout' + +export default function BookingPress({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + const [bookingPressConf, setBookingPressConf] = useState({ + name: 'BookingPress', + type: 'BookingPress', + field_map: [{ formField: '', bookingPressField: '' }], + mainAction: '', + }) + + const nextPage = val => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (val === 3) { + if (!checkMappedFields(bookingPressConf)) { + setSnackbar({ + show: true, + msg: __('Please map all required fields to continue.', 'bit-integrations'), + }) + return + } + + if (bookingPressConf.name !== '' && bookingPressConf.field_map.length > 0) { + setStep(val) + } + } else { + setStep(val) + } + } + + return ( +
+ +
+ + {/* STEP 1 */} + + + {/* STEP 2 */} +
+ +
+
+
+ +
+ + {/* STEP 3 */} + + saveIntegConfig(flow, setFlow, allIntegURL, bookingPressConf, navigate, '', '', setIsLoading) + } + isLoading={isLoading} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/BookingPress/BookingPressAuthorization.jsx b/frontend/src/components/AllIntegrations/BookingPress/BookingPressAuthorization.jsx new file mode 100644 index 000000000..7929f40b4 --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/BookingPressAuthorization.jsx @@ -0,0 +1,112 @@ +import { useState } from 'react' +import BackIcn from '../../../Icons/BackIcn' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' +import LoaderSm from '../../Loaders/LoaderSm' +import TutorialLink from '../../Utilities/TutorialLink' +import tutorialLinks from '../../../Utils/StaticData/tutorialLinks' + +export default function BookingPressAuthorization({ + bookingPressConf, + setBookingPressConf, + step, + nextPage, + isLoading, + setIsLoading, + setSnackbar +}) { + const [isAuthorized, setIsAuthorized] = useState(false) + const [showAuthMsg, setShowAuthMsg] = useState(false) + + const authorizeHandler = () => { + setIsLoading(true) + bitsFetch({}, 'bookingpress_authorize').then(result => { + if (result?.success) { + setIsAuthorized(true) + setSnackbar({ + show: true, + msg: __('Connected with BookingPress Successfully', 'bit-integrations'), + }) + } + setIsLoading(false) + setShowAuthMsg(true) + }) + } + + const handleInput = e => { + const newConf = { ...bookingPressConf } + newConf[e.target.name] = e.target.value + setBookingPressConf(newConf) + } + + return ( +
+ +
+ {__('Integration Name:', 'bit-integrations')} +
+ + + {isLoading === 'auth' && ( +
+ + {__('Checking if BookingPress is authorized!!!', 'bit-integrations')} +
+ )} + + {showAuthMsg && !isAuthorized && !isLoading && ( +
+
+
+ +
+
+ {__('BookingPress is not activated or not installed', 'bit-integrations')} +
+
+
+ )} + + {showAuthMsg && isAuthorized && !isLoading && ( +
+
+ +
+
{__('BookingPress is activated', 'bit-integrations')}
+
+ )} + + +
+ +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/BookingPress/BookingPressCommonFunc.js b/frontend/src/components/AllIntegrations/BookingPress/BookingPressCommonFunc.js new file mode 100644 index 000000000..4214ffe32 --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/BookingPressCommonFunc.js @@ -0,0 +1,32 @@ +import { create } from 'mutative' + +export const handleInput = (e, bookingPressConf, setBookingPressConf) => { + const { name, value } = e.target + setBookingPressConf(prevConf => + create(prevConf, draftConf => { + draftConf[name] = value + }) + ) +} + +export const checkMappedFields = bookingPressConf => { + const mappedFields = bookingPressConf?.field_map + ? bookingPressConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.bookingPressField || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + return mappedFields.length === 0 +} + +export const generateMappedField = fields => { + const requiredFlds = fields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ + formField: '', + bookingPressField: field.key, + })) + : [{ formField: '', bookingPressField: '' }] +} diff --git a/frontend/src/components/AllIntegrations/BookingPress/BookingPressFieldMap.jsx b/frontend/src/components/AllIntegrations/BookingPress/BookingPressFieldMap.jsx new file mode 100644 index 000000000..67f2a59a9 --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/BookingPressFieldMap.jsx @@ -0,0 +1,110 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import TagifyInput from '../../Utilities/TagifyInput' +import { + addFieldMap, + delFieldMap, + handleCustomValue, + handleFieldMapping, +} from '../GlobalIntegrationHelper' + +export default function BookingPressFieldMap({ + i, + formFields, + field, + bookingPressConf, + setBookingPressConf, +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = bookingPressConf?.bookingPressFields?.filter(fld => fld.required === true) || [] + const nonRequiredFlds = bookingPressConf?.bookingPressFields?.filter(fld => fld.required === false) || [] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, bookingPressConf, setBookingPressConf)} + 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/BookingPress/BookingPressIntegLayout.jsx b/frontend/src/components/AllIntegrations/BookingPress/BookingPressIntegLayout.jsx new file mode 100644 index 000000000..fd76bd2f0 --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/BookingPressIntegLayout.jsx @@ -0,0 +1,119 @@ +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' +import { generateMappedField } from './BookingPressCommonFunc' +import BookingPressFieldMap from './BookingPressFieldMap' +import { + appointmentIdField, + createCustomerFields, + deleteCustomerFields, + modules, + updateAppointmentStatusFields, + updateCustomerFields +} from './staticData' + +export default function BookingPressIntegLayout({ + formFields, + bookingPressConf, + setBookingPressConf, + setSnackbar, +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const handleMainAction = value => { + setBookingPressConf(prevConf => + create(prevConf, draftConf => { + draftConf.mainAction = value + + switch (value) { + case 'cancel_appointment': + draftConf.bookingPressFields = appointmentIdField + break + case 'update_appointment_status': + draftConf.bookingPressFields = updateAppointmentStatusFields + break + case 'create_customer': + draftConf.bookingPressFields = createCustomerFields + break + case 'update_customer': + draftConf.bookingPressFields = updateCustomerFields + break + case 'delete_appointment': + draftConf.bookingPressFields = appointmentIdField + break + case 'delete_customer': + draftConf.bookingPressFields = deleteCustomerFields + break + default: + draftConf.bookingPressFields = [] + } + + draftConf.field_map = generateMappedField(draftConf.bookingPressFields) + }) + ) + } + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={modules?.map(action => ({ + label: checkIsPro(isPro, action.is_pro) ? action.label : getProLabel(action.label), + value: action.name, + disabled: !checkIsPro(isPro, action.is_pro), + }))} + singleSelect + closeOnSelect + /> +
+ + {bookingPressConf?.mainAction && bookingPressConf.bookingPressFields && ( +
+ {__('Map Fields', 'bit-integrations')} +
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('BookingPress Fields', 'bit-integrations')} +
+
+ + {bookingPressConf?.field_map?.map((itm, i) => ( + + ))} +
+ +
+
+
+ )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/BookingPress/EditBookingPress.jsx b/frontend/src/components/AllIntegrations/BookingPress/EditBookingPress.jsx new file mode 100644 index 000000000..ed12785ae --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/EditBookingPress.jsx @@ -0,0 +1,75 @@ +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router' +import { useRecoilState, useRecoilValue } from 'recoil' +import { $actionConf, $formFields, $newFlow } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' +import { checkMappedFields, handleInput } from './BookingPressCommonFunc' +import BookingPressIntegLayout from './BookingPressIntegLayout' + +export default function EditBookingPress({ allIntegURL }) { + const navigate = useNavigate() + const { id } = useParams() + + const [bookingPressConf, setBookingPressConf] = 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, bookingPressConf, setBookingPressConf)} + name="name" + value={bookingPressConf.name} + type="text" + placeholder={__('Integration Name...', 'bit-integrations')} + /> +
+
+ + + + + + + saveActionConf({ + flow, + setFlow, + allIntegURL, + conf: bookingPressConf, + navigate, + id, + edit: 1, + setIsLoading, + setSnackbar, + }) + } + disabled={!checkMappedFields(bookingPressConf)} + isLoading={isLoading} + dataConf={bookingPressConf} + setDataConf={setBookingPressConf} + formFields={formFields} + /> +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/BookingPress/staticData.js b/frontend/src/components/AllIntegrations/BookingPress/staticData.js new file mode 100644 index 000000000..bb65e5c0e --- /dev/null +++ b/frontend/src/components/AllIntegrations/BookingPress/staticData.js @@ -0,0 +1,38 @@ +import { __ } from '../../../Utils/i18nwrap' + +export const modules = [ + { name: 'cancel_appointment', label: __('Cancel Appointment', 'bit-integrations'), is_pro: true }, + { name: 'update_appointment_status', label: __('Update Appointment Status', '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_appointment', label: __('Delete Appointment', 'bit-integrations'), is_pro: true }, + { name: 'delete_customer', label: __('Delete Customer', 'bit-integrations'), is_pro: true }, +] + +export const appointmentIdField = [ + { key: 'appointment_id', label: __('Appointment ID', 'bit-integrations'), required: true }, +] + +export const updateAppointmentStatusFields = [ + { key: 'appointment_id', label: __('Appointment ID', 'bit-integrations'), required: true }, + { key: 'status', label: __('Status', 'bit-integrations'), required: true }, +] + +export const createCustomerFields = [ + { key: 'first_name', label: __('First Name', 'bit-integrations'), required: true }, + { key: 'last_name', label: __('Last Name', 'bit-integrations'), required: true }, + { key: 'email', label: __('Email', 'bit-integrations'), required: true }, + { key: 'phone', label: __('Phone', 'bit-integrations'), required: false }, +] + +export const updateCustomerFields = [ + { key: 'customer_id', label: __('Customer ID', 'bit-integrations'), required: true }, + { key: 'bookingpress_user_firstname', label: __('First Name', 'bit-integrations'), required: false }, + { key: 'bookingpress_user_lastname', label: __('Last Name', 'bit-integrations'), required: false }, + { key: 'bookingpress_user_email', label: __('Email', 'bit-integrations'), required: false }, + { key: 'bookingpress_user_phone', label: __('Phone', 'bit-integrations'), required: false }, +] + +export const deleteCustomerFields = [ + { key: 'customer_id', label: __('Customer ID', 'bit-integrations'), required: true }, +] diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index c4201bb07..f3a0cf09e 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -74,6 +74,7 @@ const EditLearnDash = lazy(() => import('./LearnDash/EditLearnDash')) const EditRestrictContent = lazy(() => import('./RestrictContent/EditRestrictContent')) const EditAffiliate = lazy(() => import('./Affiliate/EditAffiliate')) const EditBuddyBoss = lazy(() => import('./BuddyBoss/EditBuddyBoss')) +const EditBookingPress = lazy(() => import('./BookingPress/EditBookingPress')) const EditGoogleContacts = lazy(() => import('./GoogleContacts/EditGoogleContacts')) const EditKirimEmail = lazy(() => import('./KirimEmail/EditKirimEmail')) const EditGamiPress = lazy(() => import('./GamiPress/EditGamiPress')) @@ -395,6 +396,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'BuddyBoss': return + case 'BookingPress': + return case 'GamiPress': return case 'Google Contacts': diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index 03de88b5b..04756add1 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -175,6 +175,7 @@ const UltimateAffiliateProAuthorization = lazy( () => import('./UltimateAffiliatePro/UltimateAffiliateProAuthorization') ) const FluentCartAuthorization = lazy(() => import('./FluentCart/FluentCartAuthorization')) +const BookingPressAuthorization = lazy(() => import('./BookingPress/BookingPressAuthorization')) const PeepSoAuthorization = lazy(() => import('./PeepSo/PeepSoAuthorization')) const NinjaTablesAuthorization = lazy(() => import('./NinjaTables/NinjaTablesAuthorization')) const WCAffiliateAuthorization = lazy(() => import('./WCAffiliate/WCAffiliateAuthorization')) @@ -637,6 +638,8 @@ export default function IntegInfo() { ) case 'FluentCart': return + case 'BookingPress': + return case 'PeepSo': return case 'Ninja Tables': diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index 505955d35..f849fa767 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -75,6 +75,7 @@ const WhatsApp = lazy(() => import('./WhatsApp/WhatsApp')) const LearnDesh = lazy(() => import('./LearnDash/LearnDash')) const Affiliate = lazy(() => import('./Affiliate/Affiliate')) const BuddyBoss = lazy(() => import('./BuddyBoss/BuddyBoss')) +const BookingPress = lazy(() => import('./BookingPress/BookingPress')) const GoogleContacts = lazy(() => import('./GoogleContacts/GoogleContacts')) const KirimEmail = lazy(() => import('./KirimEmail/KirimEmail')) const Salesforce = lazy(() => import('./Salesforce/Salesforce')) @@ -767,6 +768,15 @@ export default function NewInteg({ allIntegURL }) { setFlow={setFlow} /> ) + case 'BookingPress': + return ( + + ) case 'GamiPress': return (