Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: LatePoint
* Description: Appointment Scheduling Software for WordPress
- * Version: 5.6.0
+ * Version: 5.6.1
* Author: LatePoint
* Author URI: https://latepoint.com
* Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
* LatePoint version.
*
*/
- public $version = '5.6.0';
+ public $version = '5.6.1';
public $db_version = '2.3.0';
@@ -1136,7 +1136,7 @@
function add_upgrade_link( $links, $plugin_file ) {
if ( plugin_basename( __FILE__ ) == $plugin_file ) {
if ( apply_filters( 'latepoint_show_upgrade_link_on_plugins_page', true, $plugin_file ) ) {
- $custom_link = '<a class="latepoint-plugin-upgrade-premium-link" href="' . LATEPOINT_UPGRADE_URL . '">' . esc_html__( 'Get LatePoint Pro' ) . '</a>';
+ $custom_link = '<a class="latepoint-plugin-upgrade-premium-link" href="' . LATEPOINT_UPGRADE_URL . '">' . esc_html__( 'Get LatePoint Pro', 'latepoint' ) . '</a>';
$links[] = $custom_link;
}
}
@@ -1646,6 +1646,24 @@
wp_localize_script( 'latepoint-main-admin', 'latepoint_helper', $localized_vars );
+ wp_localize_script(
+ 'latepoint-main-admin',
+ 'latepoint_bookings_bulk_i18n',
+ array(
+ 'modal_title' => __( 'Are you sure you want to delete these appointments?', 'latepoint' ),
+ 'modal_body_one' => __( 'You are about to delete 1 appointment. This action cannot be undone.', 'latepoint' ),
+ 'modal_body_many' => __( 'You are about to delete %d appointments. This action cannot be undone.', 'latepoint' ),
+ 'modal_confirm_prompt' => __( 'To confirm, type %s in the box below.', 'latepoint' ),
+ 'modal_confirm_word' => __( 'delete', 'latepoint' ),
+ 'modal_input_placeholder' => __( 'Type "delete" to confirm', 'latepoint' ),
+ 'modal_confirm' => __( 'Delete', 'latepoint' ),
+ 'modal_cancel' => __( 'Cancel', 'latepoint' ),
+ 'selected_label_one' => __( 'Appointment selected', 'latepoint' ),
+ 'selected_label_many' => __( 'Appointments selected', 'latepoint' ),
+ 'error_generic' => __( 'Something went wrong. Please try again.', 'latepoint' ),
+ )
+ );
+
$latepoint_css_variables = OsStylesHelper::generate_css_variables();
wp_add_inline_style( 'latepoint-main-admin', $latepoint_css_variables );
--- a/latepoint/lib/config/capabilities_for_controllers.php
+++ b/latepoint/lib/config/capabilities_for_controllers.php
@@ -31,6 +31,7 @@
'update' => [ 'booking__edit' ],
'create' => [ 'booking__create' ],
'destroy' => [ 'booking__delete' ],
+ 'bulk_destroy' => [ 'booking__delete' ],
],
],
'OsOrdersController' => [
--- a/latepoint/lib/controllers/bookings_controller.php
+++ b/latepoint/lib/controllers/bookings_controller.php
@@ -416,6 +416,164 @@
}
+ /**
+ * Maximum number of appointments accepted in a single bulk-delete request.
+ * Acts as a guard against accidental or malicious oversized payloads.
+ */
+ const BULK_DESTROY_MAX_IDS = 100;
+
+ /**
+ * Bulk-delete appointments by ID.
+ *
+ * Expects POST params:
+ * - _wpnonce: nonce for action 'bulk_destroy_bookings'.
+ * - ids: array of booking IDs (or comma-separated string).
+ *
+ * Verifies the user can delete bookings, validates input, then attempts to
+ * delete each ID after a per-record capability check. Fires the standard
+ * latepoint_booking_will_be_deleted / latepoint_booking_deleted hooks per
+ * successful deletion and logs activity.
+ *
+ * Responds with JSON containing status, message, deleted_ids, failed_ids,
+ * deleted_count and failed_count.
+ *
+ * @return void
+ */
+ public function bulk_destroy() {
+ $this->check_nonce( 'bulk_destroy_bookings' );
+
+ if ( ! OsRolesHelper::can_user_perform_model_action( 'OsBookingModel', 'delete' ) ) {
+ $this->send_json(
+ array(
+ 'status' => LATEPOINT_STATUS_ERROR,
+ 'message' => __( 'You do not have permission to delete appointments.', 'latepoint' ),
+ )
+ );
+ return;
+ }
+
+ $raw_ids = $this->params['ids'] ?? array();
+ if ( ! is_array( $raw_ids ) ) {
+ $raw_ids = explode( ',', (string) $raw_ids );
+ }
+
+ $ids = array();
+ foreach ( $raw_ids as $raw_id ) {
+ $id = absint( $raw_id );
+ if ( $id ) {
+ $ids[ $id ] = $id;
+ }
+ }
+
+ if ( empty( $ids ) ) {
+ $this->send_json(
+ array(
+ 'status' => LATEPOINT_STATUS_ERROR,
+ 'message' => __( 'No appointments selected.', 'latepoint' ),
+ )
+ );
+ return;
+ }
+
+ if ( count( $ids ) > self::BULK_DESTROY_MAX_IDS ) {
+ $this->send_json(
+ array(
+ 'status' => LATEPOINT_STATUS_ERROR,
+ 'message' => sprintf(
+ /* translators: %d: maximum number of appointments allowed in a single bulk action */
+ __( 'Too many appointments selected. Please delete in batches of %d or fewer.', 'latepoint' ),
+ self::BULK_DESTROY_MAX_IDS
+ ),
+ )
+ );
+ return;
+ }
+
+ ignore_user_abort( true );
+ if ( function_exists( 'set_time_limit' ) ) {
+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+ @set_time_limit( 0 );
+ }
+
+ $deleted_ids = array();
+ $failed_ids = array();
+
+ foreach ( $ids as $id ) {
+ $booking = new OsBookingModel( $id );
+ if ( $booking->is_new_record() ) {
+ $failed_ids[] = $id;
+ continue;
+ }
+ if ( ! OsRolesHelper::can_user_make_action_on_model_record( $booking, 'delete' ) ) {
+ $failed_ids[] = $id;
+ continue;
+ }
+
+ /**
+ * Fires right before a booking is about to be deleted via bulk delete.
+ *
+ * @param {integer} $booking_id ID of the booking that will be deleted.
+ * @since 5.2.0
+ * @hook latepoint_booking_will_be_deleted
+ */
+ do_action( 'latepoint_booking_will_be_deleted', $id );
+
+ if ( $booking->delete() ) {
+ /**
+ * Fires right after a booking has been deleted via bulk delete.
+ *
+ * @param {integer} $booking_id ID of the booking that was deleted.
+ * @since 5.2.0
+ * @hook latepoint_booking_deleted
+ */
+ do_action( 'latepoint_booking_deleted', $id );
+ OsActivitiesHelper::log_booking_deleted( $booking );
+ $deleted_ids[] = $id;
+ } else {
+ $failed_ids[] = $id;
+ }
+ }
+
+ $deleted_count = count( $deleted_ids );
+ $failed_count = count( $failed_ids );
+
+ if ( $deleted_count && ! $failed_count ) {
+ $status = LATEPOINT_STATUS_SUCCESS;
+ $message = sprintf(
+ /* translators: %d: number of appointments deleted */
+ _n( '%d appointment deleted successfully.', '%d appointments deleted successfully.', $deleted_count, 'latepoint' ),
+ $deleted_count
+ );
+ } elseif ( $deleted_count && $failed_count ) {
+ $status = LATEPOINT_STATUS_SUCCESS;
+ $message = sprintf(
+ /* translators: 1: number of appointments deleted, 2: number of appointments that could not be deleted */
+ __( '%1$d appointment(s) deleted. %2$d could not be deleted.', 'latepoint' ),
+ $deleted_count,
+ $failed_count
+ );
+ } else {
+ $status = LATEPOINT_STATUS_ERROR;
+ $message = sprintf(
+ /* translators: %d: number of appointments that could not be deleted */
+ _n( '%d appointment could not be deleted.', '%d appointments could not be deleted.', $failed_count, 'latepoint' ),
+ $failed_count
+ );
+ }
+
+ $this->send_json(
+ array(
+ 'status' => $status,
+ 'message' => $message,
+ 'deleted_ids' => $deleted_ids,
+ 'failed_ids' => $failed_ids,
+ 'deleted_count' => $deleted_count,
+ 'failed_count' => $failed_count,
+ )
+ );
+ }
+
+
function change_status() {
if ( filter_var( $this->params['id'], FILTER_VALIDATE_INT ) ) {
--- a/latepoint/lib/controllers/invoices_controller.php
+++ b/latepoint/lib/controllers/invoices_controller.php
@@ -230,11 +230,7 @@
return;
}
- // Condition for pro compatibility. Remove later.
- if ( isset( $this->params['_wpnonce'] ) ) {
- // Verify nonce.
- $this->check_nonce( 'change_invoice_status_' . $this->params['invoice_id'] );
- }
+ $this->check_nonce( 'change_invoice_status_' . $this->params['invoice_id'] );
if ( ! in_array( $this->params['status'], array_keys( OsInvoicesHelper::list_of_statuses_for_select() ) ) ) {
echo 'Invalid Status';
--- a/latepoint/lib/controllers/processes_controller.php
+++ b/latepoint/lib/controllers/processes_controller.php
@@ -89,12 +89,14 @@
$operator = $this->params['operator'];
$trigger_condition_id = $this->params['trigger_condition_id'];
$values = OsProcessesHelper::values_for_trigger_condition_property( $property );
+ $message = LatePointMiscProcessEvent::value_field_html_for_trigger_condition( $property, '', $trigger_condition_id );
+
if ( $this->get_return_format() == 'json' ) {
$this->send_json(
array(
'status' => LATEPOINT_STATUS_SUCCESS,
- 'message' => OsFormHelper::multi_select_field( 'process[event][trigger_conditions][' . $trigger_condition_id . '][value]', false, $values, false, [] ),
- )
+ 'message' => $message,
+ )
);
}
}
--- a/latepoint/lib/helpers/booking_helper.php
+++ b/latepoint/lib/helpers/booking_helper.php
@@ -2187,4 +2187,65 @@
return '<td>' . esc_html( $column_value ) . '</td>';
}
}
+
+ /**
+ * Returns the <th> HTML for the bulk-selection column header (with the select-all checkbox).
+ *
+ * @return string
+ */
+ public static function render_bulk_select_header_cell(): string {
+ return '<th class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
+ '<label class="os-bulk-row-check-w">' .
+ '<input type="checkbox" class="os-bulk-select-all" aria-label="' . esc_attr__( 'Select all appointments on this page', 'latepoint' ) . '" />' .
+ '<span class="os-bulk-check-box"></span>' .
+ '</label>' .
+ '</th>';
+ }
+
+ /**
+ * Returns the empty spacer <th> HTML for the bulk-selection column in filter and footer rows.
+ *
+ * @return string
+ */
+ public static function render_bulk_select_spacer_cell(): string {
+ return '<th class="os-bulk-select-cell"></th>';
+ }
+
+ /**
+ * Returns the <td> HTML for the bulk-selection column on a single body row.
+ *
+ * @param OsBookingModel $booking Current booking row.
+ * @return string
+ */
+ public static function render_bulk_select_body_cell( OsBookingModel $booking ): string {
+ /* translators: %d: appointment ID */
+ $aria_label = sprintf( __( 'Select appointment %d', 'latepoint' ), $booking->id );
+ return '<td class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
+ '<label class="os-bulk-row-check-w">' .
+ '<input type="checkbox" class="os-bulk-row-check" value="' . esc_attr( $booking->id ) . '" aria-label="' . esc_attr( $aria_label ) . '" />' .
+ '<span class="os-bulk-check-box"></span>' .
+ '</label>' .
+ '</td>';
+ }
+
+ /**
+ * Returns the HTML for the bulk-actions toolbar shown above the appointments table.
+ *
+ * @return string
+ */
+ public static function render_bulk_actions_bar(): string {
+ return '<div class="os-bulk-actions-bar" data-bulk-nonce="' . esc_attr( wp_create_nonce( 'bulk_destroy_bookings' ) ) . '">' .
+ '<div class="os-bulk-actions-info">' .
+ '<span class="os-bulk-selected-count">0</span>' .
+ // Label is swapped between singular and plural by JS based on selection count.
+ '<span class="os-bulk-selected-label">' . esc_html__( 'Appointments selected', 'latepoint' ) . '</span>' .
+ '</div>' .
+ '<div class="os-bulk-actions-controls">' .
+ '<a href="#" class="latepoint-btn latepoint-btn-danger os-bulk-action-delete">' .
+ '<i class="latepoint-icon latepoint-icon-trash-2"></i><span>' . esc_html__( 'Delete Selected', 'latepoint' ) . '</span>' .
+ '</a>' .
+ '<a href="#" class="os-bulk-actions-clear">' . esc_html__( 'Clear selection', 'latepoint' ) . '</a>' .
+ '</div>' .
+ '</div>';
+ }
}
--- a/latepoint/lib/helpers/customer_import_helper.php
+++ b/latepoint/lib/helpers/customer_import_helper.php
@@ -40,7 +40,7 @@
if ( empty( $email ) || ! OsUtilHelper::is_valid_email( $email ) ) {
return [
'status' => false,
- 'message' => esc_html__( 'Invalid email address: ' . $email, 'latepoint' ),
+ 'message' => sprintf( esc_html__( 'Invalid email address: %s', 'latepoint' ), $email ),
];
}
$customer = new OsCustomerModel();
@@ -48,7 +48,7 @@
if ( $customer ) {
return [
'status' => false,
- 'message' => esc_html__( 'Customer with email already exists: ' . $email, 'latepoint' ),
+ 'message' => sprintf( esc_html__( 'Customer with email already exists: %s', 'latepoint' ), $email ),
];
}
return [ 'status' => true ];
--- a/latepoint/lib/helpers/transaction_helper.php
+++ b/latepoint/lib/helpers/transaction_helper.php
@@ -19,7 +19,7 @@
echo '<div class="transaction-refund-settings">';
echo '<div class="refund-settings-heading"><div>' . esc_html__( 'Refund Amount', 'latepoint' ) . '</div><div class="refund-settings-close"><i class="latepoint-icon latepoint-icon-x"></i></div></div>';
echo '<div class="refund-settings-fields">';
- $full_amount_label = sprintf( __( 'Full [%s]' ), OsMoneyHelper::format_price( ( $transaction->amount - $transaction->get_total_refunded_amount() ), true, false ) );
+ $full_amount_label = sprintf( __( 'Full [%s]', 'latepoint' ), OsMoneyHelper::format_price( ( $transaction->amount - $transaction->get_total_refunded_amount() ), true, false ) );
echo OsFormHelper::select_field(
'transaction_refund[portion]',
false,
--- a/latepoint/lib/helpers/whatsapp_helper.php
+++ b/latepoint/lib/helpers/whatsapp_helper.php
@@ -118,7 +118,7 @@
$html .= '</div>';
$html .= '<div class="latepoint-whatsapp-template-preview-variables-inner">';
$smart_variables_link = '<a href="#" class="open-template-variables-panel">' . esc_html__( 'Click here', 'latepoint' ) . '</a>';
- $html .= '<div class="latepoint-whatsapp-note">' . sprintf( __( 'You have to assign values for each variable that is used in this template. %s to show smart variables that you can use.' ), $smart_variables_link ) . '</div>';
+ $html .= '<div class="latepoint-whatsapp-note">' . sprintf( __( 'You have to assign values for each variable that is used in this template. %s to show smart variables that you can use.', 'latepoint' ), $smart_variables_link ) . '</div>';
$color_index = 0;
foreach ( $variables_by_type as $variable_type => $variables ) {
if ( ! empty( $variables ) ) {
--- a/latepoint/lib/misc/process_event.php
+++ b/latepoint/lib/misc/process_event.php
@@ -377,20 +377,15 @@
],
[ 'class' => 'process-condition-operators-w' ]
) .
- OsFormHelper::multi_select_field(
- 'process[event][trigger_conditions][' . $trigger_condition['id'] . '][value]',
- false,
- OsProcessesHelper::values_for_trigger_condition_property( $trigger_condition['property'] ),
- $trigger_condition['value'] ? explode( ',', $trigger_condition['value'] ) : [],
- [],
- [
- 'class' => 'process-condition-values-w',
- 'style' => in_array( $trigger_condition['operator'], [ 'changed', 'not_changed' ] ) ? 'display: none;' : '',
- ]
+ self::value_field_html_for_trigger_condition(
+ $trigger_condition['property'],
+ $trigger_condition['value'] ?: '',
+ $trigger_condition['id'],
+ $trigger_condition['operator'] ?? ''
) .
- '<div data-os-action="' . OsRouterHelper::build_route_name( 'processes', 'new_trigger_condition' ) . '"
- data-os-pass-response="yes"
- data-os-pass-this="yes"
+ '<div data-os-action="' . OsRouterHelper::build_route_name( 'processes', 'new_trigger_condition' ) . '"
+ data-os-pass-response="yes"
+ data-os-pass-this="yes"
data-os-before-after="none"
data-os-params="' . OsUtilHelper::build_os_params( [ 'event_type' => $this->type ] ) . '"
data-os-after-call="latepoint_add_process_condition"><button class="latepoint-btn-outline latepoint-btn"><i class="latepoint-icon latepoint-icon-plus2"></i><span>' . __( 'AND', 'latepoint' ) . '</span></button></div>' .
@@ -398,10 +393,87 @@
return $html;
}
+ /**
+ * Renders the value-input HTML for a single trigger condition. Numeric
+ * properties get a bordered number input; other properties get the
+ * existing multi-select dropdown populated from
+ * OsProcessesHelper::values_for_trigger_condition_property().
+ *
+ * Used inline by generate_trigger_condition_form_html() and from the
+ * processes__available_values_for_trigger_condition_property AJAX endpoint.
+ */
+ public static function value_field_html_for_trigger_condition( string $property, string $value, string $trigger_condition_id, string $operator = '' ): string {
+ $name = 'process[event][trigger_conditions][' . $trigger_condition_id . '][value]';
+
+ if ( self::is_numeric_trigger_condition_property( $property ) ) {
+ return OsFormHelper::number_field(
+ $name,
+ '',
+ $value,
+ 0,
+ null,
+ [
+ 'step' => '1',
+ 'theme' => 'bordered',
+ 'placeholder' => __( 'Count', 'latepoint' ),
+ ],
+ [
+ 'class' => 'process-condition-values-w',
+ ]
+ );
+ }
+
+ return OsFormHelper::multi_select_field(
+ $name,
+ '',
+ OsProcessesHelper::values_for_trigger_condition_property( $property ),
+ $value ? explode( ',', $value ) : [],
+ [],
+ [
+ 'class' => 'process-condition-values-w',
+ 'style' => in_array( $operator, [ 'changed', 'not_changed' ] ) ? 'display: none;' : '',
+ ]
+ );
+ }
+
+ /**
+ * Returns true when the given condition property holds a numeric value
+ * (free-form integer input, comparable with greater/less-than operators)
+ * rather than a fixed set of options (status keys, agent IDs, etc.).
+ *
+ * Used by trigger_condition_operators_for_property() to expose numeric
+ * operators and by value_field_html_for_trigger_condition() to render a
+ * <input type="number"> instead of the default multi-select dropdown.
+ */
+ public static function is_numeric_trigger_condition_property( string $property ): bool {
+ if ( ! $property ) {
+ return false;
+ }
+ $numeric_properties = [
+ 'booking__order_item_counts',
+ 'order__order_item_counts',
+ ];
+
+ /**
+ * Filter the list of trigger condition properties that should be treated as numeric.
+ *
+ * @since 5.x
+ * @hook latepoint_process_event_numeric_trigger_condition_properties
+ *
+ * @param {string[]} $numeric_properties Array of property codes in object__attribute format
+ *
+ * @returns {string[]} Filtered list of numeric property codes
+ */
+ $numeric_properties = apply_filters( 'latepoint_process_event_numeric_trigger_condition_properties', $numeric_properties );
+
+ return in_array( $property, $numeric_properties, true );
+ }
+
public static function trigger_condition_operators_for_property( string $property = '' ) {
$property_object = $property ? explode( '__', $property )[0] : 'booking';
$property_attribute = $property ? explode( '__', $property )[1] : '';
$operators = [];
+
switch ( $property_object ) {
case 'old_order':
case 'old_booking':
@@ -419,8 +491,15 @@
case 'agent':
case 'service':
case 'transaction':
- // TODO time range operators instead of removing these opearators completely
- if ( $property_attribute != 'start_datetime_utc' ) {
+ if ( self::is_numeric_trigger_condition_property( $property ) ) {
+ $operators['equal'] = __( 'is equal to', 'latepoint' );
+ $operators['not_equal'] = __( 'is not equal to', 'latepoint' );
+ $operators['greater_than'] = __( 'is greater than', 'latepoint' );
+ $operators['less_than'] = __( 'is less than', 'latepoint' );
+ $operators['greater_or_equal'] = __( 'is greater than or equal to', 'latepoint' );
+ $operators['less_or_equal'] = __( 'is less than or equal to', 'latepoint' );
+ } elseif ( $property_attribute != 'start_datetime_utc' ) {
+ // TODO time range operators instead of removing these opearators completely
$operators['equal'] = __( 'is equal to', 'latepoint' );
$operators['not_equal'] = __( 'is not equal to', 'latepoint' );
}
@@ -466,9 +545,10 @@
break;
case 'booking_created':
$properties = [
- 'booking__status' => __( 'Booking Status', 'latepoint' ),
- 'booking__service_id' => __( 'Service', 'latepoint' ),
- 'booking__agent_id' => __( 'Agent', 'latepoint' ),
+ 'booking__status' => __( 'Booking Status', 'latepoint' ),
+ 'booking__service_id' => __( 'Service', 'latepoint' ),
+ 'booking__agent_id' => __( 'Agent', 'latepoint' ),
+ 'booking__order_item_counts' => __( 'Order Item Counts', 'latepoint' ),
];
break;
case 'booking_updated':
@@ -504,7 +584,7 @@
'transaction_created',
'payment_request_created',
];
-
+
/**
* Returns an array of event types that trigger automation process
*
@@ -543,7 +623,7 @@
* @returns {array} Filtered array of event types/names
*/
$names = apply_filters( 'latepoint_process_event_names', $names );
-
+
return $names[ $type ] ?? $type;
}
--- a/latepoint/lib/models/booking_model.php
+++ b/latepoint/lib/models/booking_model.php
@@ -153,6 +153,7 @@
'agent_id' => __( 'Agent', 'latepoint' ),
'status' => __( 'Status', 'latepoint' ),
'start_datetime_utc' => __( 'Start Time', 'latepoint' ),
+ 'order_item_counts' => __( 'Order Item Counts', 'latepoint' ),
];
}
--- a/latepoint/lib/models/order_model.php
+++ b/latepoint/lib/models/order_model.php
@@ -395,6 +395,7 @@
'status' => __( 'Order Status', 'latepoint' ),
'fulfillment_status' => __( 'Fulfillment Status', 'latepoint' ),
'payment_status' => __( 'Payment Status', 'latepoint' ),
+ 'order_item_counts' => __( 'Order Item Counts', 'latepoint' ),
];
}
--- a/latepoint/lib/models/process_model.php
+++ b/latepoint/lib/models/process_model.php
@@ -37,17 +37,38 @@
foreach ( $this->event->trigger_conditions as $condition ) {
foreach ( $objects as $object ) {
if ( $object['model'] == LatePointMiscProcessEvent::get_object_from_property( $condition['property'] ) ) {
- $attribute = LatePointMiscProcessEvent::get_object_attribute_from_property( $condition['property'] );
+ $attribute = LatePointMiscProcessEvent::get_object_attribute_from_property( $condition['property'] );
+ $object_value = self::resolve_attribute_value( $object, $attribute );
switch ( $condition['operator'] ) {
case 'equal':
$value_arr = explode( ',', $condition['value'] );
- if ( ! in_array( $object['model_ready']->$attribute, $value_arr ) ) {
+ if ( ! in_array( $object_value, $value_arr ) ) {
return false;
}
break;
case 'not_equal':
$value_arr = explode( ',', $condition['value'] );
- if ( in_array( $object['model_ready']->$attribute, $value_arr ) ) {
+ if ( in_array( $object_value, $value_arr ) ) {
+ return false;
+ }
+ break;
+ case 'greater_than':
+ if ( ! ( (float) $object_value > (float) $condition['value'] ) ) {
+ return false;
+ }
+ break;
+ case 'less_than':
+ if ( ! ( (float) $object_value < (float) $condition['value'] ) ) {
+ return false;
+ }
+ break;
+ case 'greater_or_equal':
+ if ( ! ( (float) $object_value >= (float) $condition['value'] ) ) {
+ return false;
+ }
+ break;
+ case 'less_or_equal':
+ if ( ! ( (float) $object_value <= (float) $condition['value'] ) ) {
return false;
}
break;
@@ -57,7 +78,7 @@
case 'not_changed':
foreach ( $objects as $object_to_compare ) {
if ( $object_to_compare['model'] == str_replace( 'old_', '', $object['model'] ) ) {
- if ( $object['model_ready']->$attribute != $object_to_compare['model_ready']->$attribute ) {
+ if ( $object_value != $object_to_compare['model_ready']->$attribute ) {
return false;
}
}
@@ -65,7 +86,7 @@
case 'changed':
foreach ( $objects as $object_to_compare ) {
if ( $object_to_compare['model'] == str_replace( 'old_', '', $object['model'] ) ) {
- if ( $object['model_ready']->$attribute == $object_to_compare['model_ready']->$attribute ) {
+ if ( $object_value == $object_to_compare['model_ready']->$attribute ) {
return false;
}
}
@@ -79,6 +100,68 @@
return true;
}
+ /**
+ * Returns the value of the attribute on the model. Most attributes are
+ * read directly from the model instance, but a small number are computed
+ * (e.g. order_item_counts, which depends on related records and is not
+ * stored on the booking or order row itself).
+ */
+ private static function resolve_attribute_value( array $object, string $attribute ) {
+ if ( $attribute === 'order_item_counts' ) {
+ if ( $object['model'] === 'booking' ) {
+ return self::compute_order_item_counts_for_booking( $object['model_ready'] );
+ }
+ if ( $object['model'] === 'order' ) {
+ return self::compute_order_item_counts_for_order( $object['model_ready'] );
+ }
+ }
+ return $object['model_ready']->$attribute;
+ }
+
+ /**
+ * Counts how many bookings belong to the same transaction as the given
+ * booking. Falls back to 1 (= "single booking") for any degenerate input
+ * so the evaluator never crashes on missing related records.
+ *
+ * Order of preference:
+ * - recurrence_id sibling count (catches recurring sets and bundle
+ * scheduling)
+ * - order item count via order_item_id -> order (catches cart checkouts
+ * with multiple distinct services)
+ * - 1 (no transaction context)
+ */
+ private static function compute_order_item_counts_for_booking( OsBookingModel $booking ): int {
+ if ( ! empty( $booking->order_item_id ) ) {
+ $order_item = new OsOrderItemModel( $booking->order_item_id );
+
+ if ( ! empty( $order_item->order_id ) ) {
+ $order = new OsOrderModel( $order_item->order_id );
+ $items = $order->get_items();
+ if ( ! empty( $items ) ) {
+ return count( $items );
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Counts how many items the given order has. Falls back to 1 for any
+ * degenerate input so the evaluator never crashes on missing related
+ * records. Used by the "Order Item Counts" condition on Order Created.
+ */
+ private static function compute_order_item_counts_for_order( OsOrderModel $order ): int {
+ if ( empty( $order->id ) ) {
+ return 0;
+ }
+ $items = $order->get_items();
+ if ( empty( $items ) ) {
+ return 0;
+ }
+ return count( $items );
+ }
+
public function get_info() {
return [
'name' => $this->name,
@@ -97,7 +180,7 @@
'process_id' => $id,
'status' => LATEPOINT_JOB_STATUS_SCHEDULED,
),
- array( '%d', '%s' )
+ array( '%d', '%s' )
);
do_action( 'latepoint_process_deleted', $id );
return true;
--- a/latepoint/lib/views/bookings/_table_body.php
+++ b/latepoint/lib/views/bookings/_table_body.php
@@ -15,6 +15,7 @@
if($bookings){
foreach ($bookings as $booking): ?>
<tr class="os-clickable-row" <?php echo OsBookingHelper::quick_booking_btn_html($booking->id); ?>>
+ <?php if ( ! empty( $can_bulk_delete ) ) { echo OsBookingHelper::render_bulk_select_body_cell( $booking ); } ?>
<?php
foreach ( $ordered_columns as $col_key => $col_def ) {
if ( ! OsSettingsHelper::is_bookings_column_visible( $col_def, $selected_columns, count( $services_list ), count( $agents_list ), count( $locations_list ) ) ) continue;
--- a/latepoint/lib/views/bookings/index.php
+++ b/latepoint/lib/views/bookings/index.php
@@ -23,6 +23,9 @@
exit; // Exit if accessed directly
}
?>
+<?php
+$can_bulk_delete = OsRolesHelper::can_user_perform_model_action( 'OsBookingModel', 'delete' );
+?>
<?php if($bookings){ ?>
<div class="table-with-pagination-w has-scrollable-table">
<div class="os-pagination-w with-actions">
@@ -38,6 +41,7 @@
<?php } ?>
</div>
</div>
+ <?php if ( $can_bulk_delete ) { echo OsBookingHelper::render_bulk_actions_bar(); } ?>
<div class="os-bookings-list">
<div class="os-scrollable-table-w">
<div class="os-table-w os-table-compact">
@@ -46,6 +50,7 @@
<?php echo OsFormHelper::hidden_field('filter[records_ordered_by_direction]', $records_ordered_by_direction, ['class' => 'records-ordered-by-direction os-table-filter']); ?>
<thead>
<tr>
+ <?php if ( $can_bulk_delete ) { echo OsBookingHelper::render_bulk_select_header_cell(); } ?>
<?php
foreach ( $ordered_columns as $col_key => $col_def ) {
if ( ! OsSettingsHelper::is_bookings_column_visible( $col_def, $selected_columns, count( $services_list ), count( $agents_list ), count( $locations_list ) ) ) continue;
@@ -54,6 +59,7 @@
?>
</tr>
<tr>
+ <?php if ( $can_bulk_delete ) { echo OsBookingHelper::render_bulk_select_spacer_cell(); } ?>
<?php
foreach ( $ordered_columns as $col_key => $col_def ) {
if ( ! OsSettingsHelper::is_bookings_column_visible( $col_def, $selected_columns, count( $services_list ), count( $agents_list ), count( $locations_list ) ) ) continue;
@@ -67,6 +73,7 @@
</tbody>
<tfoot>
<tr>
+ <?php if ( $can_bulk_delete ) { echo OsBookingHelper::render_bulk_select_spacer_cell(); } ?>
<?php
foreach ( $ordered_columns as $col_key => $col_def ) {
if ( ! OsSettingsHelper::is_bookings_column_visible( $col_def, $selected_columns, count( $services_list ), count( $agents_list ), count( $locations_list ) ) ) continue;
--- a/latepoint/lib/views/orders/_balance_and_payments.php
+++ b/latepoint/lib/views/orders/_balance_and_payments.php
@@ -56,7 +56,7 @@
<div class="payment-request-row">
<?php
$payment_portions = [];
- if($total_balance > 0) $payment_portions[LATEPOINT_PAYMENT_PORTION_FULL] = sprintf( __( 'Full Price [%s]' ), OsMoneyHelper::format_price( $total_balance, true, false ) );
+ if($total_balance > 0) $payment_portions[LATEPOINT_PAYMENT_PORTION_FULL] = sprintf( __( 'Full Price [%s]', 'latepoint' ), OsMoneyHelper::format_price( $total_balance, true, false ) );
if($deposit_amount > 0) $payment_portions[LATEPOINT_PAYMENT_PORTION_DEPOSIT] = sprintf( __( 'Deposit Only [%s]', 'latepoint' ), OsMoneyHelper::format_price( $deposit_amount, true, false ) );
$payment_portions[LATEPOINT_PAYMENT_PORTION_CUSTOM] = __( 'Custom', 'latepoint' );
--- a/latepoint/lib/views/orders/_full_summary.php
+++ b/latepoint/lib/views/orders/_full_summary.php
@@ -49,21 +49,21 @@
</div>
<div class="fsoi-main">
<span><?php esc_html_e( 'Order #', 'latepoint' ); ?></span>
- <strong><?php esc_html_e($order->confirmation_code); ?></strong>
+ <strong><?php echo esc_html( $order->confirmation_code ); ?></strong>
</div>
</div>
<div class="full-summary-order-info-elements">
<div class="fsoi-element">
<span><?php esc_html_e( 'Created:', 'latepoint' ); ?></span>
- <strong><?php esc_html_e(esc_html( OsTimeHelper::get_readable_date( new OsWpDateTime( $order->created_at, new DateTimeZone('UTC'))))); ?></strong>
+ <strong><?php echo esc_html( OsTimeHelper::get_readable_date( new OsWpDateTime( $order->created_at, new DateTimeZone( 'UTC' ) ) ) ); ?></strong>
</div>
<div class="fsoi-element">
<span><?php esc_html_e( 'Status:', 'latepoint' ); ?></span>
- <strong><?php esc_html_e($order->get_nice_status_name()); ?></strong>
+ <strong><?php echo esc_html( $order->get_nice_status_name() ); ?></strong>
</div>
<div class="fsoi-element">
<span><?php esc_html_e( 'Payment:', 'latepoint' ); ?></span>
- <strong><?php esc_html_e($order->get_nice_payment_status_name()); ?></strong>
+ <strong><?php echo esc_html( $order->get_nice_payment_status_name() ); ?></strong>
</div>
</div>
</div>
--- a/latepoint/lib/views/services/index.php
+++ b/latepoint/lib/views/services/index.php
@@ -13,7 +13,7 @@
<?php if(OsRolesHelper::can_user('service__create')){ ?>
- <?php echo OsUtilHelper::add_resource_link_html(__('New Service', 'latepoint-pro-features'), OsRouterHelper::build_link(OsRouterHelper::build_route_name('services', 'new_form') )); ?>
+ <?php echo OsUtilHelper::add_resource_link_html(__('New Service', 'latepoint'), OsRouterHelper::build_link(OsRouterHelper::build_route_name('services', 'new_form') )); ?>
<?php } ?>
</div>
</div>
@@ -32,7 +32,7 @@
<?php
} ?>
<?php if(OsRolesHelper::can_user('service__create')){ ?>
- <?php echo OsUtilHelper::add_resource_link_html(__('New Service', 'latepoint-pro-features'), OsRouterHelper::build_link(OsRouterHelper::build_route_name('services', 'new_form'), ['service_category_id' => $service_category->id] )); ?>
+ <?php echo OsUtilHelper::add_resource_link_html(__('New Service', 'latepoint'), OsRouterHelper::build_link(OsRouterHelper::build_route_name('services', 'new_form'), ['service_category_id' => $service_category->id] )); ?>
<?php } ?>
</div>
</div>
--- a/latepoint/lib/views/settings/general.php
+++ b/latepoint/lib/views/settings/general.php
@@ -631,7 +631,7 @@
); ?>"
>
<i class="latepoint-icon latepoint-icon-external-link"></i>
- <span><?php esc_html_e('Export Data'); ?></span>
+ <span><?php esc_html_e('Export Data', 'latepoint'); ?></span>
</a>
<a data-os-lightbox-classes="width-700" data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name('settings', 'import_modal')); ?>" href="#" data-os-output-target="lightbox" class="latepoint-btn latepoint-btn-grey latepoint-btn-outline"><i class="latepoint-icon latepoint-icon-download"></i><span><?php esc_html_e('Import Data', 'latepoint'); ?></span></a>
</div>
--- a/latepoint/lib/views/support_topics/view.php
+++ b/latepoint/lib/views/support_topics/view.php
@@ -10,7 +10,7 @@
}
?>
<div class="latepoint-lightbox-heading">
- <h2><?php esc_html_e(OsSupportTopicsHelper::get_title_for_topic($topic)); ?></h2>
+ <h2><?php echo esc_html( OsSupportTopicsHelper::get_title_for_topic( $topic ) ); ?></h2>
</div>
<div class="latepoint-lightbox-content">
<?php include('partials/'.sanitize_file_name($topic.'.php')); ?>