From 87c4e4a4ff58c375bf94ee8508e61cb1a53c04ed Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sat, 18 Jul 2026 18:43:43 +0600 Subject: [PATCH 1/2] feat(latepoint): add LatePoint action integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the LatePoint write actions from Bit-Pi: create/update/cancel booking, create agent, create customer, create order, and create/update coupon. The free side only resolves the field map and fires bit_integrations_latepoint_*; all LatePoint model work lives in the Pro plugin. Bit-Pi's read methods (findBookingById, findAgentByEmail, findCustomerByEmail) are not ported — they are lookups rather than actions. listBundles became the bundle dropdown instead. Agent, service, location and bundle are per-flow configuration, so they are fetched dropdowns rather than field-map rows; everything the flow varies per run stays mappable. The lists are read straight from the latepoint_* tables because the Os*Model classes only autoload on LatePoint's own screens, and are held in component state so they are never serialized into flow_details. validateLatePointConf covers what the field map cannot express: the required dropdown ids, and the customer fields whose necessity depends on the selected customer option. Both the create wizard and the edit screen call it, so neither can save a flow that would fail on every run. --- .../Actions/LatePoint/LatePointController.php | 169 ++++++++++ backend/Actions/LatePoint/RecordApiHelper.php | 150 +++++++++ backend/Actions/LatePoint/Routes.php | 14 + backend/Core/Util/AllTriggersName.php | 1 + .../Utils/StaticData/webhookIntegrations.js | 3 +- .../components/AllIntegrations/EditInteg.jsx | 3 + .../components/AllIntegrations/IntegInfo.jsx | 3 + .../LatePoint/EditLatePoint.jsx | 83 +++++ .../AllIntegrations/LatePoint/LatePoint.jsx | 119 +++++++ .../LatePoint/LatePointActions.jsx | 135 ++++++++ .../LatePoint/LatePointAuthorization.jsx | 118 +++++++ .../LatePoint/LatePointCommonFunc.js | 223 +++++++++++++ .../LatePoint/LatePointFieldMap.jsx | 104 ++++++ .../LatePoint/LatePointIntegLayout.jsx | 315 ++++++++++++++++++ .../AllIntegrations/LatePoint/staticData.js | 170 ++++++++++ .../components/AllIntegrations/NewInteg.jsx | 10 + .../src/components/Flow/New/SelectAction.jsx | 1 + .../src/resource/img/integ/latePoint.webp | Bin 0 -> 472 bytes 18 files changed, 1620 insertions(+), 1 deletion(-) create mode 100644 backend/Actions/LatePoint/LatePointController.php create mode 100644 backend/Actions/LatePoint/RecordApiHelper.php create mode 100644 backend/Actions/LatePoint/Routes.php create mode 100644 frontend/src/components/AllIntegrations/LatePoint/EditLatePoint.jsx create mode 100644 frontend/src/components/AllIntegrations/LatePoint/LatePoint.jsx create mode 100644 frontend/src/components/AllIntegrations/LatePoint/LatePointActions.jsx create mode 100644 frontend/src/components/AllIntegrations/LatePoint/LatePointAuthorization.jsx create mode 100644 frontend/src/components/AllIntegrations/LatePoint/LatePointCommonFunc.js create mode 100644 frontend/src/components/AllIntegrations/LatePoint/LatePointFieldMap.jsx create mode 100644 frontend/src/components/AllIntegrations/LatePoint/LatePointIntegLayout.jsx create mode 100644 frontend/src/components/AllIntegrations/LatePoint/staticData.js create mode 100644 frontend/src/resource/img/integ/latePoint.webp diff --git a/backend/Actions/LatePoint/LatePointController.php b/backend/Actions/LatePoint/LatePointController.php new file mode 100644 index 000000000..80752db43 --- /dev/null +++ b/backend/Actions/LatePoint/LatePointController.php @@ -0,0 +1,169 @@ + $row['id'], + 'label' => trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? '')) ?: ($row['email'] ?? $row['id']), + ]; + } + ); + + wp_send_json_success($response, 200); + } + + public function refreshServices() + { + self::isExists(); + + $response['services'] = self::fetchRows( + 'LATEPOINT_TABLE_SERVICES', + 'latepoint_services', + ['id', 'name'], + function ($row) { + return (object) [ + 'value' => $row['id'], + 'label' => $row['name'] ?? $row['id'], + ]; + } + ); + + wp_send_json_success($response, 200); + } + + public function refreshLocations() + { + self::isExists(); + + $response['locations'] = self::fetchRows( + 'LATEPOINT_TABLE_LOCATIONS', + 'latepoint_locations', + ['id', 'name'], + function ($row) { + return (object) [ + 'value' => $row['id'], + 'label' => $row['name'] ?? $row['id'], + ]; + } + ); + + wp_send_json_success($response, 200); + } + + public function refreshBundles() + { + self::isExists(); + + $response['bundles'] = self::fetchRows( + 'LATEPOINT_TABLE_BUNDLES', + 'latepoint_bundles', + ['id', 'name'], + function ($row) { + return (object) [ + 'value' => $row['id'], + 'label' => $row['name'] ?? $row['id'], + ]; + } + ); + + wp_send_json_success($response, 200); + } + + public function execute($integrationData, $fieldValues) + { + $integrationDetails = $integrationData->flow_details; + $integId = $integrationData->id; + $fieldMap = $integrationDetails->field_map; + $utilities = isset($integrationDetails->utilities) ? $integrationDetails->utilities : []; + + if (empty($fieldMap)) { + return new WP_Error('field_map_empty', __('Field map is empty', 'bit-integrations')); + } + + $recordApiHelper = new RecordApiHelper($integrationDetails, $integId); + + return $recordApiHelper->execute($fieldValues, $fieldMap, $utilities); + } + + /** + * Read id/label pairs from a LatePoint table. + * + * LatePoint's Os*Model classes are only loaded on LatePoint's own screens, so the + * dropdown lists are read straight from the tables instead. + * + * @param string $constant Name of LatePoint's own table constant, preferred so a + * future rename or re-prefix is picked up automatically + * @param string $fallback Unprefixed table name, used when LatePoint has not + * defined the constant yet + * @param array $columns Columns to select — hardcoded literals, never user input + * @param callable $mapper Maps one row to a {value,label} object + * + * @return array + */ + private static function fetchRows($constant, $fallback, array $columns, $mapper) + { + global $wpdb; + + if (!$wpdb) { + return []; + } + + $tableName = \defined($constant) ? \constant($constant) : $wpdb->prefix . $fallback; + + $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tableName)); + + if ($exists !== $tableName) { + return []; + } + + $columnList = implode(', ', array_map('sanitize_key', $columns)); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- table and column names are hardcoded literals verified above + $rows = $wpdb->get_results("SELECT {$columnList} FROM {$tableName}", ARRAY_A); + + if (empty($rows)) { + return []; + } + + return array_map($mapper, $rows); + } +} diff --git a/backend/Actions/LatePoint/RecordApiHelper.php b/backend/Actions/LatePoint/RecordApiHelper.php new file mode 100644 index 000000000..df5cf62d6 --- /dev/null +++ b/backend/Actions/LatePoint/RecordApiHelper.php @@ -0,0 +1,150 @@ +_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + /** + * Execute the integration + * + * @param array $fieldValues Field values from trigger + * @param array $fieldMap Field mapping + * @param array $utilities Optional actions + * + * @return array + */ + public function execute($fieldValues, $fieldMap, $utilities) + { + if (!class_exists('LatePoint')) { + return [ + 'success' => false, + 'message' => __('LatePoint is not installed or activated', 'bit-integrations') + ]; + } + + $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); + + $mainAction = $this->_integrationDetails->mainAction ?? 'create_booking'; + $integrationDetails = $this->_integrationDetails; + + $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 'create_booking': + $response = Hooks::apply(Config::withPrefix('latepoint_create_booking'), $defaultResponse, $fieldData, $utilities, $integrationDetails); + $type = 'booking'; + $actionType = 'create_booking'; + + break; + + case 'update_booking': + $response = Hooks::apply(Config::withPrefix('latepoint_update_booking'), $defaultResponse, $fieldData, $utilities, $integrationDetails); + $type = 'booking'; + $actionType = 'update_booking'; + + break; + + case 'cancel_booking': + $response = Hooks::apply(Config::withPrefix('latepoint_cancel_booking'), $defaultResponse, $fieldData); + $type = 'booking'; + $actionType = 'cancel_booking'; + + break; + + case 'create_agent': + $response = Hooks::apply(Config::withPrefix('latepoint_create_agent'), $defaultResponse, $fieldData, $integrationDetails); + $type = 'agent'; + $actionType = 'create_agent'; + + break; + + case 'create_customer': + $response = Hooks::apply(Config::withPrefix('latepoint_create_customer'), $defaultResponse, $fieldData); + $type = 'customer'; + $actionType = 'create_customer'; + + break; + + case 'create_order': + $response = Hooks::apply(Config::withPrefix('latepoint_create_order'), $defaultResponse, $fieldData, $utilities, $integrationDetails); + $type = 'order'; + $actionType = 'create_order'; + + break; + + case 'create_coupon': + $response = Hooks::apply(Config::withPrefix('latepoint_create_coupon'), $defaultResponse, $fieldData, $integrationDetails); + $type = 'coupon'; + $actionType = 'create_coupon'; + + break; + + case 'update_coupon': + $response = Hooks::apply(Config::withPrefix('latepoint_update_coupon'), $defaultResponse, $fieldData, $integrationDetails); + $type = 'coupon'; + $actionType = 'update_coupon'; + + break; + + default: + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations') + ]; + $type = 'LatePoint'; + $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 static function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $dataFinal = []; + foreach ($fieldMap as $item) { + $triggerValue = $item->formField; + $actionValue = $item->latePointField; + + if (empty($actionValue)) { + continue; + } + + $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) + ? Common::replaceFieldWithValue($item->customValue, $fieldValues) + : $fieldValues[$triggerValue] ?? ''; + } + + return $dataFinal; + } +} diff --git a/backend/Actions/LatePoint/Routes.php b/backend/Actions/LatePoint/Routes.php new file mode 100644 index 000000000..9a69b5bbc --- /dev/null +++ b/backend/Actions/LatePoint/Routes.php @@ -0,0 +1,14 @@ + ['name' => 'Jetpack CRM', 'isPro' => true, 'is_active' => false], 'Kadence' => ['name' => 'Kadence Blocks Form', 'isPro' => true, 'is_active' => false], 'KaliForms' => ['name' => 'Kali Forms', 'isPro' => true, 'is_active' => false], + 'LatePoint' => ['name' => 'LatePoint', 'isPro' => true, 'is_active' => false], 'LearnDash' => ['name' => 'LearnDash LMS', 'isPro' => true, 'is_active' => false], 'LearnPress' => ['name' => 'LearnPress LMS', 'isPro' => true, 'is_active' => false], 'LifterLms' => ['name' => 'LifterLMS', 'isPro' => true, 'is_active' => false], diff --git a/frontend/src/Utils/StaticData/webhookIntegrations.js b/frontend/src/Utils/StaticData/webhookIntegrations.js index 1c7d8c209..849e35211 100644 --- a/frontend/src/Utils/StaticData/webhookIntegrations.js +++ b/frontend/src/Utils/StaticData/webhookIntegrations.js @@ -112,7 +112,8 @@ export const customFormIntegrations = [ 'WpErp', 'WpDataTables', 'GiveWp', - 'SenseiLMS' + 'SenseiLMS', + 'LatePoint' ] export const actionHookIntegrations = ['ActionHook'] diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index e498edfe5..fda18df1f 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -180,6 +180,7 @@ const EditCreatorLms = lazy(() => import('./CreatorLms/EditCreatorLms')) const EditUltimateAffiliatePro = lazy(() => import('./UltimateAffiliatePro/EditUltimateAffiliatePro')) const EditBookly = lazy(() => import('./Bookly/EditBookly')) const EditFluentCart = lazy(() => import('./FluentCart/EditFluentCart')) +const EditLatePoint = lazy(() => import('./LatePoint/EditLatePoint')) const EditWsms = lazy(() => import('./Wsms/EditWsms')) const EditWebbaBooking = lazy(() => import('./WebbaBooking/EditWebbaBooking')) const EditMoreConvertWishlist = lazy(() => import('./MoreConvertWishlist/EditMoreConvertWishlist')) @@ -629,6 +630,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'FluentCart': return + case 'LatePoint': + return case 'Wsms': return case 'WebbaBooking': diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index f6e27f286..a37c5bc8c 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -182,6 +182,7 @@ const UltimateAffiliateProAuthorization = lazy( ) const BooklyAuthorization = lazy(() => import('./Bookly/BooklyAuthorization')) const FluentCartAuthorization = lazy(() => import('./FluentCart/FluentCartAuthorization')) +const LatePointAuthorization = lazy(() => import('./LatePoint/LatePointAuthorization')) const WsmsAuthorization = lazy(() => import('./Wsms/WsmsAuthorization')) const MoreConvertWishlistAuthorization = lazy( () => import('./MoreConvertWishlist/MoreConvertWishlistAuthorization') @@ -628,6 +629,8 @@ const IntegrationInfo = memo(({ integrationConf, location }) => { return case 'FluentCart': return + case 'LatePoint': + return case 'Wsms': return case 'WebbaBooking': diff --git a/frontend/src/components/AllIntegrations/LatePoint/EditLatePoint.jsx b/frontend/src/components/AllIntegrations/LatePoint/EditLatePoint.jsx new file mode 100644 index 000000000..c3212b458 --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/EditLatePoint.jsx @@ -0,0 +1,83 @@ +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 Note from '../../Utilities/Note' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' +import { checkMappedFields, handleInput, validateLatePointConf } from './LatePointCommonFunc' +import LatePointIntegLayout from './LatePointIntegLayout' + +export default function EditLatePoint({ allIntegURL }) { + const navigate = useNavigate() + const { id, formID } = useParams() + + const [latePointConf, setLatePointConf] = useRecoilState($actionConf) + const [flow, setFlow] = useRecoilState($newFlow) + const formFields = useRecoilValue($formFields) + const [isLoading, setIsLoading] = useState(false) + const [snack, setSnackbar] = useState({ show: false }) + + // Same gate the create wizard applies, so an edited flow cannot be saved in a + // state that would fail on every run. + const selectionError = validateLatePointConf(latePointConf) + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} + handleInput(e, latePointConf, setLatePointConf)} + name="name" + value={latePointConf.name} + type="text" + placeholder={__('Integration Name...', 'bit-integrations')} + /> +
+
+ + + + + + {selectionError && } + + + saveActionConf({ + flow, + setFlow, + allIntegURL, + conf: latePointConf, + navigate, + id, + edit: 1, + setIsLoading, + setSnackbar + }) + } + disabled={!checkMappedFields(latePointConf) || !!selectionError} + isLoading={isLoading} + dataConf={latePointConf} + setDataConf={setLatePointConf} + formFields={formFields} + /> +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/LatePoint/LatePoint.jsx b/frontend/src/components/AllIntegrations/LatePoint/LatePoint.jsx new file mode 100644 index 000000000..a46394c71 --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/LatePoint.jsx @@ -0,0 +1,119 @@ +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 LatePointAuthorization from './LatePointAuthorization' +import { checkMappedFields, validateLatePointConf } from './LatePointCommonFunc' +import LatePointIntegLayout from './LatePointIntegLayout' + +export default function LatePoint({ 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 [latePointConf, setLatePointConf] = useState({ + name: 'LatePoint', + type: 'LatePoint', + field_map: [{ formField: '', latePointField: '' }], + actions: {}, + mainAction: '' + }) + + const nextPage = val => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (val === 3) { + // Dropdown ids and the customer fields whose necessity depends on customerType + // are invisible to checkMappedFields; validateLatePointConf covers both, and the + // edit screen runs the same check. + const selectionError = validateLatePointConf(latePointConf) + + if (selectionError) { + setSnackbar({ show: true, msg: selectionError }) + return + } + + if (!checkMappedFields(latePointConf)) { + setSnackbar({ + show: true, + msg: __('Please map all required fields to continue.', 'bit-integrations') + }) + return + } + + if (latePointConf.name !== '' && latePointConf.field_map.length > 0) { + setStep(val) + } + } else { + setStep(val) + } + } + + return ( +
+ +
+ + {/* STEP 1 */} + + + {/* STEP 2 */} +
+ +
+
+
+ +
+ + {/* STEP 3 */} + + saveIntegConfig(flow, setFlow, allIntegURL, latePointConf, navigate, '', '', setIsLoading) + } + isLoading={isLoading} + dataConf={latePointConf} + setDataConf={setLatePointConf} + formFields={formFields} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/LatePoint/LatePointActions.jsx b/frontend/src/components/AllIntegrations/LatePoint/LatePointActions.jsx new file mode 100644 index 000000000..c69cf766e --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/LatePointActions.jsx @@ -0,0 +1,135 @@ +import { useState } from 'react' +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { __ } from '../../../Utils/i18nwrap' +import ConfirmModal from '../../Utilities/ConfirmModal' +import TableCheckBox from '../../Utilities/TableCheckBox' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { + bookingStatusOptions, + fulfillmentStatusOptions, + orderStatusOptions, + paymentStatusOptions +} from './staticData' + +/** + * Optional status enums. These all have sensible LatePoint defaults, so they are + * opt-in here rather than always-visible selects in the integration layout. + */ +export default function LatePointActions({ latePointConf, setLatePointConf }) { + const [actionMdl, setActionMdl] = useState({ show: false }) + + const clsActionMdl = () => setActionMdl({ show: false }) + + const setAction = (val, name) => { + setLatePointConf(prevConf => + create(prevConf, draftConf => { + if (!draftConf.utilities) { + draftConf.utilities = {} + } + draftConf.utilities[name] = val + }) + ) + } + + const renderActionModal = (type, title, options, valueName) => ( + +
+
{title}
+
+ setAction(val, valueName)} + /> +
+ + ) + + const isBooking = + latePointConf?.mainAction === 'create_booking' || latePointConf?.mainAction === 'update_booking' + const isOrder = latePointConf?.mainAction === 'create_order' + + return ( +
+ {isBooking && ( + <> + setActionMdl({ show: 'booking_status' })} + className="wdt-200 mt-4 mr-2" + value="booking_status" + title={__('Booking Status', 'bit-integrations')} + subTitle={__('Set the booking status', 'bit-integrations')} + /> + {renderActionModal( + 'booking_status', + __('Booking Status', 'bit-integrations'), + bookingStatusOptions, + 'selected_booking_status' + )} + + )} + + {(isBooking || isOrder) && ( + <> + setActionMdl({ show: 'order_status' })} + className="wdt-200 mt-4 mr-2" + value="order_status" + title={__('Order Status', 'bit-integrations')} + subTitle={__('Set the order status', 'bit-integrations')} + /> + {renderActionModal( + 'order_status', + __('Order Status', 'bit-integrations'), + orderStatusOptions, + 'selected_order_status' + )} + + setActionMdl({ show: 'payment_status' })} + className="wdt-200 mt-4 mr-2" + value="payment_status" + title={__('Payment Status', 'bit-integrations')} + subTitle={__('Set the payment status', 'bit-integrations')} + /> + {renderActionModal( + 'payment_status', + __('Payment Status', 'bit-integrations'), + paymentStatusOptions, + 'selected_payment_status' + )} + + setActionMdl({ show: 'fulfillment_status' })} + className="wdt-200 mt-4 mr-2" + value="fulfillment_status" + title={__('Fulfillment Status', 'bit-integrations')} + subTitle={__('Set the fulfillment status', 'bit-integrations')} + /> + {renderActionModal( + 'fulfillment_status', + __('Fulfillment Status', 'bit-integrations'), + fulfillmentStatusOptions, + 'selected_fulfillment_status' + )} + + )} +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/LatePoint/LatePointAuthorization.jsx b/frontend/src/components/AllIntegrations/LatePoint/LatePointAuthorization.jsx new file mode 100644 index 000000000..393130f02 --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/LatePointAuthorization.jsx @@ -0,0 +1,118 @@ +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' + +export default function LatePointAuthorization({ + formID, + latePointConf, + setLatePointConf, + step, + nextPage, + isLoading, + setIsLoading, + setSnackbar +}) { + const [isAuthorized, setIsAuthorized] = useState(false) + const [showAuthMsg, setShowAuthMsg] = useState(false) + + const authorizeHandler = () => { + setIsLoading('auth') + bitsFetch({}, 'latepoint_authorize') + .then(result => { + if (result?.success) { + setIsAuthorized(true) + setSnackbar({ + show: true, + msg: __('Connected with LatePoint Successfully', 'bit-integrations') + }) + } + setIsLoading(false) + setShowAuthMsg(true) + }) + .catch(() => { + setIsLoading(false) + setShowAuthMsg(true) + }) + } + + const handleInput = e => { + const newConf = { ...latePointConf } + newConf[e.target.name] = e.target.value + setLatePointConf(newConf) + } + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} +
+ + + {isLoading === 'auth' && ( +
+ + {__('Checking if LatePoint is authorized!!!', 'bit-integrations')} +
+ )} + + {showAuthMsg && !isAuthorized && !isLoading && ( +
+
+
+ ✕ +
+
+ {__('LatePoint is not activated or not installed', 'bit-integrations')} +
+
+
+ )} + + {showAuthMsg && isAuthorized && !isLoading && ( +
+
+ ✓ +
+
{__('LatePoint is activated', 'bit-integrations')}
+
+ )} + + +
+ +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/LatePoint/LatePointCommonFunc.js b/frontend/src/components/AllIntegrations/LatePoint/LatePointCommonFunc.js new file mode 100644 index 000000000..e3f1edc29 --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/LatePointCommonFunc.js @@ -0,0 +1,223 @@ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' +import { + needsAgentAndLocation, + needsBundle, + needsCustomerType, + needsDiscountType, + needsService +} from './staticData' + +export const handleInput = (e, latePointConf, setLatePointConf) => { + const { name, value } = e.target + + setLatePointConf(prevConf => + create(prevConf, draftConf => { + draftConf[name] = value + }) + ) +} + +/** + * Option lists are held in component state rather than on conf, so they are not + * serialized into flow_details on every save. + */ +const FETCHERS = { + agents: { route: 'refresh_latepoint_agents', dataKey: 'agents' }, + services: { route: 'refresh_latepoint_services', dataKey: 'services' }, + locations: { route: 'refresh_latepoint_locations', dataKey: 'locations' }, + bundles: { route: 'refresh_latepoint_bundles', dataKey: 'bundles' } +} + +const LIST_LABELS = { + agents: () => __('agents', 'bit-integrations'), + services: () => __('services', 'bit-integrations'), + locations: () => __('locations', 'bit-integrations'), + bundles: () => __('bundles', 'bit-integrations') +} + +const fetchList = (key, setLists) => { + const { route, dataKey } = FETCHERS[key] + + return bitsFetch(null, route).then(result => { + if (result?.success && result?.data?.[dataKey]) { + setLists(prev => ({ ...prev, [key]: result.data[dataKey] })) + return true + } + + throw new Error(key) + }) +} + +/** + * Fetch several lists at once and clear the loading flag only after every request + * has settled — a single shared flag cleared by the first responder would re-enable + * the refresh buttons while other dropdowns were still empty. + * + * @param {Function} setLists state setter for the lists map + * @param {Function} setIsLoading shared loading flag + * @param {string[]} keys which lists to fetch + */ +export const refreshLatePointLists = (setLists, setIsLoading, keys) => { + const wanted = keys.filter(key => FETCHERS[key]) + + if (wanted.length === 0) { + return Promise.resolve() + } + + setIsLoading(true) + + return Promise.allSettled(wanted.map(key => fetchList(key, setLists))).then(results => { + setIsLoading(false) + + const failed = wanted.filter((key, i) => results[i].status === 'rejected') + + if (failed.length > 0) { + toast.error( + `${__('LatePoint fetch failed for', 'bit-integrations')} ${failed + .map(key => LIST_LABELS[key]()) + .join(', ')}. ${__('Please try again', 'bit-integrations')}` + ) + return + } + + toast.success(__('LatePoint lists fetched successfully', 'bit-integrations')) + }) +} + +export const refreshLatePointAgents = (setLists, setIsLoading) => + refreshLatePointLists(setLists, setIsLoading, ['agents']) + +export const refreshLatePointServices = (setLists, setIsLoading) => + refreshLatePointLists(setLists, setIsLoading, ['services']) + +export const refreshLatePointLocations = (setLists, setIsLoading) => + refreshLatePointLists(setLists, setIsLoading, ['locations']) + +export const refreshLatePointBundles = (setLists, setIsLoading) => + refreshLatePointLists(setLists, setIsLoading, ['bundles']) + +/** + * Which lists a given action's dropdowns need. + * + * @param {string} action mainAction slug + * + * @returns {string[]} + */ +export const listsForAction = action => { + const keys = [] + + if (needsService.includes(action)) { + keys.push('services') + } + if (needsAgentAndLocation.includes(action)) { + keys.push('agents', 'locations') + } + if (action === 'create_agent' && !keys.includes('services')) { + keys.push('services') + } + if (needsBundle.includes(action)) { + keys.push('bundles') + } + + return keys +} + +export const checkMappedFields = latePointConf => { + const mappedFields = latePointConf?.field_map + ? latePointConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.latePointField || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + if (mappedFields.length > 0) { + return false + } + return true +} + +const isMapped = (latePointConf, key) => + (latePointConf?.field_map || []).some( + row => row.latePointField === key && row.formField && (row.formField !== 'custom' || row.customValue) + ) + +/** + * Validate the selections the field map cannot express. + * + * Shared by the create wizard and the edit screen so both paths reject the same + * configurations — the required dropdown ids, plus the customer fields whose + * necessity depends on the chosen customer option rather than on staticData. + * + * @param {object} latePointConf + * + * @returns {string} error message, or '' when the config is valid + */ +export const validateLatePointConf = latePointConf => { + const action = latePointConf?.mainAction + + if (!action) { + return __('Please select an action to continue.', 'bit-integrations') + } + + if (needsService.includes(action) && !latePointConf?.selectedService) { + return __('Please select a service to continue.', 'bit-integrations') + } + + if (action === 'create_booking') { + if (!latePointConf?.selectedAgent) { + return __('Please select an agent to continue.', 'bit-integrations') + } + if (!latePointConf?.selectedLocation) { + return __('Please select a location to continue.', 'bit-integrations') + } + } + + if (needsBundle.includes(action) && !latePointConf?.selectedBundle) { + return __('Please select a bundle to continue.', 'bit-integrations') + } + + if (action === 'create_coupon' && needsDiscountType.includes(action) && !latePointConf?.discountType) { + return __('Please select a discount type to continue.', 'bit-integrations') + } + + if (needsCustomerType.includes(action)) { + if (!latePointConf?.customerType) { + return __('Please select a customer option to continue.', 'bit-integrations') + } + + // These are required by the action only for the chosen customer option, which + // is why they cannot simply be marked required in staticData. + if (latePointConf.customerType === 'existing') { + if (!isMapped(latePointConf, 'customer_id')) { + return __('Please map Customer ID to use an existing customer.', 'bit-integrations') + } + } else { + const missing = ['customer_first_name', 'customer_last_name', 'customer_email'].filter( + key => !isMapped(latePointConf, key) + ) + + if (missing.length > 0) { + return __( + 'Please map Customer First Name, Customer Last Name and Customer Email.', + 'bit-integrations' + ) + } + } + } + + return '' +} + +export const generateMappedField = fields => { + const requiredFlds = fields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ + formField: '', + latePointField: field.key + })) + : [{ formField: '', latePointField: '' }] +} diff --git a/frontend/src/components/AllIntegrations/LatePoint/LatePointFieldMap.jsx b/frontend/src/components/AllIntegrations/LatePoint/LatePointFieldMap.jsx new file mode 100644 index 000000000..b0a7f335e --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/LatePointFieldMap.jsx @@ -0,0 +1,104 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import TagifyInput from '../../Utilities/TagifyInput' +import { + addFieldMap, + delFieldMap, + handleCustomValue, + handleFieldMapping +} from '../GlobalIntegrationHelper' + +export default function LatePointFieldMap({ i, formFields, field, latePointConf, setLatePointConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = latePointConf?.latePointFields?.filter(fld => fld.required === true) || [] + const nonRequiredFlds = latePointConf?.latePointFields?.filter(fld => fld.required === false) || [] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, latePointConf, setLatePointConf)} + 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/LatePoint/LatePointIntegLayout.jsx b/frontend/src/components/AllIntegrations/LatePoint/LatePointIntegLayout.jsx new file mode 100644 index 000000000..502bda245 --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/LatePointIntegLayout.jsx @@ -0,0 +1,315 @@ +import { useEffect, useState } from 'react' +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import Note from '../../Utilities/Note' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' +import LatePointActions from './LatePointActions' +import { + generateMappedField, + listsForAction, + refreshLatePointAgents, + refreshLatePointBundles, + refreshLatePointLocations, + refreshLatePointLists, + refreshLatePointServices +} from './LatePointCommonFunc' +import LatePointFieldMap from './LatePointFieldMap' +import { + CancelBookingFields, + CreateAgentFields, + CreateBookingFields, + CreateCouponFields, + CreateCustomerFields, + CreateOrderFields, + couponStatusOptions, + customerTypeOptions, + discountTypeOptions, + hasUtilities, + modules, + needsAgentAndLocation, + needsAgentServices, + needsBundle, + needsCouponStatus, + needsCustomerType, + needsDiscountType, + needsService, + UpdateBookingFields, + UpdateCouponFields +} from './staticData' + +const fieldsByAction = { + create_booking: CreateBookingFields, + update_booking: UpdateBookingFields, + cancel_booking: CancelBookingFields, + create_agent: CreateAgentFields, + create_customer: CreateCustomerFields, + create_order: CreateOrderFields, + create_coupon: CreateCouponFields, + update_coupon: UpdateCouponFields +} + +export default function LatePointIntegLayout({ + formID, + formFields, + latePointConf, + setLatePointConf, + isLoading, + setIsLoading, + setSnackbar +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + // Option lists live here rather than on conf so they are not persisted into + // flow_details every time the flow is saved. + const [lists, setLists] = useState({}) + + const mainAction = latePointConf?.mainAction + + // Populate the dropdowns for whatever action is already selected. Matters most on + // the edit screen, where handleMainAction never runs. + useEffect(() => { + const keys = listsForAction(mainAction) + + if (keys.length > 0) { + refreshLatePointLists(setLists, setIsLoading, keys) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mainAction]) + + const setField = (key, val) => + setLatePointConf(prevConf => + create(prevConf, draftConf => { + draftConf[key] = val + }) + ) + + const handleMainAction = value => { + setLatePointConf(prevConf => + create(prevConf, draftConf => { + draftConf.mainAction = value + draftConf.latePointFields = fieldsByAction[value] || [] + draftConf.field_map = generateMappedField(draftConf.latePointFields) + }) + ) + // The effect above fetches the lists this action needs. + } + + const renderFetchedSelect = (label, confKey, listKey, refresher, tooltip, multi = false) => ( + <> +
+
+ {label} + ({ + label: item.label, + value: item.value?.toString() + })) + : [] + } + onChange={val => setField(confKey, val)} + singleSelect={!multi} + closeOnSelect={!multi} + /> + +
+ + ) + + const renderEnumSelect = (label, confKey, options) => ( + <> +
+
+ {label} + setField(confKey, val)} + singleSelect + closeOnSelect + /> +
+ + ) + + 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 + /> +
+ + {needsService.includes(mainAction) && + renderFetchedSelect( + __('Service:', 'bit-integrations'), + 'selectedService', + 'services', + refreshLatePointServices, + __('Refresh Services', 'bit-integrations') + )} + + {needsAgentAndLocation.includes(mainAction) && ( + <> + {renderFetchedSelect( + __('Agent:', 'bit-integrations'), + 'selectedAgent', + 'agents', + refreshLatePointAgents, + __('Refresh Agents', 'bit-integrations') + )} + {renderFetchedSelect( + __('Location:', 'bit-integrations'), + 'selectedLocation', + 'locations', + refreshLatePointLocations, + __('Refresh Locations', 'bit-integrations') + )} + + )} + + {needsAgentServices.includes(mainAction) && + renderFetchedSelect( + __('Assign Services:', 'bit-integrations'), + 'selectedServices', + 'services', + refreshLatePointServices, + __('Refresh Services', 'bit-integrations'), + true + )} + + {needsBundle.includes(mainAction) && + renderFetchedSelect( + __('Bundle:', 'bit-integrations'), + 'selectedBundle', + 'bundles', + refreshLatePointBundles, + __('Refresh Bundles', 'bit-integrations') + )} + + {needsCustomerType.includes(mainAction) && + renderEnumSelect(__('Customer:', 'bit-integrations'), 'customerType', customerTypeOptions)} + + {needsDiscountType.includes(mainAction) && + renderEnumSelect(__('Discount Type:', 'bit-integrations'), 'discountType', discountTypeOptions)} + + {needsCouponStatus.includes(mainAction) && + renderEnumSelect(__('Coupon Status:', 'bit-integrations'), 'couponStatus', couponStatusOptions)} + + {isLoading && ( + + )} + + {mainAction && latePointConf.latePointFields && ( +
+ {__('Map Fields', 'bit-integrations')} +
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('LatePoint Fields', 'bit-integrations')} +
+
+ + {latePointConf?.field_map?.map((itm, i) => ( + + ))} +
+ +
+
+
+ )} + + {needsCustomerType.includes(mainAction) && latePointConf?.customerType === 'existing' && ( + + )} + + {needsCustomerType.includes(mainAction) && latePointConf?.customerType !== 'existing' && ( + + )} + + {mainAction === 'create_booking' && ( + + )} + + {hasUtilities.includes(mainAction) && ( +
+ {__('Utilities', 'bit-integrations')} +
+ +
+ )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/LatePoint/staticData.js b/frontend/src/components/AllIntegrations/LatePoint/staticData.js new file mode 100644 index 000000000..c1fcab477 --- /dev/null +++ b/frontend/src/components/AllIntegrations/LatePoint/staticData.js @@ -0,0 +1,170 @@ +import { __ } from '../../../Utils/i18nwrap' + +export const modules = [ + { name: 'create_booking', label: __('Create Booking', 'bit-integrations'), is_pro: true }, + { name: 'update_booking', label: __('Update Booking', 'bit-integrations'), is_pro: true }, + { name: 'cancel_booking', label: __('Cancel Booking', 'bit-integrations'), is_pro: true }, + { name: 'create_agent', label: __('Create Agent', 'bit-integrations'), is_pro: true }, + { name: 'create_customer', label: __('Create Customer', 'bit-integrations'), is_pro: true }, + { name: 'create_order', label: __('Create Order', 'bit-integrations'), is_pro: true }, + { name: 'create_coupon', label: __('Create Coupon', 'bit-integrations'), is_pro: true }, + { name: 'update_coupon', label: __('Update Coupon', 'bit-integrations'), is_pro: true } +] + +// Agent, service, location and bundle are chosen per flow via fetched dropdowns, +// so they are deliberately absent from every field map below. + +export const CreateBookingFields = [ + { key: 'start_date', label: __('Start Date (Y-m-d)', 'bit-integrations'), required: true }, + { key: 'start_time', label: __('Start Time (HH:MM)', 'bit-integrations'), required: true }, + { key: 'end_time', label: __('End Time (HH:MM)', 'bit-integrations'), required: false }, + { key: 'customer_first_name', label: __('Customer First Name', 'bit-integrations'), required: false }, + { key: 'customer_last_name', label: __('Customer Last Name', 'bit-integrations'), required: false }, + { key: 'customer_email', label: __('Customer Email', 'bit-integrations'), required: false }, + { key: 'customer_phone', label: __('Customer Phone', 'bit-integrations'), required: false }, + { key: 'customer_notes', label: __('Customer Notes', 'bit-integrations'), required: false }, + { + key: 'customer_id', + label: __('Customer ID (existing customer)', 'bit-integrations'), + required: false + }, + { key: 'customer_comment', label: __('Customer Comment', 'bit-integrations'), required: false }, + { key: 'total_attendees', label: __('Total Attendees', 'bit-integrations'), required: false }, + { key: 'buffer_before', label: __('Buffer Before (minutes)', 'bit-integrations'), required: false }, + { key: 'buffer_after', label: __('Buffer After (minutes)', 'bit-integrations'), required: false } +] + +export const UpdateBookingFields = [ + { key: 'booking_id', label: __('Booking ID', 'bit-integrations'), required: true }, + { key: 'start_date', label: __('Start Date (Y-m-d)', 'bit-integrations'), required: false }, + { key: 'start_time', label: __('Start Time (HH:MM)', 'bit-integrations'), required: false }, + { key: 'end_time', label: __('End Time (HH:MM)', 'bit-integrations'), required: false }, + { key: 'customer_comment', label: __('Customer Comment', 'bit-integrations'), required: false }, + { key: 'total_attendees', label: __('Total Attendees', 'bit-integrations'), required: false }, + { key: 'buffer_before', label: __('Buffer Before (minutes)', 'bit-integrations'), required: false }, + { key: 'buffer_after', label: __('Buffer After (minutes)', 'bit-integrations'), required: false } +] + +export const CancelBookingFields = [ + { key: 'booking_id', label: __('Booking ID', 'bit-integrations'), required: true } +] + +export const CreateAgentFields = [ + { key: 'first_name', label: __('First Name', 'bit-integrations'), required: true }, + { key: 'last_name', label: __('Last Name', 'bit-integrations'), required: true }, + { key: 'email', label: __('Email Address', 'bit-integrations'), required: true }, + { key: 'phone', label: __('Phone', 'bit-integrations'), required: false }, + { key: 'bio', label: __('Bio', 'bit-integrations'), required: false }, + { key: 'wp_user_id', label: __('WordPress User ID', 'bit-integrations'), required: false } +] + +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 Address', 'bit-integrations'), required: true }, + { key: 'phone', label: __('Phone', 'bit-integrations'), required: false }, + { key: 'notes', label: __('Notes', 'bit-integrations'), required: false } +] + +export const CreateOrderFields = [ + { key: 'customer_first_name', label: __('Customer First Name', 'bit-integrations'), required: false }, + { key: 'customer_last_name', label: __('Customer Last Name', 'bit-integrations'), required: false }, + { key: 'customer_email', label: __('Customer Email', 'bit-integrations'), required: false }, + { key: 'customer_phone', label: __('Customer Phone', 'bit-integrations'), required: false }, + { key: 'customer_notes', label: __('Customer Notes', 'bit-integrations'), required: false }, + { + key: 'customer_id', + label: __('Customer ID (existing customer)', 'bit-integrations'), + required: false + } +] + +export const CreateCouponFields = [ + { key: 'code', label: __('Coupon Code', 'bit-integrations'), required: true }, + { key: 'discount_value', label: __('Discount Value', 'bit-integrations'), required: true }, + { key: 'name', label: __('Coupon Name', 'bit-integrations'), required: false }, + { key: 'limit_per_customer', label: __('Limit Per Customer', 'bit-integrations'), required: false }, + { key: 'limit_total', label: __('Total Usage Limit', 'bit-integrations'), required: false }, + { key: 'orders_more', label: __('Orders More Than', 'bit-integrations'), required: false }, + { key: 'orders_less', label: __('Orders Less Than', 'bit-integrations'), required: false }, + { key: 'agent_ids', label: __('Agent IDs (comma separated)', 'bit-integrations'), required: false }, + { + key: 'customer_ids', + label: __('Customer IDs (comma separated)', 'bit-integrations'), + required: false + }, + { key: 'service_ids', label: __('Service IDs (comma separated)', 'bit-integrations'), required: false } +] + +export const UpdateCouponFields = [ + { key: 'coupon_id', label: __('Coupon ID', 'bit-integrations'), required: true }, + { key: 'code', label: __('Coupon Code', 'bit-integrations'), required: false }, + { key: 'discount_value', label: __('Discount Value', 'bit-integrations'), required: false }, + { key: 'name', label: __('Coupon Name', 'bit-integrations'), required: false }, + { key: 'limit_per_customer', label: __('Limit Per Customer', 'bit-integrations'), required: false }, + { key: 'limit_total', label: __('Total Usage Limit', 'bit-integrations'), required: false }, + { key: 'orders_more', label: __('Orders More Than', 'bit-integrations'), required: false }, + { key: 'orders_less', label: __('Orders Less Than', 'bit-integrations'), required: false }, + { key: 'agent_ids', label: __('Agent IDs (comma separated)', 'bit-integrations'), required: false }, + { + key: 'customer_ids', + label: __('Customer IDs (comma separated)', 'bit-integrations'), + required: false + }, + { key: 'service_ids', label: __('Service IDs (comma separated)', 'bit-integrations'), required: false } +] + +// Fixed option sets — rendered as selects, never field mapped. +export const customerTypeOptions = [ + { label: __('Create or match by email', 'bit-integrations'), value: 'new' }, + { label: __('Use existing customer ID', 'bit-integrations'), value: 'existing' } +] + +export const discountTypeOptions = [ + { label: __('Percentage', 'bit-integrations'), value: 'percentage' }, + { label: __('Fixed Amount', 'bit-integrations'), value: 'fixed' } +] + +export const couponStatusOptions = [ + { label: __('Active', 'bit-integrations'), value: 'active' }, + { label: __('Disabled', 'bit-integrations'), value: 'disabled' } +] + +export const bookingStatusOptions = [ + { label: __('Approved', 'bit-integrations'), value: 'approved' }, + { label: __('Pending Approval', 'bit-integrations'), value: 'pending_approval' }, + { label: __('Cancelled', 'bit-integrations'), value: 'cancelled' }, + { label: __('No Show', 'bit-integrations'), value: 'no_show' }, + { label: __('Completed', 'bit-integrations'), value: 'completed' } +] + +export const orderStatusOptions = [ + { label: __('Open', 'bit-integrations'), value: 'open' }, + { label: __('Cancelled', 'bit-integrations'), value: 'cancelled' }, + { label: __('Completed', 'bit-integrations'), value: 'completed' } +] + +export const paymentStatusOptions = [ + { label: __('Not Paid', 'bit-integrations'), value: 'not_paid' }, + { label: __('Partially Paid', 'bit-integrations'), value: 'partially_paid' }, + { label: __('Fully Paid', 'bit-integrations'), value: 'fully_paid' }, + { label: __('Processing', 'bit-integrations'), value: 'processing' } +] + +export const fulfillmentStatusOptions = [ + { label: __('Not Fulfilled', 'bit-integrations'), value: 'not_fulfilled' }, + { label: __('Partially Fulfilled', 'bit-integrations'), value: 'partially_fulfilled' }, + { label: __('Fulfilled', 'bit-integrations'), value: 'fulfilled' } +] + +// Which actions render which control in the integration layout. +export const needsService = ['create_booking', 'update_booking'] +export const needsAgentAndLocation = ['create_booking', 'update_booking'] +export const needsCustomerType = ['create_booking', 'create_order'] +export const needsBundle = ['create_order'] +export const needsDiscountType = ['create_coupon', 'update_coupon'] +export const needsCouponStatus = ['create_coupon', 'update_coupon'] +export const needsAgentServices = ['create_agent'] + +// Actions exposing optional status enums under Utilities. +export const hasUtilities = ['create_booking', 'update_booking', 'create_order'] diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index 11840feeb..7c6e8b681 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -179,6 +179,7 @@ const CreatorLms = lazy(() => import('./CreatorLms/CreatorLms')) const UltimateAffiliatePro = lazy(() => import('./UltimateAffiliatePro/UltimateAffiliatePro')) const Bookly = lazy(() => import('./Bookly/Bookly')) const FluentCart = lazy(() => import('./FluentCart/FluentCart')) +const LatePoint = lazy(() => import('./LatePoint/LatePoint')) const Wsms = lazy(() => import('./Wsms/Wsms')) const WebbaBooking = lazy(() => import('./WebbaBooking/WebbaBooking')) const MoreConvertWishlist = lazy(() => import('./MoreConvertWishlist/MoreConvertWishlist')) @@ -1748,6 +1749,15 @@ const NewIntegs = memo(({ integUrlName, allIntegURL, flow, setFlow }) => { setFlow={setFlow} /> ) + case 'LatePoint': + return ( + + ) case 'Wsms': return ( Z=6B=zIePI+)kH3T zxPf5u*Qd;=;IMgX)8s5eACKmvIX6c|?j%2HSjp7Y$D0HAl^or1(*Lp--3UFT5|#V|Dw+7N!PFtx9LEQ?t&y#B@gK*Bi3dBpW`UYi5 O1`kYYj0001i?#tQ$ literal 0 HcmV?d00001 From cef7419510f7c5817711779aead1beae768a773b Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Mon, 17 Aug 2026 11:36:55 +0600 Subject: [PATCH 2/2] fix(LatePoint): remove customer comment field from update booking fields --- frontend/src/components/AllIntegrations/LatePoint/staticData.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/AllIntegrations/LatePoint/staticData.js b/frontend/src/components/AllIntegrations/LatePoint/staticData.js index c1fcab477..bfcefa900 100644 --- a/frontend/src/components/AllIntegrations/LatePoint/staticData.js +++ b/frontend/src/components/AllIntegrations/LatePoint/staticData.js @@ -39,7 +39,6 @@ export const UpdateBookingFields = [ { key: 'start_date', label: __('Start Date (Y-m-d)', 'bit-integrations'), required: false }, { key: 'start_time', label: __('Start Time (HH:MM)', 'bit-integrations'), required: false }, { key: 'end_time', label: __('End Time (HH:MM)', 'bit-integrations'), required: false }, - { key: 'customer_comment', label: __('Customer Comment', 'bit-integrations'), required: false }, { key: 'total_attendees', label: __('Total Attendees', 'bit-integrations'), required: false }, { key: 'buffer_before', label: __('Buffer Before (minutes)', 'bit-integrations'), required: false }, { key: 'buffer_after', label: __('Buffer After (minutes)', 'bit-integrations'), required: false }