PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
← All changes | includes/page-appointment-services/_src/appointment_services_page.js +1235 -201 11.5 → 11.9 View file →
@@ -2,8 +2,9 @@
2 2 'use strict';
3 3 var config = w.wpbc_appointment_services_config || {};
4 4 var state = {
5 5 storageReady: false,
6 + catalog_loading: true,
6 7 selectedId: Number( config.selected_id || 0 ),
7 8 requested_focus: String( config.focus_section || '' ),
8 9 focus_handled: false,
9 10 busy: false,
@@ -11,17 +12,36 @@
11 12 services: [],
12 13 providers: {},
13 14 providerCount: 0,
14 15 editor_snapshot: '',
16 + editor_request_sequence: 0,
17 + initial_selection_pending: 0 < Number( config.selected_id || 0 ),
18 + inspector_focus_target: null,
19 + mutation_in_progress: false,
20 + operation_mode: '',
21 + operation_review: null,
22 + operation_request_sequence: 0,
23 + inline_editing: false,
24 + inline_drafts: {},
25 + inline_schema: {},
26 + inline_schema_loading: false,
27 + inline_request_sequence: 0,
28 + last_response: null,
15 29 page: 1,
16 - page_size: Number( config.listing && config.listing.items_per_page ? config.listing.items_per_page : 10 ),
30 + page_size: Number( config.catalog && config.catalog.initial_request && config.catalog.initial_request.items_per_page ? config.catalog.initial_request.items_per_page : 10 ),
17 31 total_items: 0,
18 32 total_pages: 0,
19 - sort_by: String( config.listing && config.listing.sort_by ? config.listing.sort_by : 'service_id' ),
20 - sort_order: String( config.listing && config.listing.sort_order ? config.listing.sort_order : 'desc' )
33 + sort_by: String( config.catalog && config.catalog.initial_request && config.catalog.initial_request.sort_by ? config.catalog.initial_request.sort_by : 'service_id' ),
34 + sort_order: String( config.catalog && config.catalog.initial_request && config.catalog.initial_request.sort_order ? config.catalog.initial_request.sort_order : 'desc' )
21 35 };
36 + var catalogController = false;
37 + var inlineWorkflowController = false;
38 + var inlineReviewWorkflowController = false;
39 + var deleteReviewWorkflowController = false;
40 + var inspectorWorkflowController = false;
22 41 var searchTimer = 0;
23 42 var weekdayKeys = [ 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' ];
43 + var pending_highlight_ids = [];
24 44
25 45 /** Extract an API message while preserving a caller-provided fallback. */
26 46 function messageFrom( response, fallback ) { return response && response.data && response.data.message ? response.data.message : fallback; }
27 47 /** Display a shared Booking Calendar administrator notice. */
@@ -28,8 +48,39 @@
28 48 function notify( message, type ) {
29 49 if ( message && typeof w.wpbc_admin_show_message === 'function' ) { w.wpbc_admin_show_message( message, type || 'info', 5000, false ); }
30 50 }
31 51 /**
52 + * Return the shared signed-review presentation controller.
53 + *
54 + * @return {Object|false} Shared review controller or false when unavailable.
55 + */
56 + function get_inline_review_workflow() {
57 + if ( inlineReviewWorkflowController ) { return inlineReviewWorkflowController; }
58 + if ( ! w.wpbc_ui_catalog || 'function' !== typeof w.wpbc_ui_catalog.create_inline_review_workflow ) { return false; }
59 + inlineReviewWorkflowController = w.wpbc_ui_catalog.create_inline_review_workflow( {
60 + apply_selector: '.wpbc_appointment_services__operation_apply',
61 + cancel_selector: '.wpbc_appointment_services__cancel',
62 + root: document
63 + } );
64 + return inlineReviewWorkflowController;
65 + }
66 + /**
67 + * Return the shared permanent-deletion presentation controller.
68 + *
69 + * @return {Object|false} Shared deletion controller or false when unavailable.
70 + */
71 + function get_delete_review_workflow() {
72 + if ( deleteReviewWorkflowController ) { return deleteReviewWorkflowController; }
73 + if ( ! w.wpbc_ui_catalog || 'function' !== typeof w.wpbc_ui_catalog.create_delete_review_workflow ) { return false; }
74 + deleteReviewWorkflowController = w.wpbc_ui_catalog.create_delete_review_workflow( {
75 + acknowledgement_selector: '[data-wpbc-ui-catalog-delete-acknowledgement]',
76 + apply_selector: '.wpbc_appointment_services__operation_apply',
77 + cancel_selector: '.wpbc_appointment_services__cancel',
78 + root: document
79 + } );
80 + return deleteReviewWorkflowController;
81 + }
82 + /**
32 83 * Show the native Booking Calendar Processing notice.
33 84 *
34 85 * The returned element identifies this specific request's notice so
35 86 * overlapping requests cannot dismiss each other's feedback.
@@ -58,26 +109,23 @@
58 109 }
59 110 /**
60 111 * Send an authenticated Appointment Services AJAX request.
61 112 *
62 - * Every request uses the shared administrator Processing notice and removes
63 - * only its own notice after the request settles.
113 + * Requests normally use the shared administrator Processing notice and remove
114 + * only their own notice after settling. Inspector-loading requests may opt out
115 + * because the shared inspector already exposes equivalent progress feedback.
64 116 *
65 117 * @param {string} action WordPress AJAX action name.
66 - * @param {Object} data Request-specific payload.
118 + * @param {Object} data Request-specific payload.
119 + * @param {boolean} use_processing_notice Whether to show the global notice.
67 120 * @return {jqXHR} jQuery AJAX promise for the request.
68 121 */
69 - function request( action, data ) {
70 - var $processing_notice = show_processing_notice();
122 + function request( action, data, use_processing_notice ) {
123 + var $processing_notice = false === use_processing_notice ? $() : show_processing_notice();
71 124
72 125 return $.ajax( { url: config.ajax_url, type: 'POST', dataType: 'json', data: $.extend( { action: action, nonce: config.nonce }, data || {} ) } )
73 126 .always( function () { hide_processing_notice( $processing_notice ); } );
74 127 }
75 - /** Toggle the loading overlay without hiding the existing Service table. */
76 - function setLoading( isLoading ) {
77 - $( '.wpbc_appointment_services__loading' ).toggleClass( 'is-visible', isLoading ).attr( 'aria-hidden', isLoading ? 'false' : 'true' );
78 - $( '.wpbc_appointment_services__content' ).attr( 'aria-busy', isLoading ? 'true' : 'false' );
79 - }
80 128 /** Activate the Settings or Help panel selected in the right sidebar. */
81 129 function switchRightPanel( $tab ) {
82 130 var panelId = $tab.attr( 'aria-controls' );
83 131 var panel = panelId ? document.getElementById( panelId ) : null;
@@ -195,10 +243,11 @@
195 243 function editorIsOpen() { return ! $( '[data-service-field="title"]' ).prop( 'disabled' ); }
196 244 /**
197 245 * Synchronize one numeric Service field with its range control.
198 246 *
199 - * The slider expands to represent an existing value above its normal visual
200 - * range without clamping or rewriting that stored value.
247 + * Non-price sliders expand to represent an existing value above their normal
248 + * visual range. The price slider remains the product-defined 0-1000 control;
249 + * its number field can still preserve and submit a legacy value above 1000.
201 250 *
202 251 * @param {string} field_id Service field identifier.
203 252 * @return {void}
204 253 */
@@ -214,9 +263,11 @@
214 263 if ( ! $field.length || ! $range.length || ! isFinite( value ) ) { return; }
215 264 default_min = isFinite( default_min ) ? default_min : 0;
216 265 default_max = isFinite( default_max ) ? default_max : 100;
217 266 step = isFinite( step ) && step > 0 ? step : 1;
218 - range_max = value > default_max ? default_min + ( Math.ceil( ( value - default_min ) / step ) * step ) : default_max;
267 + range_max = 'base_cost' === field_id || value <= default_max
268 + ? default_max
269 + : default_min + ( Math.ceil( ( value - default_min ) / step ) * step );
219 270 $range.attr( { min: default_min, max: range_max } ).val( value );
220 271 }
221 272 /**
222 273 * Synchronize every numeric Service field with its range control.
@@ -251,17 +302,64 @@
251 302 }
252 303 /** Synchronize toolbar and inspector action states with the page state. */
253 304 function updateControls() {
254 305 var open = state.storageReady && editorIsOpen();
306 + var operation_open = !! state.operation_mode && 'loading' !== state.operation_mode;
307 + var operation_is_review = 'bulk_review' === state.operation_mode || 'inline_review' === state.operation_mode || 'delete_review' === state.operation_mode;
308 + var operation_is_delete_review = 'delete_review' === state.operation_mode;
255 309 var hasPicture = !! String( $( '[data-service-field="picture_url"]' ).val() || '' ).trim();
256 - var show_save = open && 'true' === $( '#wpbc_tab_service_settings' ).attr( 'aria-selected' );
310 + var show_save = ( open || operation_open ) && 'true' === $( '#wpbc_tab_service_settings' ).attr( 'aria-selected' );
257 311 $( '.wpbc_appointment_services__add' ).prop( 'hidden', false ).prop( 'disabled', ! state.storageReady || state.busy );
258 312 $( '.wpbc_appointment_services__right_sidebar_footer, .wpbc_appointment_services__top_actions' ).prop( 'hidden', ! show_save );
259 - $( '.wpbc_appointment_services__save' ).prop( 'hidden', ! show_save ).prop( 'disabled', ! open || state.busy );
313 + $( '.wpbc_appointment_services__cancel' ).prop( 'hidden', ! show_save ).prop( 'disabled', ( ! open && ! operation_open ) || state.busy );
314 + $( '.wpbc_appointment_services__save' ).prop( 'hidden', ! show_save || operation_open ).prop( 'disabled', ! open || state.busy );
315 + $( '.wpbc_appointment_services__operation_review' ).prop( 'hidden', 'bulk_edit' !== state.operation_mode ).prop( 'disabled', 'bulk_edit' !== state.operation_mode || state.busy || ! collect_bulk_changes() );
316 + $( '.wpbc_appointment_services__operation_apply' ).prop( 'hidden', ! operation_is_review );
317 + if ( ! operation_is_delete_review ) {
318 + $( '.wpbc_appointment_services__operation_apply' ).prop( 'disabled', ! operation_is_review || state.busy || ! state.operation_review );
319 + }
260 320 $( '.wpbc_appointment_services__duplicate, .wpbc_appointment_services__archive' ).prop( 'disabled', ! open || ! state.selectedId || state.busy );
261 321 $( '.wpbc_appointment_services__media_preview, .wpbc_appointment_services__select_image' ).prop( 'disabled', ! open || state.busy );
262 322 $( '.wpbc_appointment_services__remove_image' ).prop( 'disabled', ! open || ! hasPicture || state.busy );
323 + synchronize_inline_workflow();
263 324 }
325 +
326 + /** Synchronize the shared inline workflow from Service-owned state. */
327 + function synchronize_inline_workflow() {
328 + var changed_count = get_inline_changed_count();
329 +
330 + if ( ! inlineWorkflowController ) { return; }
331 + inlineWorkflowController.synchronize( {
332 + active: state.inline_editing,
333 + busy: state.busy,
334 + changed_count: changed_count,
335 + count_text: String( config.i18n.changed_rows || '%s changed rows' ).replace( '%s', changed_count ),
336 + has_items: 0 < state.services.length,
337 + lock_controls: state.inline_schema_loading || !! state.operation_mode,
338 + toggle_disabled: state.catalog_loading || ! state.storageReady || !! state.operation_mode,
339 + active_toggle_text: config.i18n.editing_rows,
340 + inactive_toggle_text: config.catalog && config.catalog.i18n ? config.catalog.i18n.edit_rows : ''
341 + } );
342 + }
343 + /**
344 + * Stop page-changing catalog controls while a pending operation owns row state.
345 + *
346 + * Summary elements do not honor the disabled property, so this capture guard
347 + * complements the visual disabled state while Service inline or bulk editing is active.
348 + *
349 + * @param {Event} event Captured catalog event.
350 + * @return {void}
351 + */
352 + function protect_inline_drafts_from_catalog_controls( event ) {
353 + var protected_control;
354 +
355 + if ( inlineWorkflowController && inlineWorkflowController.protect_event( event, state.inline_editing || state.inline_schema_loading || !! state.operation_mode ) ) { return; }
356 + if ( ( ! state.inline_editing && ! state.inline_schema_loading && ! state.operation_mode ) || ! event.target || ! event.target.closest ) { return; }
357 + protected_control = event.target.closest( '.wpbc_appointment_services__status_filter, #wpbc_service_provider_filter, .wpbc_appointment_services__add' );
358 + if ( ! protected_control ) { return; }
359 + event.preventDefault();
360 + event.stopImmediatePropagation();
361 + }
264 362 /** Mark the page busy during a mutating request and refresh controls. */
265 363 function setBusy( value ) { state.busy = value; $( '.wpbc_appointment_services_page' ).toggleClass( 'is-busy', value ); updateControls(); }
266 364 /** Enable or disable all fields in the Service inspector. */
267 365 function setFieldsEnabled( enabled ) { $( '[data-service-field], [data-service-range-field], [data-service-status-choice]' ).prop( 'disabled', ! enabled ); updateControls(); }
@@ -266,21 +364,49 @@
266 364 /** Enable or disable all fields in the Service inspector. */
267 365 function setFieldsEnabled( enabled ) { $( '[data-service-field], [data-service-range-field], [data-service-status-choice]' ).prop( 'disabled', ! enabled ); updateControls(); }
268 366 /** Return defaults for a new unsaved Service. */
269 367 function blankService() {
270 - return { service_id: 0, title: '', description: '', picture_url: '', status: 'active', duration_minutes: 30, buffer_before_minutes: 0, buffer_after_minutes: 0, base_cost: '0.00', booking_form_id: 0, resource_ids: [] };
368 + return { service_id: 0, title: '', description: '', picture_url: '', status: 'active', duration_minutes: 30, buffer_before_minutes: 0, buffer_after_minutes: 0, base_cost: '0.00', booking_form_id: 0, resource_ids: ( config.default_provider_ids || [] ).slice() };
271 369 }
370 + /**
371 + * Render the Service-owned create or edit inspector header template.
372 + *
373 + * Existing Service fields remain in their native collapsible groups so the
374 + * template changes presentation without moving domain validation to shared code.
375 + *
376 + * @param {boolean} is_edit Whether an existing Service is being edited.
377 + * @return {void}
378 + */
379 + function render_inspector_header( is_edit ) {
380 + var template_id = is_edit ? 'wpbc-appointment-service-inspector-edit' : 'wpbc-appointment-service-inspector-create';
381 + var template = catalogTemplate( template_id );
382 + var $header = $( '[data-wpbc-appointment-service-inspector-header]' );
383 + var context = is_edit
384 + ? String( config.i18n.inspector_context_id || 'ID: %d' ).replace( '%d', String( state.selectedId ) )
385 + : String( config.i18n.inspector_context_new || 'New' );
386 +
387 + if ( ! template || ! $header.length ) {
388 + return;
389 + }
390 +
391 + $header.html( template( {
392 + title: is_edit ? config.i18n.edit_service_title : config.i18n.create_service_title,
393 + context: context,
394 + description: is_edit ? config.i18n.edit_service_description : config.i18n.create_service_description
395 + } ) );
396 + }
272 397 /** Populate the inspector from a normalized Service response. */
273 398 function fillEditor( service ) {
274 399 service = $.extend( blankService(), service || {} );
275 400 state.selectedId = Number( service.service_id || 0 );
401 + render_inspector_header( 0 < state.selectedId );
276 402 $.each( service, function ( key, value ) { $( '[data-service-field="' + key + '"]' ).val( value ); } );
277 403 sync_status_radios();
278 404 sync_all_numeric_ranges();
279 405 updateMediaPreview();
280 406 setFieldsEnabled( state.storageReady );
281 - $( '.wpbc_appointment_services__item' ).removeClass( 'is-selected' ).attr( 'aria-current', 'false' );
282 - $( '.wpbc_appointment_services__item[data-service-id="' + state.selectedId + '"]' ).addClass( 'is-selected' ).attr( 'aria-current', 'true' );
407 + $( '.wpbc_appointment_services__item' ).removeClass( 'is-inspector-selected' ).attr( 'aria-current', 'false' );
408 + $( '.wpbc_appointment_services__item[data-service-id="' + state.selectedId + '"]' ).addClass( 'is-inspector-selected' ).attr( 'aria-current', 'true' );
283 409 capture_editor_snapshot();
284 410 }
285 411 /** Collect the current Service inspector values for saving. */
286 412 function collectEditor() {
@@ -311,8 +437,129 @@
311 437 */
312 438 function can_replace_editor() {
313 439 return ! is_editor_dirty() || w.confirm( config.i18n.confirm_discard || 'Discard unsaved Service changes?' );
314 440 }
441 + /**
442 + * Determine whether the current Service operation contains unapplied values.
443 + *
444 + * Inline drafts and signed review screens remain dirty until they are
445 + * explicitly applied or discarded. This prevents an outside click from
446 + * silently replacing a reviewed mutation.
447 + *
448 + * @return {boolean} True when closing would discard pending changes.
449 + */
450 + function is_operation_dirty() {
451 + if ( 'bulk_edit' === state.operation_mode ) {
452 + return !! collect_bulk_changes();
453 + }
454 +
455 + if ( 'inline_review' === state.operation_mode || 'bulk_review' === state.operation_mode || 'delete_review' === state.operation_mode ) {
456 + return !! state.operation_review;
457 + }
458 +
459 + return state.inline_editing && 0 < get_inline_changed_count();
460 + }
461 + /**
462 + * Clear the Service editor after its native sidebar has been closed.
463 + *
464 + * Invalidating the request sequence prevents a late load response from
465 + * reopening an inspector that the user already dismissed.
466 + *
467 + * @return {void}
468 + */
469 + function reset_service_editor() {
470 + var service = blankService();
471 +
472 + state.editor_request_sequence += 1;
473 + state.selectedId = 0;
474 + state.requested_focus = '';
475 + state.focus_handled = true;
476 + render_inspector_header( false );
477 + $.each( service, function ( key, value ) { $( '[data-service-field="' + key + '"]' ).val( value ); } );
478 + sync_status_radios();
479 + sync_all_numeric_ranges();
480 + updateMediaPreview();
481 + setFieldsEnabled( false );
482 + $( '.wpbc_appointment_services__item' ).removeClass( 'is-inspector-selected' ).attr( 'aria-current', 'false' );
483 + capture_editor_snapshot();
484 + updateUrl( 0 );
485 + }
486 + /**
487 + * Clear a Service inline or bulk operation without changing selection.
488 + *
489 + * @param {boolean} restore_inline Whether inline rows should return to presentation mode.
490 + * @return {void}
491 + */
492 + function reset_operation( restore_inline ) {
493 + state.operation_request_sequence += 1;
494 + state.operation_mode = '';
495 + state.operation_review = null;
496 + $( '[data-wpbc-appointment-services-operation-host]' ).empty().prop( 'hidden', true );
497 + $( '[data-wpbc-appointment-services-native-inspector]' ).prop( 'hidden', false );
498 + $( '.wpbc_appointment_services__operation_apply' )
499 + .removeClass( 'wpbc_ui_catalog_delete_review__apply button-secondary is-busy' )
500 + .addClass( 'button-primary' )
501 + .removeAttr( 'aria-busy form' )
502 + .text( config.i18n.apply_changes || 'Apply changes' );
503 + if ( restore_inline ) {
504 + state.inline_editing = false;
505 + state.inline_drafts = {};
506 + state.inline_schema = {};
507 + state.inline_schema_loading = false;
508 + state.inline_request_sequence += 1;
509 + $( '[data-wpbc-appointment-services-inline-bar-host]' ).empty();
510 + if ( state.last_response ) {
511 + renderCatalogResponse( state.last_response );
512 + }
513 + }
514 + setBusy( false );
515 + }
516 + /**
517 + * Close the Service inspector using the Booking Resources lifecycle.
518 + *
519 + * @param {boolean} confirm_discard Whether to confirm unsaved changes.
520 + * @param {boolean} hide_sidebar Whether this function must hide the native sidebar.
521 + * @return {boolean} True when the inspector was closed.
522 + */
523 + function close_service_inspector( confirm_discard, hide_sidebar ) {
524 + var focus_target = state.inspector_focus_target;
525 +
526 + if ( state.mutation_in_progress ) {
527 + return false;
528 + }
529 +
530 + if ( state.operation_mode ) {
531 + if ( confirm_discard && is_operation_dirty() && ! w.confirm( config.i18n.confirm_discard || 'Discard unsaved Service changes?' ) ) {
532 + return false;
533 + }
534 + reset_operation( true );
535 + state.inspector_focus_target = null;
536 + if ( hide_sidebar && 'function' === typeof w.wpbc_admin_ui__sidebar_right__do_hide ) {
537 + w.wpbc_admin_ui__sidebar_right__do_hide();
538 + }
539 + notify_setup_wizard_layout_changed();
540 + if ( hide_sidebar && focus_target && document.documentElement.contains( focus_target ) && 'function' === typeof focus_target.focus ) {
541 + focus_target.focus();
542 + }
543 + return true;
544 + }
545 +
546 + if ( confirm_discard && ! can_replace_editor() ) {
547 + return false;
548 + }
549 +
550 + reset_service_editor();
551 + state.inspector_focus_target = null;
552 + if ( hide_sidebar && 'function' === typeof w.wpbc_admin_ui__sidebar_right__do_hide ) {
553 + w.wpbc_admin_ui__sidebar_right__do_hide();
554 + }
555 + notify_setup_wizard_layout_changed();
556 + if ( hide_sidebar && focus_target && document.documentElement.contains( focus_target ) && 'function' === typeof focus_target.focus ) {
557 + focus_target.focus();
558 + }
559 +
560 + return true;
561 + }
315 562 /** Reflect the selected Service in the admin URL without reloading. */
316 563 function updateUrl( serviceId ) {
317 564 if ( ! w.history || ! w.URL ) { return; }
318 565 var url = new w.URL( w.location.href );
@@ -318,17 +565,8 @@
318 565 var url = new w.URL( w.location.href );
319 566 if ( serviceId ) { url.searchParams.set( 'service_id', serviceId ); } else { url.searchParams.delete( 'service_id' ); }
320 567 w.history.replaceState( {}, '', url.toString() );
321 568 }
322 - /** Render the empty or storage-unavailable state in the central workspace. */
323 - function renderEmpty( message, storageNotice ) {
324 - var $empty = $( '.wpbc_appointment_services__empty' );
325 - var noProviders = ! storageNotice && ! message && 0 === state.providerCount;
326 - $( '.wpbc_appointment_services__list' ).prop( 'hidden', true );
327 - $empty.prop( 'hidden', false ).toggleClass( 'is-storage-notice', !! storageNotice );
328 - $empty.find( 'h2' ).text( storageNotice ? ( config.i18n.not_connected || 'Services storage is not connected' ) : ( noProviders ? ( config.i18n.no_providers || 'No Providers available' ) : ( config.i18n.empty || 'No Services yet' ) ) );
329 - $empty.find( 'p' ).text( message || ( noProviders ? config.i18n.no_providers_help : config.i18n.empty_help ) || '' );
330 - }
331 569 /** Index Provider presentation records by booking resource ID. */
332 570 function indexProviders( providers ) {
333 571 state.providers = {};
334 572 $.each( providers || [], function ( index, provider ) { state.providers[ String( provider.id ) ] = provider; } );
@@ -393,9 +631,9 @@
393 631 var service_description = String( service.description || '' ).trim() || config.i18n.no_description || 'No description';
394 632 var tooltip_format = String( config.i18n.service_thumbnail_tooltip || 'Title: %1$s\nDescription: %2$s' );
395 633 var tooltip_text = tooltip_format.replace( '%1$s', service_title ).replace( '%2$s', service_description );
396 634 var $thumbnail = $( '<span>', {
397 - 'class': 'wpbc_appointment_services__service_thumbnail tooltip_top',
635 + 'class': 'wpbc_ui_listing__table_icon wpbc_appointment_services__service_thumbnail tooltip_top',
398 636 'data-original-title': tooltip_text,
399 637 'aria-label': tooltip_text,
400 638 role: 'img',
401 639 tabindex: '0'
@@ -427,9 +665,9 @@
427 665 *
428 666 * @return {void}
429 667 */
430 668 function refresh_service_thumbnail_tooltips() {
431 - var listing_selector = '#wpbc_ui_listing_appointment_services_catalog ';
669 + var listing_selector = '#wpbc_appointment_services_catalog ';
432 670 var $thumbnails = $( listing_selector + '.wpbc_appointment_services__service_thumbnail' );
433 671 var tooltips_initialized = false;
434 672
435 673 if ( 'function' === typeof w.wpbc_define_tippy_tooltips ) {
@@ -530,202 +768,885 @@
530 768 return config.i18n.active || 'Active';
531 769 }
532 770 /** Format the translated table pagination summary. */
533 771 function showingText( from, to, total ) {
534 - var format = config.i18n.showing || 'Showing %1$s–%2$s of %3$s Services';
772 + var format = config.i18n.showing || 'Showing %1$s-%2$s of %3$s Services';
535 773 return format.replace( '%1$s', from ).replace( '%2$s', to ).replace( '%3$s', total );
536 774 }
775 + /** Return one compiled, allow-listed WordPress template. */
776 + function catalogTemplate( templateId ) {
777 + try { return templateId && w.wp && 'function' === typeof w.wp.template ? w.wp.template( templateId ) : null; }
778 + catch ( error ) { return null; }
779 + }
537 780 /**
538 - * Apply the server-authoritative listing preference to state and controls.
781 + * Return the shared native inspector state workflow for Service operations.
539 782 *
540 - * @param {Object} listing_settings Shared listing client settings.
783 + * @return {Object|false} Shared inspector workflow or false.
784 + */
785 + function get_operation_inspector_workflow() {
786 + var template_id;
787 +
788 + if ( inspectorWorkflowController ) { return inspectorWorkflowController; }
789 + if ( ! w.wpbc_ui_catalog || 'function' !== typeof w.wpbc_ui_catalog.create_inspector_workflow ) { return false; }
790 + template_id = config.catalog && config.catalog.templates ? config.catalog.templates.inspector : '';
791 + inspectorWorkflowController = w.wpbc_ui_catalog.create_inspector_workflow( {
792 + expand: function () {
793 + $( '[data-wpbc-appointment-services-operation-host]' ).prop( 'hidden', false );
794 + $( '[data-wpbc-appointment-services-native-inspector]' ).prop( 'hidden', true );
795 + expand_service_inspector();
796 + },
797 + get_footer: function () { return document.querySelector( '.wpbc_appointment_services__right_sidebar_footer' ); },
798 + get_host: function () { return document.querySelector( '[data-wpbc-appointment-services-operation-host]' ); },
799 + render_shell: function ( shell_data ) {
800 + var shell_template = catalogTemplate( template_id );
801 +
802 + return shell_template ? shell_template( shell_data ) : '';
803 + },
804 + shell_data: {
805 + catalog_id: config.catalog && config.catalog.id ? config.catalog.id : 'appointment_services_catalog',
806 + empty_icon: 'wpbc-bi-pencil-square',
807 + empty_message: '',
808 + empty_title: '',
809 + loading_label: config.i18n.loading || ''
810 + }
811 + } );
812 +
813 + return inspectorWorkflowController;
814 + }
815 + /** Return safe HTML from a detached jQuery presentation node. */
816 + function nodeHtml( $node ) { return $node && $node.length ? $( '<div>' ).append( $node ).html() : ''; }
817 + /**
818 + * Return the shared selection controller mounted for the Services catalog.
819 + *
820 + * @return {Object|null} Shared selection controller, or null before mount.
821 + */
822 + function get_selection_controller() {
823 + var mount = document.getElementById( 'wpbc_appointment_services_catalog' );
824 +
825 + return mount && mount._wpbc_ui_catalog_selection_controller ? mount._wpbc_ui_catalog_selection_controller : null;
826 + }
827 + /**
828 + * Return selected Service identifiers without exposing selection internals.
829 + *
830 + * @return {Array<number>} Persisted selected Service identifiers.
831 + */
832 + function get_selected_service_ids() {
833 + var selection = get_selection_controller();
834 +
835 + return selection && 'function' === typeof selection.get_selected_ids ? selection.get_selected_ids() : [];
836 + }
837 + /**
838 + * Return one Service from the last normalized catalog response.
839 + *
840 + * @param {number|string} service_id Service identifier to find.
841 + * @return {Object|null} Matching Service DTO, or null when not on this page.
842 + */
843 + function find_service( service_id ) {
844 + var found = null;
845 +
846 + $.each( state.services, function ( index, service ) {
847 + if ( Number( service.service_id || service.id || 0 ) === Number( service_id ) ) {
848 + found = service;
849 + return false;
850 + }
851 + } );
852 +
853 + return found;
854 + }
855 + /**
856 + * Return the allow-listed initial inline draft for one Service.
857 + *
858 + * Provider assignments, status, and buffers are deliberately omitted because
859 + * they require the reviewed bulk inspector or the complete Service inspector.
860 + *
861 + * @param {Object} row_schema Server-authoritative row schema.
862 + * @return {Object} Editable row-specific draft.
863 + */
864 + function create_inline_draft( row_schema ) {
865 + var draft = {};
866 +
867 + $.each( row_schema && Array.isArray( row_schema.fields ) ? row_schema.fields : [], function ( index, field ) {
868 + var field_key = String( field && field.key ? field.key : '' );
869 + if ( field_key ) { draft[ field_key ] = String( field.value ); }
870 + } );
871 +
872 + return draft;
873 + }
874 + /** Return one cached server-authoritative inline row schema. */
875 + function find_inline_schema( service_id ) {
876 + return state.inline_schema[ String( Number( service_id || 0 ) ) ] || null;
877 + }
878 + /**
879 + * Return whether one inline draft differs from its current Service DTO.
880 + *
881 + * @param {Object|null} row_schema Current server-authoritative row schema.
882 + * @param {Object|null} draft Row-specific inline draft.
883 + * @return {boolean} True when at least one allow-listed value changed.
884 + */
885 + function inline_draft_changed( row_schema, draft ) {
886 + var changed = false;
887 +
888 + if ( ! row_schema || ! draft ) { return false; }
889 + $.each( Array.isArray( row_schema.fields ) ? row_schema.fields : [], function ( index, field ) {
890 + if ( String( field.value ) !== String( draft[ field.key ] ) ) {
891 + changed = true;
892 + return false;
893 + }
894 + } );
895 +
896 + return changed;
897 + }
898 + /**
899 + * Return changed inline drafts keyed by Service ID.
900 + *
901 + * @return {Object<string,Object>} Changed drafts keyed by Service identifier.
902 + */
903 + function collect_inline_changes() {
904 + var changes = {};
905 +
906 + $.each( state.inline_drafts, function ( service_id, draft ) {
907 + var row_schema = find_inline_schema( service_id );
908 + var row_changes = {};
909 +
910 + $.each( row_schema && Array.isArray( row_schema.fields ) ? row_schema.fields : [], function ( index, field ) {
911 + if ( String( field.value ) !== String( draft[ field.key ] ) ) {
912 + row_changes[ field.key ] = draft[ field.key ];
913 + }
914 + } );
915 + if ( Object.keys( row_changes ).length ) {
916 + changes[ service_id ] = row_changes;
917 + }
918 + } );
919 +
920 + return changes;
921 + }
922 + /**
923 + * Return the count of changed Service rows in inline mode.
924 + *
925 + * @return {number} Number of row drafts that differ from their DTOs.
926 + */
927 + function get_inline_changed_count() { return Object.keys( collect_inline_changes() ).length; }
928 + /**
929 + * Render and register the sticky inline-editing status bar.
930 + *
931 + * Registration delegates viewport positioning to the shared selection
932 + * controller so the Service page follows the Resource catalog behavior.
933 + *
541 934 * @return {void}
542 935 */
543 - function sync_listing_settings( listing_settings ) {
544 - var page_size = Number( listing_settings && listing_settings.items_per_page ? listing_settings.items_per_page : state.page_size );
545 - if ( ! isFinite( page_size ) || page_size < 1 ) { return; }
546 - state.page_size = page_size;
547 - $( '[data-wpbc-listing-items-per-page-control="appointment_services_catalog"]' ).val( String( page_size ) );
936 + function render_inline_bar() {
937 + var template = catalogTemplate( 'wpbc-appointment-services-inline-bar' );
938 + var changed_count = get_inline_changed_count();
939 + var $host = $( '[data-wpbc-appointment-services-inline-bar-host]' );
940 +
941 + if ( ! template || ! $host.length ) { return; }
942 + if ( ! $host.children().length ) {
943 + $host.html( template( {
944 + title: config.i18n.editing_rows,
945 + changed_label: String( config.i18n.changed_rows || '%s changed rows' ).replace( '%s', changed_count ),
946 + description: config.i18n.inline_help,
947 + cancel: config.i18n.cancel,
948 + review: config.i18n.review_changes,
949 + changed_count: changed_count
950 + } ) );
951 + }
952 + synchronize_inline_bar();
548 953 }
549 954 /**
550 - * Synchronize sortable table headers with the server-authoritative ordering.
955 + * Synchronize the inline bar without replacing its active controls.
551 956 *
552 - * @param {Object} sorting Normalized sort key and direction.
957 + * A focused inline field emits its final change event while the Review
958 + * button is being clicked. Replacing the bar from that change handler would
959 + * remove the pointer target before the click event can complete.
960 + *
553 961 * @return {void}
554 962 */
555 - function sync_sorting_controls( sorting ) {
556 - state.sort_by = String( sorting && sorting.sort_by ? sorting.sort_by : state.sort_by );
557 - state.sort_order = 'desc' === String( sorting && sorting.sort_order ? sorting.sort_order : state.sort_order ) ? 'desc' : 'asc';
963 + function synchronize_inline_bar() {
964 + var changed_count = get_inline_changed_count();
965 + var $bar = $( '[data-wpbc-appointment-services-inline-bar]' );
558 966
559 - $( '[data-wpbc-listing-sort="appointment_services_catalog"]' ).each( function () {
560 - var $sort_link = $( this );
561 - var is_active = String( $sort_link.data( 'wpbc-listing-sort-key' ) || '' ) === state.sort_by;
562 - var $sort_icon = $sort_link.find( '.wpbc_ui_listing__sort_icon' );
967 + if ( ! $bar.length ) { return; }
968 + $bar.find( '[data-wpbc-appointment-services-inline-changed-label]' ).text(
969 + String( config.i18n.changed_rows || '%s changed rows' ).replace( '%s', changed_count )
970 + );
971 + synchronize_inline_workflow();
972 + }
973 + /**
974 + * Start row-specific inline editing for the current catalog page.
975 + *
976 + * Drafts intentionally cover the current page only. Page-changing controls
977 + * remain protected until the user cancels or completes the reviewed change.
978 + *
979 + * @return {void}
980 + */
981 + function start_inline_editing() {
982 + var request_sequence;
983 + var visible_ids;
563 984
564 - $sort_link.toggleClass( 'is-active', is_active );
565 - $sort_icon.removeClass( 'wpbc_icn_import_export wpbc-bi-arrow-down wpbc-bi-arrow-up' )
566 - .addClass( is_active ? ( 'desc' === state.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export' );
985 + if ( state.busy || state.catalog_loading || state.operation_mode ) { return; }
986 + if ( state.inline_editing ) {
987 + cancel_inline_editing( true );
988 + return;
989 + }
990 + if ( ! state.services.length ) { return; }
991 + if ( ! can_replace_editor() ) { return; }
992 + if ( editorIsOpen() ) { reset_service_editor(); }
993 + visible_ids = $.map( state.services, function ( service ) { return Number( service.service_id || service.id || 0 ); } );
994 + request_sequence = ++state.inline_request_sequence;
995 + state.inline_editing = false;
996 + state.inline_drafts = {};
997 + state.inline_schema = {};
998 + state.inline_schema_loading = true;
999 + setBusy( true );
1000 + request( config.actions.inline_schema, { ids: JSON.stringify( visible_ids ) } ).done( function ( response ) {
1001 + var schema = response && response.success && response.data ? response.data.schema : null;
1002 +
1003 + if ( request_sequence !== state.inline_request_sequence ) { return; }
1004 + if ( ! schema || ! Array.isArray( schema.rows ) ) {
1005 + notify( messageFrom( response, config.i18n.inline_schema_failed ), 'error' );
1006 + return;
1007 + }
1008 + $.each( schema.rows, function ( index, row_schema ) {
1009 + var service_id = String( Number( row_schema.service_id || 0 ) );
1010 + if ( '0' === service_id || ! Array.isArray( row_schema.fields ) || ! row_schema.fields.length ) { return; }
1011 + state.inline_schema[ service_id ] = row_schema;
1012 + state.inline_drafts[ service_id ] = create_inline_draft( row_schema );
1013 + } );
1014 + if ( ! Object.keys( state.inline_drafts ).length ) {
1015 + notify( config.i18n.inline_schema_failed, 'error' );
1016 + return;
1017 + }
1018 + state.inline_editing = true;
1019 + renderCatalogResponse( state.last_response );
1020 + render_inline_bar();
1021 + $( '[data-wpbc-appointment-services-inline-field]' ).first().trigger( 'focus' );
1022 + } ).fail( function ( xhr ) {
1023 + if ( request_sequence === state.inline_request_sequence ) {
1024 + notify( messageFrom( xhr.responseJSON, config.i18n.inline_schema_failed ), 'error' );
1025 + }
1026 + } ).always( function () {
1027 + if ( request_sequence === state.inline_request_sequence ) {
1028 + state.inline_schema_loading = false;
1029 + setBusy( false );
1030 + }
567 1031 } );
568 - $( '.wpbc_appointment_services__table th' ).each( function () {
569 - var $column = $( this );
570 - if ( ! $column.find( '.wpbc_ui_listing__sort_link' ).length ) {
1032 + }
1033 + /**
1034 + * Cancel Service inline editing and restore focus to its toolbar action.
1035 + *
1036 + * The stable shared data attribute is the interaction contract. Keeping the
1037 + * cancellation in this domain adapter preserves ownership of Service drafts
1038 + * while preventing surrounding page and sidebar click handlers from deciding
1039 + * whether those drafts should be discarded.
1040 + *
1041 + * @param {boolean} confirm_discard Whether changed drafts need confirmation.
1042 + * @return {boolean} True when inline editing was cancelled.
1043 + */
1044 + function cancel_inline_editing( confirm_discard ) {
1045 + var focus_target = document.querySelector( '[data-wpbc-ui-catalog-inline-toggle]' );
1046 +
1047 + if ( state.mutation_in_progress ) { return false; }
1048 + if ( confirm_discard && get_inline_changed_count() && ! w.confirm( config.i18n.confirm_discard || 'Discard unsaved Service changes?' ) ) {
1049 + return false;
1050 + }
1051 + reset_operation( true );
1052 + if ( focus_target && document.documentElement.contains( focus_target ) && 'function' === typeof focus_target.focus ) {
1053 + focus_target.focus();
1054 + }
1055 +
1056 + return true;
1057 + }
1058 + /**
1059 + * Return enabled values from the Service bulk editor.
1060 + *
1061 + * @return {Object<string,string>|null} Shared changes, or null when none enabled.
1062 + */
1063 + function collect_bulk_changes() {
1064 + var changes = {};
1065 +
1066 + $( '[data-wpbc-appointment-services-bulk-enable]:checked' ).each( function () {
1067 + var field_id = String( $( this ).data( 'wpbc-appointment-services-bulk-enable' ) || '' );
1068 + var $field = $( '[data-wpbc-appointment-services-bulk-value="' + field_id + '"]' );
1069 + if ( field_id && $field.length ) { changes[ field_id ] = $field.val(); }
1070 + } );
1071 +
1072 + return Object.keys( changes ).length ? changes : null;
1073 + }
1074 + /**
1075 + * Open a Service-owned operation inside the native right inspector.
1076 + *
1077 + * @param {string} mode Operation state identifier.
1078 + * @param {string} template_id Allow-listed WordPress template ID.
1079 + * @param {Object} template_data Presentation data for the template.
1080 + * @param {HTMLElement} focus_target Element that should regain focus on close.
1081 + * @return {boolean} True when the operation template was opened.
1082 + */
1083 + function open_operation( mode, template_id, template_data, focus_target ) {
1084 + var inspector_workflow = get_operation_inspector_workflow();
1085 + var template = catalogTemplate( template_id );
1086 + var $host = $( '[data-wpbc-appointment-services-operation-host]' );
1087 + var rendered_operation;
1088 + var target;
1089 +
1090 + if ( ! template || ! $host.length || ! inspector_workflow || ! inspector_workflow.mount() ) {
1091 + state.operation_mode = '';
1092 + state.operation_review = null;
1093 + updateControls();
1094 + notify( config.i18n.operation_failed || config.i18n.load_failed, 'error' );
1095 + return false;
1096 + }
1097 +
1098 + try {
1099 + rendered_operation = template( template_data );
1100 + } catch ( error ) {
1101 + state.operation_mode = '';
1102 + state.operation_review = null;
1103 + $host.empty().prop( 'hidden', true );
1104 + $( '[data-wpbc-appointment-services-native-inspector]' ).prop( 'hidden', false );
1105 + updateControls();
1106 + notify( config.i18n.operation_failed || config.i18n.load_failed, 'error' );
1107 + return false;
1108 + }
1109 +
1110 + state.operation_mode = mode;
1111 + state.inspector_focus_target = focus_target || document.activeElement;
1112 + target = inspector_workflow.get_form_target();
1113 + if ( ! target ) {
1114 + state.operation_mode = '';
1115 + state.operation_review = null;
1116 + inspector_workflow.set_state( 'error', config.i18n.operation_failed || config.i18n.load_failed );
1117 + updateControls();
1118 + notify( config.i18n.operation_failed || config.i18n.load_failed, 'error' );
1119 + return false;
1120 + }
1121 + target.innerHTML = rendered_operation;
1122 + inspector_workflow.set_state( 'form', '' );
1123 + $host.prop( 'hidden', false );
1124 + $( '[data-wpbc-appointment-services-native-inspector]' ).prop( 'hidden', true );
1125 + expand_service_inspector();
1126 + updateControls();
1127 + $host.find( '[data-wpbc-ui-catalog-delete-review-heading], [data-wpbc-ui-catalog-inline-review-heading], h2' ).first().attr( 'tabindex', '-1' ).trigger( 'focus' );
1128 +
1129 + return true;
1130 + }
1131 + /**
1132 + * Open bulk editing for the current persistent Service selection.
1133 + *
1134 + * @param {HTMLElement} focus_target Element that opened the bulk inspector.
1135 + * @return {void}
1136 + */
1137 + function open_bulk_edit( focus_target ) {
1138 + var selected_ids = get_selected_service_ids();
1139 +
1140 + if ( ! selected_ids.length || state.busy || state.inline_editing ) { return; }
1141 + if ( ! can_replace_editor() ) { return; }
1142 + if ( editorIsOpen() ) { reset_service_editor(); }
1143 + setBusy( true );
1144 + request( config.actions.bulk_contract, { ids: JSON.stringify( selected_ids ) } ).done( function ( response ) {
1145 + var contract = response && response.success && response.data ? response.data.contract : null;
1146 +
1147 + if ( ! contract || ! Array.isArray( contract.fields ) || ! contract.fields.length ) {
1148 + notify( messageFrom( response, config.i18n.bulk_contract_failed ), 'error' );
571 1149 return;
572 1150 }
573 - var has_active_sort = $column.find( '.wpbc_ui_listing__sort_link.is-active' ).length > 0;
574 - $column.attr( 'aria-sort', has_active_sort ? ( 'desc' === state.sort_order ? 'descending' : 'ascending' ) : 'none' );
1151 + open_operation( 'bulk_edit', 'wpbc-appointment-services-bulk-edit', {
1152 + title: config.i18n.bulk_edit_title,
1153 + description: contract.message || config.i18n.bulk_edit_description,
1154 + fields: contract.fields
1155 + }, focus_target );
1156 + } ).fail( function ( xhr ) {
1157 + notify( messageFrom( xhr.responseJSON, config.i18n.bulk_contract_failed ), 'error' );
1158 + } ).always( function () { setBusy( false ); } );
1159 + }
1160 + /**
1161 + * Render a signed inline or bulk review in the Service inspector.
1162 + *
1163 + * The preview endpoint is non-mutating. Only the returned signed plan can be
1164 + * submitted to the separate apply endpoint.
1165 + *
1166 + * @param {string} mode Either inline or bulk.
1167 + * @param {Array<number>} ids Service identifiers to review.
1168 + * @param {Object} changes Row-specific or shared field changes.
1169 + * @param {HTMLElement} focus_target Element that opened the review.
1170 + * @return {void}
1171 + */
1172 + function preview_operation( mode, ids, changes, focus_target ) {
1173 + var inspector_workflow;
1174 + var request_sequence;
1175 +
1176 + if ( state.busy || ! ids.length || ! changes ) { return; }
1177 + inspector_workflow = get_operation_inspector_workflow();
1178 + request_sequence = ++state.operation_request_sequence;
1179 + state.operation_mode = 'loading';
1180 + state.operation_review = null;
1181 + state.inspector_focus_target = focus_target || document.activeElement;
1182 + if ( ! inspector_workflow || ! inspector_workflow.open_loading() ) {
1183 + reset_operation( false );
1184 + notify( config.i18n.operation_failed || config.i18n.preview_failed, 'error' );
1185 + return;
1186 + }
1187 + setBusy( true );
1188 + request( config.actions.preview, {
1189 + mode: mode,
1190 + ids: JSON.stringify( ids ),
1191 + changes: JSON.stringify( changes )
1192 + }, false ).done( function ( response ) {
1193 + var review = response && response.success ? response.data : null;
1194 + var review_workflow = get_inline_review_workflow();
1195 + var review_rows;
1196 + var review_model;
1197 + var template_id = 'inline' === mode ? 'wpbc-appointment-services-inline-review' : 'wpbc-appointment-services-bulk-review';
1198 + if ( request_sequence !== state.operation_request_sequence ) { return; }
1199 + if ( ! review || ! review.plan || ! review.token ) {
1200 + var error_message = messageFrom( response, config.i18n.preview_failed );
1201 + inspector_workflow.set_state( 'error', error_message );
1202 + notify( error_message, 'error' );
1203 + return;
1204 + }
1205 + state.operation_review = review;
1206 + review_rows = review.review && Array.isArray( review.review.rows ) ? review.review.rows : [];
1207 + review_model = review_workflow ? review_workflow.prepare( review.review || {}, {
1208 + changed_label: String( config.i18n.changed_rows || '%s changed rows' ).replace( '%s', String( review_rows.length ) ),
1209 + description: config.i18n.review_confirmation || '',
1210 + form_id: 'wpbc_appointment_services_' + mode + '_review_form',
1211 + mode: mode + '_review',
1212 + pending_message: config.i18n.review_description || '',
1213 + title: 'inline' === mode ? config.i18n.inline_review_title : config.i18n.bulk_review_title
1214 + } ) : {};
1215 + if ( ! open_operation( mode + '_review', template_id, review_model, focus_target ) ) {
1216 + inspector_workflow.set_state( 'error', config.i18n.operation_failed || config.i18n.preview_failed );
1217 + return;
1218 + }
1219 + if ( review_workflow ) { review_workflow.synchronize( { busy: false, can_apply: true } ); }
1220 + } ).fail( function ( xhr ) {
1221 + var error_message = messageFrom( xhr.responseJSON, config.i18n.preview_failed );
1222 + if ( request_sequence !== state.operation_request_sequence ) { return; }
1223 + state.operation_mode = 'loading';
1224 + inspector_workflow.set_state( 'error', error_message );
1225 + notify( error_message, 'error' );
1226 + } ).always( function () {
1227 + if ( request_sequence === state.operation_request_sequence ) { setBusy( false ); }
575 1228 } );
576 1229 }
577 1230 /**
578 - * Synchronize direct-page and previous/next controls with the server response.
1231 + * Open a loading inspector immediately and request a signed deletion review.
579 1232 *
580 - * @param {Object} pagination Server-authoritative pagination metadata.
1233 + * @param {Array<number>} ids Selected Service identifiers.
1234 + * @param {HTMLElement} focus_target Control that opened the review.
581 1235 * @return {void}
582 1236 */
583 - function sync_pagination_controls( pagination ) {
584 - var total_pages = Math.max( 0, Number( pagination && pagination.total_pages ? pagination.total_pages : 0 ) );
585 - var maximum_page = Math.max( 1, total_pages );
586 - var $page_control = $( '[data-wpbc-listing-page-number-control="appointment_services_catalog"]' );
587 - $page_control.attr( 'max', maximum_page ).val( state.page ).prop( 'disabled', total_pages <= 1 );
588 - $( '[data-wpbc-listing-page-total="appointment_services_catalog"]' ).text( total_pages );
1237 + function preview_deletion( ids, focus_target ) {
1238 + var inspector_workflow;
1239 + var request_sequence;
1240 +
1241 + ids = Array.isArray( ids ) ? ids.map( Number ).filter( function ( service_id ) { return service_id > 0; } ) : [];
1242 + if ( state.busy || ! ids.length || state.inline_editing ) { return; }
1243 + if ( ! can_replace_editor() ) { return; }
1244 + if ( editorIsOpen() ) { reset_service_editor(); }
1245 + inspector_workflow = get_operation_inspector_workflow();
1246 + request_sequence = ++state.operation_request_sequence;
1247 + state.operation_mode = 'loading';
1248 + state.operation_review = null;
1249 + state.inspector_focus_target = focus_target || document.activeElement;
1250 + if ( ! inspector_workflow || ! inspector_workflow.open_loading() ) {
1251 + reset_operation( false );
1252 + notify( config.i18n.operation_failed || config.i18n.delete_preview_failed, 'error' );
1253 + return;
1254 + }
1255 + setBusy( true );
1256 + request( config.actions.delete_preview, { ids: JSON.stringify( ids ) }, false ).done( function ( response ) {
1257 + var review = response && response.success ? response.data : null;
1258 + var delete_review = review && review.delete_review ? review.delete_review : {};
1259 + var delete_i18n = delete_review.i18n || {};
1260 + var delete_workflow = get_delete_review_workflow();
1261 + var review_model;
1262 +
1263 + if ( request_sequence !== state.operation_request_sequence ) { return; }
1264 + if ( ! review || ! review.plan || ! review.token ) {
1265 + inspector_workflow.set_state( 'error', messageFrom( response, config.i18n.delete_preview_failed ) );
1266 + return;
1267 + }
1268 + review_model = {
1269 + acknowledgement: String( delete_i18n.acknowledgement || '' ),
1270 + actions_heading: String( delete_i18n.actions_heading || '' ),
1271 + can_apply: true === review.can_apply,
1272 + description: String( delete_i18n.description || '' ),
1273 + id_label: String( delete_i18n.id_label || config.i18n.column_id || 'ID' ),
1274 + items: Array.isArray( delete_review.items ) ? delete_review.items : [],
1275 + items_heading: String( delete_i18n.items_heading || '' ),
1276 + pending_message: String( delete_i18n.pending_message || '' ),
1277 + selection_label: String( delete_i18n.selection_label || '' ),
1278 + title: String( delete_i18n.title || '' ),
1279 + warning: String( delete_review.warning || review.warning || '' )
1280 + };
1281 + state.operation_review = review;
1282 + if ( ! open_operation( 'delete_review', 'wpbc-appointment-services-delete-review', review_model, focus_target ) ) {
1283 + inspector_workflow.set_state( 'error', config.i18n.delete_preview_failed );
1284 + return;
1285 + }
1286 + if ( delete_workflow ) {
1287 + delete_workflow.configure_footer( {
1288 + can_apply: true === review.can_apply,
1289 + footer: document.querySelector( '.wpbc_appointment_services__right_sidebar_footer' ),
1290 + form_id: 'wpbc_appointment_services_delete_review_form',
1291 + label: String( delete_i18n.delete_button || '' )
1292 + } );
1293 + delete_workflow.synchronize( { busy: false, can_apply: true === review.can_apply } );
1294 + if ( true === review.can_apply ) { delete_workflow.pulse_acknowledgement(); }
1295 + }
1296 + } ).fail( function ( xhr ) {
1297 + var error_message = messageFrom( xhr.responseJSON, config.i18n.delete_preview_failed );
1298 + if ( request_sequence !== state.operation_request_sequence ) { return; }
1299 + state.operation_mode = 'loading';
1300 + inspector_workflow.set_state( 'error', error_message );
1301 + notify( error_message, 'error' );
1302 + } ).always( function () {
1303 + if ( request_sequence === state.operation_request_sequence ) { setBusy( false ); }
1304 + } );
589 1305 }
590 1306 /**
591 - * Request the valid page entered in the shared direct-page control.
1307 + * Apply the current signed Service review, then refresh the active page.
592 1308 *
593 - * @param {jQuery} $page_control Direct-page number input.
1309 + * Selection remains owned by the shared controller and is therefore restored
1310 + * after the AJAX refresh instead of being silently cleared by the mutation.
1311 + *
594 1312 * @return {void}
595 1313 */
596 - function request_selected_page( $page_control ) {
597 - var requested_page = Number( $page_control.val() );
598 - requested_page = Math.min( Math.max( 1, isFinite( requested_page ) ? requested_page : state.page ), Math.max( 1, state.total_pages ) );
599 - $page_control.val( requested_page );
600 - if ( requested_page === state.page ) { return; }
601 - state.page = requested_page;
602 - loadList();
1314 + function apply_operation() {
1315 + var review = state.operation_review;
1316 + var changed_ids;
1317 + var is_delete = 'delete_review' === state.operation_mode;
1318 + var acknowledgement;
1319 +
1320 + if ( state.busy || ! review || ! review.plan || ! review.token ) { return; }
1321 + if ( is_delete ) {
1322 + acknowledgement = document.querySelector( '[data-wpbc-ui-catalog-delete-acknowledgement]' );
1323 + if ( true !== review.can_apply || ! acknowledgement || ! acknowledgement.checked ) {
1324 + if ( get_delete_review_workflow() ) { get_delete_review_workflow().pulse_acknowledgement(); }
1325 + return;
1326 + }
1327 + }
1328 + state.mutation_in_progress = true;
1329 + setBusy( true );
1330 + if ( is_delete && get_delete_review_workflow() ) { get_delete_review_workflow().synchronize( { busy: true, can_apply: true } ); }
1331 + else if ( get_inline_review_workflow() ) { get_inline_review_workflow().synchronize( { busy: true, can_apply: true } ); }
1332 + request( is_delete ? config.actions.delete_apply : config.actions.apply, { acknowledged: is_delete ? '1' : '', plan: JSON.stringify( review.plan ), token: review.token } ).done( function ( response ) {
1333 + if ( ! response || ! response.success ) {
1334 + notify( messageFrom( response, is_delete ? config.i18n.delete_apply_failed : config.i18n.apply_failed ), 'error' );
1335 + return;
1336 + }
1337 + changed_ids = response.data && Array.isArray( response.data.changed_ids ) ? response.data.changed_ids.map( String ) : [];
1338 + notify( response.data.message, 'success' );
1339 + state.mutation_in_progress = false;
1340 + close_service_inspector( false, true );
1341 + pending_highlight_ids = is_delete ? [] : changed_ids;
1342 + if ( is_delete && catalogController && 'function' === typeof catalogController.clear_selection ) { catalogController.clear_selection(); }
1343 + if ( catalogController ) { catalogController.load( { page_number: state.page } ); }
1344 + } ).fail( function ( xhr ) {
1345 + notify( messageFrom( xhr.responseJSON, is_delete ? config.i18n.delete_apply_failed : config.i18n.apply_failed ), 'error' );
1346 + } ).always( function () {
1347 + state.mutation_in_progress = false;
1348 + setBusy( false );
1349 + if ( is_delete && get_delete_review_workflow() ) { get_delete_review_workflow().synchronize( { busy: false, can_apply: !! state.operation_review && true === state.operation_review.can_apply } ); }
1350 + else if ( get_inline_review_workflow() ) { get_inline_review_workflow().synchronize( { busy: false, can_apply: !! state.operation_review } ); }
1351 + } );
603 1352 }
604 1353 /**
605 - * Render one server-selected page of Services in the management table.
1354 + * Highlight Services changed by the last reviewed mutation.
606 1355 *
607 - * @param {Array} services Services returned for the requested page only.
608 - * @param {Object} pagination Server-authoritative pagination metadata.
1356 + * The identifiers are retained until the refreshed catalog contains at least
1357 + * one affected Service. This avoids consuming the highlight while closing an
1358 + * operation re-renders the previous response.
1359 + *
609 1360 * @return {void}
610 1361 */
611 - function renderList( services, pagination ) {
612 - var $list = $( '.wpbc_appointment_services__list' );
613 - var $tbody = $( '.wpbc_appointment_services__table tbody' );
614 - var items_from;
615 - var items_to;
616 - var page_items;
617 - destroy_service_thumbnail_tooltips();
618 - $tbody.empty();
619 - pagination = pagination || {};
620 - state.services = services || [];
621 - state.page = Math.max( 1, Number( pagination.page_number || state.page || 1 ) );
622 - state.total_items = Math.max( 0, Number( pagination.total_items || 0 ) );
623 - state.total_pages = Math.max( 0, Number( pagination.total_pages || 0 ) );
624 - sync_pagination_controls( pagination );
625 - $( '.wpbc_appointment_services__empty' ).prop( 'hidden', true );
626 - if ( ! state.services.length ) { renderEmpty( '', false ); return; }
627 - items_from = Math.max( 1, Number( pagination.items_from || ( ( state.page - 1 ) * state.page_size ) + 1 ) );
628 - items_to = Math.max( items_from, Number( pagination.items_to || ( items_from + state.services.length - 1 ) ) );
629 - page_items = state.services;
630 - $list.prop( 'hidden', false );
1362 + function apply_pending_highlights() {
1363 + var first_service = null;
631 1364
632 - $.each( page_items, function ( index, service ) {
633 - var id = Number( service.service_id || 0 );
634 - var title = service.title || config.i18n.untitled || 'Untitled Service';
635 - var description = String( service.description || '#' + id );
636 - var status = String( service.status || 'active' );
637 - var $row = $( '<tr>', { 'class': 'wpbc_appointment_services__item', 'data-service-id': id, tabindex: '0', 'aria-current': id === state.selectedId ? 'true' : 'false' } );
638 - var $serviceCell = $( '<td>', { 'class': 'column-service', 'data-label': config.i18n.column_service || 'Service' } ).appendTo( $row );
639 - $row.toggleClass( 'is-selected', id === state.selectedId );
640 - var $serviceIdentity = $( '<div>', { 'class': 'wpbc_appointment_services__service_identity' } ).appendTo( $serviceCell );
641 - var $serviceCopy = $( '<span>', { 'class': 'wpbc_appointment_services__service_copy' } ).appendTo( $serviceIdentity );
642 - serviceThumbnailNode( service ).prependTo( $serviceIdentity );
643 - $( '<strong>', { text: title } ).appendTo( $serviceCopy );
644 - $( '<span>', { text: description } ).appendTo( $serviceCopy );
645 - $( '<td>', { 'class': 'column-duration', 'data-label': config.i18n.column_duration || 'Duration' } ).append( service_duration_node( service ) ).appendTo( $row );
646 - if ( config.pricing_available ) {
647 - $( '<td>', { 'class': 'column-price', 'data-label': config.i18n.column_price || 'Price', text: formatCost( service.base_cost ) } ).appendTo( $row );
1365 + pending_highlight_ids.forEach( function ( service_id ) {
1366 + var service = document.querySelector( '.wpbc_appointment_services__item[data-service-id="' + service_id + '"]' );
1367 +
1368 + if ( service ) {
1369 + service.classList.add( 'is-recently-saved' );
1370 + first_service = first_service || service;
648 1371 }
649 - $( '<td>', { 'class': 'column-providers', 'data-label': config.i18n.column_providers || 'Providers' } ).append( providerNodes( service ) ).appendTo( $row );
650 - var $availabilityCell = $( '<td>', { 'class': 'column-weekdays', 'data-label': config.i18n.column_weekly_availability || 'Weekly Availability' } ).appendTo( $row );
651 - var $availabilityWeek = $( '<div>', { 'class': 'wpbc_appointment_services__availability_week' } ).appendTo( $availabilityCell );
652 - var hasWeeklyAvailability = false;
653 - $.each( weekdayKeys, function ( dayIndex, day ) {
654 - var available_providers = providers_available_on( service, day );
655 - var available = available_providers.length > 0;
656 - if ( available ) { hasWeeklyAvailability = true; }
657 - var dayTitle = config.weekdays && config.weekdays[ dayIndex ] ? config.weekdays[ dayIndex ] : day;
658 - var provider_titles = $.map( available_providers, function ( provider ) { return provider.title || ''; } ).filter( function ( title ) { return !! title; } );
659 - var availability_title = available
660 - ? String( config.i18n.available_providers || 'Available Providers: %s' ).replace( '%s', provider_titles.join( ', ' ) )
661 - : ( config.i18n.no_available_providers || 'No assigned Providers are available' );
662 - $( '<span>', { 'class': 'wpbc_appointment_services__availability' + ( available ? ' is-available' : '' ), title: dayTitle + ': ' + availability_title, 'aria-label': dayTitle + ': ' + availability_title } ).appendTo( $availabilityWeek );
1372 + } );
1373 + if ( ! first_service ) {
1374 + return;
1375 + }
1376 + first_service.scrollIntoView( { block: 'nearest', behavior: 'smooth' } );
1377 + window.setTimeout( function () {
1378 + document.querySelectorAll( '.wpbc_appointment_services__item.is-recently-saved' ).forEach( function ( service ) {
1379 + service.classList.remove( 'is-recently-saved' );
663 1380 } );
664 - $availabilityCell.append( availability_edit_links( service ) );
665 - if ( service.resource_ids && service.resource_ids.length && ! hasWeeklyAvailability ) {
666 - $( '<span>', { 'class': 'wpbc_appointment_services__availability_empty', text: config.i18n.no_availability || 'No weekly availability' } ).appendTo( $availabilityCell );
1381 + }, 5000 );
1382 + pending_highlight_ids = [];
1383 + }
1384 + /** Return the current ordered and visible Service column definitions. */
1385 + function responseColumns( response ) {
1386 + var definitions = config.catalog && config.catalog.columns ? config.catalog.columns.definitions || {} : {};
1387 + var visible = response.display && response.display.visible_columns ? response.display.visible_columns : [];
1388 + var order = response.display && response.display.column_order ? response.display.column_order : [];
1389 + return $.map( order, function ( columnId ) {
1390 + var definition = definitions[ columnId ];
1391 + var is_sorted;
1392 + if ( ! definition || -1 === visible.indexOf( columnId ) ) { return null; }
1393 + is_sorted = !! definition.sort_key && definition.sort_key === response.sorting.sort_by;
1394 + return {
1395 + aria_sort: is_sorted ? ( 'desc' === response.sorting.sort_order ? 'descending' : 'ascending' ) : 'none',
1396 + id: columnId,
1397 + label: definition.label || columnId,
1398 + class_name: definition.class || 'column-' + columnId,
1399 + is_sorted: is_sorted,
1400 + sort_icon: is_sorted ? ( 'desc' === response.sorting.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export',
1401 + sort_key: definition.sort_key || ''
1402 + };
1403 + } );
1404 + }
1405 + /**
1406 + * Return sortable presentation records for the cards layout header.
1407 + *
1408 + * The table combines Status and Service ID in one column, so cards expand
1409 + * that column into two independent sort controls without changing the DTO.
1410 + *
1411 + * @param {Object} response Normalized shared catalog response.
1412 + * @return {Array<Object>} Visible, allow-listed cards sorting records.
1413 + */
1414 + function responseSortColumns( response ) {
1415 + var columns = [];
1416 + $.each( responseColumns( response ), function ( columnIndex, column ) {
1417 + if ( 'status' === column.id ) {
1418 + columns.push( {
1419 + is_sorted: 'status' === response.sorting.sort_by,
1420 + label: column.label,
1421 + sort_icon: 'status' === response.sorting.sort_by ? ( 'desc' === response.sorting.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export',
1422 + sort_key: 'status'
1423 + } );
1424 + columns.push( {
1425 + is_sorted: 'service_id' === response.sorting.sort_by,
1426 + label: config.i18n.column_id || 'ID',
1427 + sort_icon: 'service_id' === response.sorting.sort_by ? ( 'desc' === response.sorting.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export',
1428 + sort_key: 'service_id'
1429 + } );
1430 + return;
667 1431 }
668 - var $status_cell = $( '<td>', { 'class': 'column-status', 'data-label': config.i18n.column_status || 'Status' } ).appendTo( $row );
669 - var $status_identity = $( '<div>', { 'class': 'wpbc_appointment_services__status_identity' } ).appendTo( $status_cell );
670 - $( '<span>', { 'class': 'wpbc_appointment_services__status status-' + status, text: statusLabel( status ) } ).appendTo( $status_identity );
671 - $( '<span>', { 'class': 'wpbc_appointment_services__id', text: ( config.i18n.column_id || 'ID' ) + ': ' + id } ).appendTo( $status_identity );
672 - var $actions = $( '<td>', { 'class': 'column-actions', 'data-label': config.i18n.column_actions || 'Actions' } ).appendTo( $row );
673 - $( '<button>', { type: 'button', 'class': 'button-link wpbc_appointment_services__row_edit wpbc_icn_edit', 'data-service-id': id, title: config.i18n.edit || 'Edit Service', 'aria-label': config.i18n.edit || 'Edit Service' } ).appendTo( $actions );
674 - if ( 'archived' !== status ) { $( '<button>', { type: 'button', 'class': 'button-link wpbc_appointment_services__row_archive wpbc_icn_open_in_browser wpbc_icn_rotate_180 ', 'data-service-id': id, title: config.i18n.archive || 'Archive Service', 'aria-label': config.i18n.archive || 'Archive Service' } ).appendTo( $actions ); }
675 - $tbody.append( $row );
1432 + if ( ! column.sort_key ) { return; }
1433 + columns.push( {
1434 + is_sorted: column.sort_key === response.sorting.sort_by,
1435 + label: column.label,
1436 + sort_icon: column.sort_key === response.sorting.sort_by ? ( 'desc' === response.sorting.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export',
1437 + sort_key: column.sort_key
1438 + } );
676 1439 } );
1440 + return columns;
1441 + }
1442 + /**
1443 + * Build presentation cells for the Service-owned row and card templates.
1444 + *
1445 + * Inline drafts replace only the allow-listed editable controls. Provider,
1446 + * availability, status, and action presentation stays read-only.
1447 + *
1448 + * @param {Object} service Normalized Service DTO.
1449 + * @return {Object} Escaped HTML fragments keyed by catalog column ID.
1450 + */
1451 + function serviceCells( service ) {
1452 + var id = Number( service.service_id || service.id || 0 );
1453 + var draft = state.inline_editing ? state.inline_drafts[ String( id ) ] : null;
1454 + var row_schema = draft ? find_inline_schema( id ) : null;
1455 + var inline_field_template = catalogTemplate( 'wpbc-appointment-service-inline-field' );
1456 + var inline_cells = {};
1457 + var title = service.title || config.i18n.untitled || 'Untitled Service';
1458 + var description = String( service.description || '#' + id );
1459 + var status = String( service.status || 'active' );
1460 + var $identity = $( '<div>', { 'class': 'wpbc_appointment_services__service_identity' } );
1461 + var identity_fields_class = draft
1462 + ? 'wpbc_appointment_services__inline_identity_fields'
1463 + : 'wpbc_ui_listing__item_copy wpbc_appointment_services__service_copy';
1464 + var $copy = $( '<span>', { 'class': identity_fields_class } ).appendTo( $identity );
1465 + var $availability = $( '<div>' );
1466 + var $availabilityWeek = $( '<div>', { 'class': 'wpbc_appointment_services__availability_week' } ).appendTo( $availability );
1467 + var hasWeeklyAvailability = false;
1468 + var statusTemplate = catalogTemplate( 'wpbc-appointment-service-status-label' );
1469 + var providerTemplate = catalogTemplate( 'wpbc-appointment-service-provider-labels' );
1470 + var $actions = $( '<div>', { 'class': 'wpbc_appointment_services__actions' } );
1471 + if ( row_schema && inline_field_template ) {
1472 + $.each( row_schema.fields || [], function ( field_index, field ) {
1473 + var column_id = String( field.column || '' );
1474 + var field_key = String( field.key || '' );
1475 + var field_data;
1476 + if ( ! column_id || ! field_key || ! Object.prototype.hasOwnProperty.call( draft, field_key ) ) { return; }
1477 + field_data = $.extend( {}, field, { original_value: field.value, value: draft[ field_key ] } );
1478 + inline_cells[ column_id ] = ( inline_cells[ column_id ] || '' ) + inline_field_template( { field: field_data, service_id: id } );
1479 + } );
1480 + }
677 1481
1482 + serviceThumbnailNode( service ).prependTo( $identity );
1483 + if ( draft && inline_cells.service ) {
1484 + $copy.append( inline_cells.service );
1485 + } else {
1486 + $( '<strong>', { 'class': 'wpbc_ui_listing__item_title wpbc_ui_listing__overflow_tooltip', 'data-wpbc-ui-catalog-overflow-tooltip': title, title: title, text: title } ).appendTo( $copy );
1487 + $( '<span>', { 'class': 'wpbc_ui_listing__item_description wpbc_ui_listing__overflow_tooltip', 'data-wpbc-ui-catalog-overflow-tooltip': description, title: description, text: description } ).appendTo( $copy );
1488 + }
1489 + $.each( weekdayKeys, function ( dayIndex, day ) {
1490 + var availableProviders = providers_available_on( service, day );
1491 + var available = availableProviders.length > 0;
1492 + var dayTitle = config.weekdays && config.weekdays[ dayIndex ] ? config.weekdays[ dayIndex ] : day;
1493 + var providerTitles = $.map( availableProviders, function ( provider ) { return provider.title || ''; } ).filter( function ( providerTitle ) { return !! providerTitle; } );
1494 + var availabilityTitle = available
1495 + ? String( config.i18n.available_providers || 'Available Providers: %s' ).replace( '%s', providerTitles.join( ', ' ) )
1496 + : ( config.i18n.no_available_providers || 'No assigned Providers are available' );
1497 + if ( available ) { hasWeeklyAvailability = true; }
1498 + $( '<span>', { 'class': 'wpbc_appointment_services__availability' + ( available ? ' is-available' : '' ), title: dayTitle + ': ' + availabilityTitle, 'aria-label': dayTitle + ': ' + availabilityTitle } ).appendTo( $availabilityWeek );
1499 + } );
1500 + $availability.append( availability_edit_links( service ) );
1501 + if ( service.resource_ids && service.resource_ids.length && ! hasWeeklyAvailability ) {
1502 + $( '<span>', { 'class': 'wpbc_appointment_services__availability_empty', text: config.i18n.no_availability || 'No weekly availability' } ).appendTo( $availability );
1503 + }
1504 + $( '<button>', { type: 'button', 'class': 'button-link wpbc_appointment_services__row_edit wpbc_icn_edit', 'data-service-id': id, title: config.i18n.edit || 'Edit Service', 'aria-label': config.i18n.edit || 'Edit Service' } ).appendTo( $actions );
1505 + if ( service.actions && service.actions.archive ) {
1506 + $( '<button>', { type: 'button', 'class': 'button-link wpbc_appointment_services__row_archive wpbc_icn_open_in_browser wpbc_icn_rotate_180', 'data-service-id': id, title: config.i18n.archive || 'Archive Service', 'aria-label': config.i18n.archive || 'Archive Service' } ).appendTo( $actions );
1507 + }
1508 +
1509 + return {
1510 + service: nodeHtml( $identity ),
1511 + duration: draft && inline_cells.duration ? inline_cells.duration : nodeHtml( service_duration_node( service ) ),
1512 + price: draft && inline_cells.price ? inline_cells.price : nodeHtml( $( '<span>', { text: formatCost( service.base_cost ) } ) ),
1513 + providers: providerTemplate ? providerTemplate( { providers: service.providers || [], max_visible: 3, empty_label: config.i18n.no_provider || 'No Providers assigned', more_label: config.i18n.more_providers || 'more Providers' } ) : nodeHtml( providerNodes( service ) ),
1514 + availability: nodeHtml( $availability ),
1515 + status: ( statusTemplate ? statusTemplate( { status: status, label: statusLabel( status ) } ) : '' ) + nodeHtml( $( '<span>', { 'class': 'wpbc_appointment_services__id', text: ( config.i18n.column_id || 'ID' ) + ': ' + id } ) ),
1516 + actions: nodeHtml( $actions )
1517 + };
1518 + }
1519 + /**
1520 + * Render a normalized catalog response through Service-owned WP templates.
1521 + *
1522 + * @param {Object} response Normalized shared-catalog response.
1523 + * @return {void}
1524 + */
1525 + function renderCatalogResponse( response ) {
1526 + if ( ! response ) { return; }
1527 + var columns = responseColumns( response );
1528 + var rowTemplate = catalogTemplate( 'wpbc-appointment-service-row' );
1529 + var cardTemplate = catalogTemplate( 'wpbc-appointment-service-card' );
1530 + var headerTemplate = catalogTemplate( 'wpbc-appointment-services-header' );
1531 + var cardsHeaderTemplate = catalogTemplate( 'wpbc-appointment-services-cards-header' );
1532 + var paginationTemplate = catalogTemplate( 'wpbc-appointment-services-pagination' );
1533 + var isCards = 'cards' === response.display.template_pack;
1534 + var $rowHost = $( '[data-wpbc-appointment-services-rows]' );
1535 + var $cardHost = $( '[data-wpbc-appointment-services-cards]' );
1536 + var pagination = response.pagination || {};
1537 + var renderedItems = [];
1538 +
1539 + destroy_service_thumbnail_tooltips();
1540 + state.last_response = response;
1541 + state.services = response.items || [];
1542 + state.page = Number( pagination.page_number || 1 );
1543 + state.page_size = Number( pagination.items_per_page || state.page_size );
1544 + state.total_items = Number( pagination.total_items || 0 );
1545 + state.total_pages = Number( pagination.total_pages || 0 );
1546 + state.sort_by = String( response.sorting.sort_by || state.sort_by );
1547 + state.sort_order = String( response.sorting.sort_order || state.sort_order );
1548 +
1549 + if ( headerTemplate && $rowHost.length ) {
1550 + $( '[data-wpbc-appointment-services-header]' ).html( headerTemplate( { columns: columns, id_label: config.i18n.column_id || 'ID', sort_by: state.sort_by, sort_order: state.sort_order, select_all_label: config.i18n.select_all } ) );
1551 + }
1552 + if ( cardsHeaderTemplate && $cardHost.length ) {
1553 + $( '[data-wpbc-appointment-services-cards-header]' ).html( cardsHeaderTemplate( { columns: responseSortColumns( response ), i18n: config.catalog.i18n || {}, select_all_label: config.i18n.select_all } ) );
1554 + }
1555 + $.each( state.services, function ( index, service ) {
1556 + var template = isCards ? cardTemplate : rowTemplate;
1557 + if ( template ) {
1558 + var service_id = Number( service.service_id || service.id || 0 );
1559 + renderedItems.push( template( {
1560 + service_id: service_id,
1561 + is_inspector_selected: service_id === state.selectedId,
1562 + select_label: String( config.i18n.select_service || 'Select %s' ).replace( '%s', service.title || config.i18n.untitled ),
1563 + picture_url: String( service.picture_url || '' ),
1564 + columns: columns,
1565 + cells: serviceCells( service )
1566 + } ) );
1567 + }
1568 + } );
1569 + ( isCards ? $cardHost : $rowHost ).html( renderedItems.join( '' ) );
1570 + if ( paginationTemplate ) {
1571 + $( '[data-wpbc-appointment-services-pagination]' ).html( paginationTemplate( {
1572 + results_status: showingText( pagination.items_from || 0, pagination.items_to || 0, pagination.total_items || 0 ),
1573 + show_label: config.catalog.i18n.show_label,
1574 + per_page_label: config.catalog.i18n.per_page_label,
1575 + items_per_page_options: config.catalog.items_per_page.options || [],
1576 + items_per_page: state.page_size,
1577 + aria_label: config.catalog.i18n.pagination_label,
1578 + page_number_label: config.catalog.i18n.page_number,
1579 + page_number: state.page,
1580 + total_pages: Math.max( 1, state.total_pages ),
1581 + previous_page: Math.max( 1, state.page - 1 ),
1582 + next_page: Math.min( Math.max( 1, state.total_pages ), state.page + 1 ),
1583 + previous_label: config.catalog.i18n.previous_page,
1584 + next_label: config.catalog.i18n.next_page,
1585 + has_previous: state.page > 1,
1586 + has_next: state.total_pages > 0 && state.page < state.total_pages
1587 + } ) );
1588 + }
678 1589 refresh_service_thumbnail_tooltips();
679 - $( '.wpbc_appointment_services__list_footer' ).prop( 'hidden', false );
680 - $( '.wpbc_appointment_services__result_count' ).text( showingText( items_from, items_to, state.total_items ) );
681 - $( '.wpbc_appointment_services__page_prev' ).prop( 'disabled', state.page <= 1 );
682 - $( '.wpbc_appointment_services__page_next' ).prop( 'disabled', state.total_pages < 1 || state.page >= state.total_pages );
1590 + if ( catalogController ) { catalogController.refresh_controls(); catalogController.sync_table_min_width(); }
1591 + var selection = get_selection_controller();
1592 + if ( selection && 'function' === typeof selection.synchronize ) {
1593 + selection.synchronize();
1594 + }
1595 + apply_pending_highlights();
1596 + if ( state.inline_editing ) { render_inline_bar(); }
683 1597 }
684 1598 /** Load one Service and open it in the right inspector. */
685 - function loadOne( serviceId ) {
1599 + function loadOne( serviceId, focus_target ) {
1600 + var request_sequence;
1601 +
686 1602 if ( ! serviceId || state.busy ) { return; }
687 1603 if ( editorIsOpen() && state.selectedId === serviceId ) { expand_service_inspector(); focus_requested_service_section(); return; }
688 1604 if ( ! can_replace_editor() ) { return; }
1605 + state.inspector_focus_target = focus_target || document.activeElement;
1606 + request_sequence = ++state.editor_request_sequence;
689 1607 setBusy( true );
690 1608 request( config.actions.load, { service_id: serviceId } ).done( function ( response ) {
1609 + if ( request_sequence !== state.editor_request_sequence ) { return; }
691 1610 if ( response && response.success && response.data && response.data.service ) { fillEditor( response.data.service ); updateUrl( state.selectedId ); expand_service_inspector(); focus_requested_service_section(); return; }
692 1611 notify( messageFrom( response, config.i18n.load_failed ), 'error' );
693 - } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.load_failed ), 'error' ); } ).always( function () { setBusy( false ); } );
1612 + } ).fail( function ( xhr ) {
1613 + if ( request_sequence === state.editor_request_sequence ) { notify( messageFrom( xhr.responseJSON, config.i18n.load_failed ), 'error' ); }
1614 + } ).always( function () { setBusy( false ); } );
694 1615 }
695 - /** Reload Services and Provider presentation data for the active filters. */
696 - function loadList( save_items_per_page ) {
697 - var request_data = {
1616 + /**
1617 + * Reload Services and Provider presentation data for the active filters.
1618 + *
1619 + * @param {boolean} save_preferences Whether to persist the active Service filters.
1620 + * @return {void}
1621 + */
1622 + function loadList( save_preferences ) {
1623 + var requestData = {
698 1624 search: $( '#wpbc_service_search' ).val() || '',
699 1625 status: state.status,
700 1626 resource_id: $( '#wpbc_service_provider_filter' ).val() || 0,
701 - page_number: state.page,
702 - items_per_page: state.page_size,
703 - sort_by: state.sort_by,
704 - sort_order: state.sort_order
1627 + page_number: 1
705 1628 };
706 - if ( save_items_per_page ) { request_data.save_items_per_page = 1; }
707 - setLoading( true );
708 - request( config.actions.list, request_data ).done( function ( response ) {
709 - if ( ! response || ! response.success || ! response.data ) { renderEmpty( messageFrom( response, config.i18n.load_failed ), false ); return; }
710 - state.storageReady = !! response.data.storage_ready;
711 - if ( ! state.storageReady ) { $( '.wpbc_appointment_services__table tbody' ).empty(); setFieldsEnabled( false ); renderEmpty( response.data.message || config.i18n.not_connected, true ); return; }
712 - sync_listing_settings( response.data.listing || config.listing || {} );
713 - sync_sorting_controls( response.data.sorting || {} );
714 - indexProviders( response.data.providers || [] );
715 - updateSummary( response.data.counts, response.data.provider_count );
716 - renderList( response.data.services || [], response.data.pagination || {} );
717 - updateControls();
718 - if ( state.selectedId ) { loadOne( state.selectedId ); }
719 - } ).fail( function ( xhr ) {
720 - state.storageReady = false;
721 - setFieldsEnabled( false );
722 - renderEmpty( messageFrom( xhr.responseJSON, config.i18n.load_failed ), false );
723 - } ).always( function () { setLoading( false ); } );
1629 + if ( save_preferences ) { requestData.preference_action = 'save'; }
1630 + return catalogController ? catalogController.load( requestData ) : Promise.resolve( false );
724 1631 }
1632 + /**
1633 + * Keep the Service search clear control synchronized with the search value.
1634 + *
1635 + * The Service filters live outside the shared catalog mount, so this small
1636 + * adapter mirrors the shared catalog clear-button visibility contract.
1637 + *
1638 + * @return {void}
1639 + */
1640 + function sync_search_clear_button() {
1641 + var search_value = String( $( '#wpbc_service_search' ).val() || '' );
1642 +
1643 + $( '[data-wpbc-appointment-services-search-clear]' ).prop( 'hidden', ! search_value );
1644 + }
725 1645 /** Archive one Service after confirmation, then refresh the list. */
726 1646 function archiveService( serviceId ) {
727 1647 if ( ! serviceId || state.busy || ! w.confirm( config.i18n.confirm_archive || 'Archive this Service?' ) ) { return; }
1648 + state.mutation_in_progress = true;
728 1649 setBusy( true );
729 1650 request( config.actions.archive, { service_id: serviceId } ).done( function ( response ) {
730 1651 if ( response && response.success ) {
731 1652 if ( state.selectedId === serviceId ) { state.selectedId = 0; setFieldsEnabled( false ); updateUrl( 0 ); }
@@ -731,16 +1652,16 @@
731 1652 if ( state.selectedId === serviceId ) { state.selectedId = 0; setFieldsEnabled( false ); updateUrl( 0 ); }
732 1653 notify( response.data.message, 'success' ); loadList(); return;
733 1654 }
734 1655 notify( messageFrom( response, config.i18n.archive_failed ), 'error' );
735 - } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.archive_failed ), 'error' ); } ).always( function () { setBusy( false ); } );
1656 + } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.archive_failed ), 'error' ); } ).always( function () { state.mutation_in_progress = false; setBusy( false ); } );
736 1657 }
737 1658
738 1659 $( document ).on( 'click', '.wpbc_appointment_services__rightbar_tabs [role="tab"]', function ( event ) { event.preventDefault(); switchRightPanel( $( this ) ); } );
739 1660 $( document ).on( 'click', '.wpbc_appointment_services__rightbar .wpbc_ui__collapsible_group > .group__header', function ( event ) { event.preventDefault(); toggleInspectorGroup( $( this ) ); } );
740 - $( document ).on( 'click', '.wpbc_appointment_services__item', function ( event ) { if ( ! $( event.target ).closest( 'button, a' ).length ) { loadOne( Number( $( this ).data( 'service-id' ) || 0 ) ); } } );
741 - $( document ).on( 'keydown', '.wpbc_appointment_services__item', function ( event ) { if ( ! $( event.target ).closest( 'button, a' ).length && ( 'Enter' === event.key || ' ' === event.key ) ) { event.preventDefault(); loadOne( Number( $( this ).data( 'service-id' ) || 0 ) ); } } );
742 - $( document ).on( 'click', '.wpbc_appointment_services__row_edit', function () { loadOne( Number( $( this ).data( 'service-id' ) || 0 ) ); } );
1661 + $( document ).on( 'click', '.wpbc_appointment_services__item', function ( event ) { if ( ! state.inline_editing && ! $( event.target ).closest( 'button, a, input, select, textarea, label' ).length ) { loadOne( Number( $( this ).data( 'service-id' ) || 0 ), this ); } } );
1662 + $( document ).on( 'keydown', '.wpbc_appointment_services__item', function ( event ) { if ( ! state.inline_editing && ! $( event.target ).closest( 'button, a, input, select, textarea, label' ).length && ( 'Enter' === event.key || ' ' === event.key ) ) { event.preventDefault(); loadOne( Number( $( this ).data( 'service-id' ) || 0 ), this ); } } );
1663 + $( document ).on( 'click', '.wpbc_appointment_services__row_edit', function () { loadOne( Number( $( this ).data( 'service-id' ) || 0 ), this ); } );
743 1664 $( document ).on( 'click', '.wpbc_appointment_services__row_archive', function () { archiveService( Number( $( this ).data( 'service-id' ) || 0 ) ); } );
744 1665 $( document ).on( 'click', '.wpbc_appointment_services__status_filter', function () {
745 1666 state.status = String( $( this ).data( 'service-status' ) || 'all' );
746 1667 state.page = 1;
@@ -745,56 +1666,100 @@
745 1666 state.status = String( $( this ).data( 'service-status' ) || 'all' );
746 1667 state.page = 1;
747 1668 $( '.wpbc_appointment_services__status_filter' ).removeClass( 'is-active' ).attr( 'aria-pressed', 'false' );
748 1669 $( this ).addClass( 'is-active' ).attr( 'aria-pressed', 'true' );
749 - loadList();
750 - } );
751 - $( document ).on( 'click', '.wpbc_appointment_services__page_prev', function () { if ( state.page > 1 ) { state.page--; loadList(); } } );
752 - $( document ).on( 'click', '.wpbc_appointment_services__page_next', function () { if ( state.page < state.total_pages ) { state.page++; loadList(); } } );
753 - $( document ).on( 'click', '[data-wpbc-listing-sort="appointment_services_catalog"]', function ( event ) {
754 - var sort_by = String( $( this ).data( 'wpbc-listing-sort-key' ) || '' );
755 -
756 - event.preventDefault();
757 - if ( ! sort_by ) { return; }
758 - state.sort_order = sort_by === state.sort_by && 'asc' === state.sort_order ? 'desc' : 'asc';
759 - state.sort_by = sort_by;
760 - state.page = 1;
761 - loadList();
762 - } );
763 - $( document ).on( 'change', '[data-wpbc-listing-page-number-control="appointment_services_catalog"]', function () { request_selected_page( $( this ) ); } );
764 - $( document ).on( 'keydown', '[data-wpbc-listing-page-number-control="appointment_services_catalog"]', function ( event ) {
765 - if ( 'Enter' === event.key ) { event.preventDefault(); request_selected_page( $( this ) ); }
766 - } );
767 - $( document ).on( 'change', '[data-wpbc-listing-items-per-page-control="appointment_services_catalog"]', function () {
768 - var page_size = Number( $( this ).val() );
769 - if ( ! isFinite( page_size ) || page_size < 1 || page_size === state.page_size ) { return; }
770 - state.page_size = page_size;
771 - state.page = 1;
772 1670 loadList( true );
773 1671 } );
774 1672 $( document ).on( 'click', '.wpbc_appointment_services__add', function () {
775 1673 if ( ! state.storageReady || state.busy || ! can_replace_editor() ) { return; }
1674 + state.inspector_focus_target = this;
776 1675 fillEditor( blankService() );
777 1676 updateUrl( 0 );
778 1677 open_add_service_inspector();
779 1678 } );
1679 + $( document ).on( 'click', '[data-wpbc-appointment-services-cancel]', function ( event ) {
1680 + event.preventDefault();
1681 + event.stopPropagation();
1682 + close_service_inspector( true, true );
1683 + } );
780 1684 $( document ).on( 'click', '.wpbc_appointment_services__save', function () {
781 1685 if ( ! state.storageReady || state.busy ) { return; }
1686 + state.mutation_in_progress = true;
782 1687 setBusy( true );
783 1688 request( config.actions.save, { service: collectEditor() } ).done( function ( response ) {
784 1689 if ( response && response.success && response.data && response.data.service ) { fillEditor( response.data.service ); updateUrl( state.selectedId ); notify( response.data.message, 'success' ); loadList(); return; }
785 1690 notify( messageFrom( response, config.i18n.save_failed ), 'error' );
786 - } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.save_failed ), 'error' ); } ).always( function () { setBusy( false ); } );
1691 + } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.save_failed ), 'error' ); } ).always( function () { state.mutation_in_progress = false; setBusy( false ); } );
787 1692 } );
788 1693 $( document ).on( 'click', '.wpbc_appointment_services__duplicate', function () {
789 1694 if ( ! state.selectedId || state.busy ) { return; }
1695 + state.mutation_in_progress = true;
790 1696 setBusy( true );
791 1697 request( config.actions.duplicate, { service_id: state.selectedId } ).done( function ( response ) {
792 1698 if ( response && response.success && response.data && response.data.service ) { fillEditor( response.data.service ); updateUrl( state.selectedId ); notify( response.data.message, 'success' ); loadList(); return; }
793 1699 notify( messageFrom( response, config.i18n.duplicate_failed ), 'error' );
794 - } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.duplicate_failed ), 'error' ); } ).always( function () { setBusy( false ); } );
1700 + } ).fail( function ( xhr ) { notify( messageFrom( xhr.responseJSON, config.i18n.duplicate_failed ), 'error' ); } ).always( function () { state.mutation_in_progress = false; setBusy( false ); } );
795 1701 } );
796 1702 $( document ).on( 'click', '.wpbc_appointment_services__archive', function () { archiveService( state.selectedId ); } );
1703 + $( document ).on( 'click', '.wpbc_appointment_services__inline_toggle', function () { start_inline_editing(); } );
1704 + $( document ).on( 'click', '[data-wpbc-appointment-services-inline-bar] [data-wpbc-ui-catalog-inline-cancel]', function ( event ) {
1705 + event.preventDefault();
1706 + event.stopPropagation();
1707 + cancel_inline_editing( true );
1708 + } );
1709 + $( document ).on( 'click', '.wpbc_appointment_services__inline_review', function () {
1710 + var changes = collect_inline_changes();
1711 +
1712 + preview_operation( 'inline', $.map( Object.keys( changes ), function ( service_id ) { return Number( service_id ); } ), changes, this );
1713 + } );
1714 + $( document ).on( 'input change', '[data-wpbc-appointment-services-inline-field]', function () {
1715 + var service_id = String( Number( $( this ).data( 'service-id' ) || 0 ) );
1716 + var field_id = String( $( this ).data( 'wpbc-appointment-services-inline-field' ) || '' );
1717 + var row_element;
1718 + var indicator_element;
1719 + var changed;
1720 +
1721 + if ( state.inline_drafts[ service_id ] && field_id ) {
1722 + state.inline_drafts[ service_id ][ field_id ] = $( this ).val();
1723 + row_element = $( this ).closest( '.wpbc_appointment_services__item' ).get( 0 );
1724 + indicator_element = row_element ? row_element.querySelector( '.wpbc_appointment_services__inline_identity_fields' ) : null;
1725 + changed = inline_draft_changed( find_inline_schema( service_id ), state.inline_drafts[ service_id ] );
1726 + if ( inlineWorkflowController ) {
1727 + inlineWorkflowController.set_row_changed( row_element, changed, indicator_element, config.i18n.changed );
1728 + }
1729 + synchronize_inline_bar();
1730 + }
1731 + } );
1732 + $( document ).on( 'click', '.wpbc_appointment_services__bulk_edit', function () { open_bulk_edit( this ); } );
1733 + $( document ).on( 'click', '.wpbc_appointment_services__bulk_delete', function () { preview_deletion( get_selected_service_ids(), this ); } );
1734 + $( document ).on( 'change', '[data-wpbc-appointment-services-bulk-enable]', function () {
1735 + var field_id = String( $( this ).data( 'wpbc-appointment-services-bulk-enable' ) || '' );
1736 + $( '[data-wpbc-appointment-services-bulk-value="' + field_id + '"], [data-wpbc-appointment-services-bulk-range="' + field_id + '"]' ).prop( 'disabled', ! this.checked );
1737 + updateControls();
1738 + } );
1739 + $( document ).on( 'input change', '[data-wpbc-appointment-services-bulk-value]', function () {
1740 + var field_id = String( $( this ).data( 'wpbc-appointment-services-bulk-value' ) || '' );
1741 +
1742 + $( '[data-wpbc-appointment-services-bulk-range="' + field_id + '"]' ).val( $( this ).val() );
1743 + updateControls();
1744 + } );
1745 + $( document ).on( 'input change', '[data-wpbc-appointment-services-bulk-range]', function () {
1746 + var field_id = String( $( this ).data( 'wpbc-appointment-services-bulk-range' ) || '' );
1747 +
1748 + $( '[data-wpbc-appointment-services-bulk-value="' + field_id + '"]' ).val( $( this ).val() );
1749 + updateControls();
1750 + } );
1751 + $( document ).on( 'click', '.wpbc_appointment_services__operation_review', function () {
1752 + preview_operation( 'bulk', get_selected_service_ids(), collect_bulk_changes(), this );
1753 + } );
1754 + $( document ).on( 'click', '.wpbc_appointment_services__operation_apply', apply_operation );
1755 + $( document ).on( 'submit', '[data-wpbc-ui-catalog-inline-review-form], [data-wpbc-ui-catalog-delete-review-form]', function ( event ) {
1756 + event.preventDefault();
1757 + apply_operation();
1758 + } );
1759 + $( document ).on( 'change', '[data-wpbc-ui-catalog-delete-acknowledgement]', function ( event ) {
1760 + if ( get_delete_review_workflow() ) { get_delete_review_workflow().handle_change( event ); }
1761 + } );
797 1762 $( document ).on( 'input change', '[data-service-range-field]', function () {
798 1763 var field_id = String( $( this ).data( 'service-range-field' ) || '' );
799 1764 if ( field_id ) { $( '[data-service-field="' + field_id + '"]' ).val( $( this ).val() ).trigger( 'input' ); }
800 1765 } );
@@ -806,8 +1771,77 @@
806 1771 $( document ).on( 'click', '.wpbc_appointment_services__remove_image', function () {
807 1772 if ( $( this ).prop( 'disabled' ) ) { return; }
808 1773 $( '[data-service-field="picture_url"]' ).val( '' ).trigger( 'input' ).trigger( 'change' );
809 1774 } );
810 - $( document ).on( 'input', '#wpbc_service_search', function () { w.clearTimeout( searchTimer ); state.page = 1; searchTimer = w.setTimeout( loadList, 250 ); } );
811 - $( document ).on( 'change', '#wpbc_service_provider_filter', function () { state.page = 1; loadList(); } );
812 - $( function () { if ( $( '[data-wpbc-appointment-services-page="1"]' ).length ) { loadList(); } } );
1775 + $( document ).on( 'input', '#wpbc_service_search', function () {
1776 + w.clearTimeout( searchTimer );
1777 + state.page = 1;
1778 + sync_search_clear_button();
1779 + searchTimer = w.setTimeout( loadList, 250 );
1780 + } );
1781 + $( document ).on( 'click', '[data-wpbc-appointment-services-search-clear]', function ( event ) {
1782 + var $search_control = $( '#wpbc_service_search' );
1783 +
1784 + event.preventDefault();
1785 + w.clearTimeout( searchTimer );
1786 + $search_control.val( '' ).trigger( 'focus' );
1787 + state.page = 1;
1788 + sync_search_clear_button();
1789 + loadList();
1790 + } );
1791 + $( document ).on( 'change', '#wpbc_service_provider_filter', function () { state.page = 1; loadList( true ); } );
1792 + $( document ).on( 'wpbc:ui-catalog-loading', '#wpbc_appointment_services_catalog', function ( event ) {
1793 + var event_detail = event.originalEvent && event.originalEvent.detail ? event.originalEvent.detail : {};
1794 +
1795 + if ( event_detail.catalog_id !== 'appointment_services_catalog' ) { return; }
1796 + state.catalog_loading = true;
1797 + updateControls();
1798 + } );
1799 + $( document ).on( 'wpbc:ui-catalog-rendered', '#wpbc_appointment_services_catalog', function ( event ) {
1800 + var response = event.originalEvent && event.originalEvent.detail ? event.originalEvent.detail.response : null;
1801 + var filters = response && response.filters ? response.filters : {};
1802 + if ( ! response || response.catalog_id !== 'appointment_services_catalog' ) { return; }
1803 + state.catalog_loading = false;
1804 + state.storageReady = !! filters.storage_ready;
1805 + state.status = String( filters.status || state.status );
1806 + indexProviders( filters.providers || ( response.items && response.items.length ? response.items[0].providers || [] : [] ) );
1807 + updateSummary( filters.status_counts || {}, filters.provider_count || 0 );
1808 + $( '.wpbc_appointment_services__status_filter' ).removeClass( 'is-active' ).attr( 'aria-pressed', 'false' )
1809 + .filter( '[data-service-status="' + state.status + '"]' ).addClass( 'is-active' ).attr( 'aria-pressed', 'true' );
1810 + $( '#wpbc_service_provider_filter' ).val( String( filters.resource_id || 0 ) );
1811 + renderCatalogResponse( response );
1812 + updateControls();
1813 + if ( state.initial_selection_pending && state.selectedId ) {
1814 + state.initial_selection_pending = false;
1815 + loadOne( state.selectedId );
1816 + }
1817 + } );
1818 + $( function () {
1819 + var mount_element = document.getElementById( 'wpbc_appointment_services_catalog' );
1820 + var page_element = document.querySelector( '.wpbc_appointment_services_page' );
1821 + var protected_events_root = page_element || mount_element;
1822 +
1823 + if ( ! $( '[data-wpbc-appointment-services-page="1"]' ).length || ! mount_element || ! config.catalog || ! w.wpbc_ui_catalog ) { return; }
1824 + state.status = String( config.catalog.initial_request && config.catalog.initial_request.status ? config.catalog.initial_request.status : 'all' );
1825 + $( '#wpbc_service_search' ).val( String( config.catalog.initial_request && config.catalog.initial_request.search ? config.catalog.initial_request.search : '' ) );
1826 + sync_search_clear_button();
1827 + protected_events_root.addEventListener( 'click', protect_inline_drafts_from_catalog_controls, true );
1828 + protected_events_root.addEventListener( 'change', protect_inline_drafts_from_catalog_controls, true );
1829 + protected_events_root.addEventListener( 'input', protect_inline_drafts_from_catalog_controls, true );
1830 + catalogController = w.wpbc_ui_catalog.mount( config.catalog );
1831 + if ( ! catalogController ) {
1832 + notify( config.i18n.load_failed, 'error' );
1833 + } else if ( 'function' === typeof w.wpbc_ui_catalog.create_inline_editing_workflow ) {
1834 + inlineWorkflowController = w.wpbc_ui_catalog.create_inline_editing_workflow( mount_element, {
1835 + controls_root: page_element || mount_element,
1836 + page_element: page_element || mount_element,
1837 + protected_selector: '.wpbc_appointment_services__status_filter, #wpbc_service_provider_filter, .wpbc_appointment_services__add'
1838 + } );
1839 + synchronize_inline_workflow();
1840 + }
1841 + $( '.wpbc_settings_page_wrapper' ).on( 'wpbc:right-sidebar-before-content-collapse.wpbcAppointmentServices', function ( event ) {
1842 + if ( ( editorIsOpen() || state.operation_mode ) && ! close_service_inspector( true, false ) ) {
1843 + event.preventDefault();
1844 + }
1845 + } );
1846 + } );
813 1847 } )( window, jQuery );