| 1 |
( function ( w, $ ) { |
| 2 |
'use strict'; |
| 3 |
var config = w.wpbc_appointment_services_config || {}; |
| 4 |
var state = { |
| 5 |
storageReady: false, |
| 6 |
selectedId: Number( config.selected_id || 0 ), |
| 7 |
requested_focus: String( config.focus_section || '' ), |
| 8 |
focus_handled: false, |
| 9 |
busy: false, |
| 10 |
status: 'all', |
| 11 |
services: [], |
| 12 |
providers: {}, |
| 13 |
providerCount: 0, |
| 14 |
editor_snapshot: '', |
| 15 |
page: 1, |
| 16 |
page_size: Number( config.listing && config.listing.items_per_page ? config.listing.items_per_page : 10 ), |
| 17 |
total_items: 0, |
| 18 |
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' ) |
| 21 |
}; |
| 22 |
var searchTimer = 0; |
| 23 |
var weekdayKeys = [ 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' ]; |
| 24 |
|
| 25 |
/** Extract an API message while preserving a caller-provided fallback. */ |
| 26 |
function messageFrom( response, fallback ) { return response && response.data && response.data.message ? response.data.message : fallback; } |
| 27 |
/** Display a shared Booking Calendar administrator notice. */ |
| 28 |
function notify( message, type ) { |
| 29 |
if ( message && typeof w.wpbc_admin_show_message === 'function' ) { w.wpbc_admin_show_message( message, type || 'info', 5000, false ); } |
| 30 |
} |
| 31 |
/** |
| 32 |
* Show the native Booking Calendar Processing notice. |
| 33 |
* |
| 34 |
* The returned element identifies this specific request's notice so |
| 35 |
* overlapping requests cannot dismiss each other's feedback. |
| 36 |
* |
| 37 |
* @return {jQuery} Processing notice wrapper, or an empty collection. |
| 38 |
*/ |
| 39 |
function show_processing_notice() { |
| 40 |
if ( 'function' !== typeof w.wpbc_admin_show_message_processing ) { |
| 41 |
return $(); |
| 42 |
} |
| 43 |
|
| 44 |
w.wpbc_admin_show_message_processing( '' ); |
| 45 |
|
| 46 |
return $( '#ajax_working .wpbc_processing.wpbc_spin' ).last().closest( '[id^="wpbc_notice_"]' ); |
| 47 |
} |
| 48 |
/** |
| 49 |
* Hide the Processing notice created for one completed request. |
| 50 |
* |
| 51 |
* @param {jQuery} $processing_notice Notice wrapper returned by show_processing_notice(). |
| 52 |
* @return {void} |
| 53 |
*/ |
| 54 |
function hide_processing_notice( $processing_notice ) { |
| 55 |
if ( $processing_notice && $processing_notice.length ) { |
| 56 |
$processing_notice.stop( true, true ).hide(); |
| 57 |
} |
| 58 |
} |
| 59 |
/** |
| 60 |
* Send an authenticated Appointment Services AJAX request. |
| 61 |
* |
| 62 |
* Every request uses the shared administrator Processing notice and removes |
| 63 |
* only its own notice after the request settles. |
| 64 |
* |
| 65 |
* @param {string} action WordPress AJAX action name. |
| 66 |
* @param {Object} data Request-specific payload. |
| 67 |
* @return {jqXHR} jQuery AJAX promise for the request. |
| 68 |
*/ |
| 69 |
function request( action, data ) { |
| 70 |
var $processing_notice = show_processing_notice(); |
| 71 |
|
| 72 |
return $.ajax( { url: config.ajax_url, type: 'POST', dataType: 'json', data: $.extend( { action: action, nonce: config.nonce }, data || {} ) } ) |
| 73 |
.always( function () { hide_processing_notice( $processing_notice ); } ); |
| 74 |
} |
| 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 |
/** Activate the Settings or Help panel selected in the right sidebar. */ |
| 81 |
function switchRightPanel( $tab ) { |
| 82 |
var panelId = $tab.attr( 'aria-controls' ); |
| 83 |
var panel = panelId ? document.getElementById( panelId ) : null; |
| 84 |
var $tabs = $tab.closest( '.wpbc_appointment_services__rightbar_tabs' ).find( '[role="tab"]' ); |
| 85 |
var $panels = $( '.wpbc_appointment_services__rightbar [role="tabpanel"]' ); |
| 86 |
if ( ! panel ) { return; } |
| 87 |
$tabs.attr( 'aria-selected', 'false' ); |
| 88 |
$tab.attr( 'aria-selected', 'true' ); |
| 89 |
$panels.prop( 'hidden', true ).attr( 'aria-hidden', 'true' ); |
| 90 |
$( panel ).prop( 'hidden', false ).attr( 'aria-hidden', 'false' ); |
| 91 |
updateControls(); |
| 92 |
} |
| 93 |
/** |
| 94 |
* Tell the active Setup Wizard bar that the page workspace changed width. |
| 95 |
* |
| 96 |
* The delayed notification runs after the shared sidebar transition so the |
| 97 |
* setup bar can measure the final inspector boundary. |
| 98 |
* |
| 99 |
* @return {void} |
| 100 |
*/ |
| 101 |
function notify_setup_wizard_layout_changed() { |
| 102 |
$( document ).trigger( 'wpbc_setup_wizard_layout_changed' ); |
| 103 |
w.setTimeout( function () { $( document ).trigger( 'wpbc_setup_wizard_layout_changed' ); }, 300 ); |
| 104 |
} |
| 105 |
/** |
| 106 |
* Expand the right sidebar and display the Service Settings inspector. |
| 107 |
* |
| 108 |
* @return {void} |
| 109 |
*/ |
| 110 |
function expand_service_inspector() { |
| 111 |
var $settings_tab = $( '#wpbc_tab_service_settings' ); |
| 112 |
|
| 113 |
if ( 'function' === typeof w.wpbc_admin_ui__sidebar_right__do_max ) { |
| 114 |
w.wpbc_admin_ui__sidebar_right__do_max(); |
| 115 |
} |
| 116 |
if ( $settings_tab.length ) { |
| 117 |
switchRightPanel( $settings_tab ); |
| 118 |
} |
| 119 |
notify_setup_wizard_layout_changed(); |
| 120 |
} |
| 121 |
/** |
| 122 |
* Reveal, expand, highlight, and optionally focus one Service editor group. |
| 123 |
* |
| 124 |
* @param {string} fields_selector Group fields selector from the fixed editor markup. |
| 125 |
* @param {string} focus_selector Optional control selector to focus after scrolling. |
| 126 |
* @return {boolean} True when the requested inspector group was found. |
| 127 |
*/ |
| 128 |
function open_service_inspector_group( fields_selector, focus_selector ) { |
| 129 |
var $group_fields = $( fields_selector ); |
| 130 |
var $group = $group_fields.closest( '.wpbc_ui__collapsible_group' ); |
| 131 |
var $group_header = $group.children( '.group__header' ); |
| 132 |
var group_element = $group.get( 0 ); |
| 133 |
var focus_element = focus_selector ? document.querySelector( focus_selector ) : null; |
| 134 |
|
| 135 |
expand_service_inspector(); |
| 136 |
if ( ! $group.length ) { |
| 137 |
return false; |
| 138 |
} |
| 139 |
|
| 140 |
$group.addClass( 'is-open' ); |
| 141 |
$group_header.attr( 'aria-expanded', 'true' ); |
| 142 |
$group_fields.prop( 'hidden', false ).attr( 'aria-hidden', 'false' ); |
| 143 |
$group.removeClass( 'wpbc_appointment_services__focus_pulse' ); |
| 144 |
if ( group_element ) { |
| 145 |
void group_element.offsetWidth; |
| 146 |
$group.addClass( 'wpbc_appointment_services__focus_pulse' ); |
| 147 |
try { group_element.scrollIntoView( { behavior: 'smooth', block: 'start', inline: 'nearest' } ); } |
| 148 |
catch ( error ) { group_element.scrollIntoView( true ); } |
| 149 |
w.setTimeout( function () { $group.removeClass( 'wpbc_appointment_services__focus_pulse' ); }, 900 ); |
| 150 |
} |
| 151 |
if ( focus_element && typeof focus_element.focus === 'function' ) { |
| 152 |
try { focus_element.focus( { preventScroll: true } ); } |
| 153 |
catch ( error ) { focus_element.focus(); } |
| 154 |
} |
| 155 |
|
| 156 |
return true; |
| 157 |
} |
| 158 |
/** |
| 159 |
* Open, reveal, and highlight the General Service editor section. |
| 160 |
* |
| 161 |
* @return {void} |
| 162 |
*/ |
| 163 |
function open_add_service_inspector() { |
| 164 |
open_service_inspector_group( '#wpbc_service_general', '[data-service-field="title"]' ); |
| 165 |
} |
| 166 |
/** |
| 167 |
* Apply the one-time inspector focus requested by an administration link. |
| 168 |
* |
| 169 |
* @return {void} |
| 170 |
*/ |
| 171 |
function focus_requested_service_section() { |
| 172 |
if ( state.focus_handled || 'booking_form' !== state.requested_focus ) { |
| 173 |
return; |
| 174 |
} |
| 175 |
|
| 176 |
if ( open_service_inspector_group( '#wpbc_service_form', '[data-service-field="booking_form_id"]' ) ) { |
| 177 |
state.focus_handled = true; |
| 178 |
if ( w.history && w.URL ) { |
| 179 |
var url = new w.URL( w.location.href ); |
| 180 |
url.searchParams.delete( 'wpbc_service_focus' ); |
| 181 |
w.history.replaceState( {}, '', url.toString() ); |
| 182 |
} |
| 183 |
} |
| 184 |
} |
| 185 |
/** Expand or collapse one inspector field group. */ |
| 186 |
function toggleInspectorGroup( $button ) { |
| 187 |
var $group = $button.closest( '.wpbc_ui__collapsible_group' ); |
| 188 |
var $fields = $group.find( '> .group__fields' ); |
| 189 |
var isOpen = $group.hasClass( 'is-open' ); |
| 190 |
$group.toggleClass( 'is-open', ! isOpen ); |
| 191 |
$button.attr( 'aria-expanded', isOpen ? 'false' : 'true' ); |
| 192 |
$fields.prop( 'hidden', isOpen ).attr( 'aria-hidden', isOpen ? 'true' : 'false' ); |
| 193 |
} |
| 194 |
/** Determine whether a Service is currently loaded into the editor. */ |
| 195 |
function editorIsOpen() { return ! $( '[data-service-field="title"]' ).prop( 'disabled' ); } |
| 196 |
/** |
| 197 |
* Synchronize one numeric Service field with its range control. |
| 198 |
* |
| 199 |
* The slider expands to represent an existing value above its normal visual |
| 200 |
* range without clamping or rewriting that stored value. |
| 201 |
* |
| 202 |
* @param {string} field_id Service field identifier. |
| 203 |
* @return {void} |
| 204 |
*/ |
| 205 |
function sync_numeric_range( field_id ) { |
| 206 |
var $field = $( '[data-service-field="' + field_id + '"]' ); |
| 207 |
var $range = $( '[data-service-range-field="' + field_id + '"]' ); |
| 208 |
var value = Number( $field.val() ); |
| 209 |
var default_min = Number( $range.data( 'service-range-default-min' ) ); |
| 210 |
var default_max = Number( $range.data( 'service-range-default-max' ) ); |
| 211 |
var step = Number( $range.attr( 'step' ) || 1 ); |
| 212 |
var range_max; |
| 213 |
|
| 214 |
if ( ! $field.length || ! $range.length || ! isFinite( value ) ) { return; } |
| 215 |
default_min = isFinite( default_min ) ? default_min : 0; |
| 216 |
default_max = isFinite( default_max ) ? default_max : 100; |
| 217 |
step = isFinite( step ) && step > 0 ? step : 1; |
| 218 |
range_max = value > default_max ? default_min + ( Math.ceil( ( value - default_min ) / step ) * step ) : default_max; |
| 219 |
$range.attr( { min: default_min, max: range_max } ).val( value ); |
| 220 |
} |
| 221 |
/** |
| 222 |
* Synchronize every numeric Service field with its range control. |
| 223 |
* |
| 224 |
* @return {void} |
| 225 |
*/ |
| 226 |
function sync_all_numeric_ranges() { |
| 227 |
$( '[data-service-range-field]' ).each( function () { sync_numeric_range( String( $( this ).data( 'service-range-field' ) || '' ) ); } ); |
| 228 |
} |
| 229 |
/** |
| 230 |
* Synchronize the visible Status radios with the stored Service status field. |
| 231 |
* |
| 232 |
* @return {void} |
| 233 |
*/ |
| 234 |
function sync_status_radios() { |
| 235 |
var status = String( $( '[data-service-field="status"]' ).val() || 'active' ); |
| 236 |
$( '[data-service-status-choice]' ).each( function () { |
| 237 |
$( this ).prop( 'checked', String( $( this ).val() ) === status ); |
| 238 |
} ); |
| 239 |
} |
| 240 |
/** |
| 241 |
* Synchronize the Service image preview with the readonly URL field. |
| 242 |
* |
| 243 |
* @return {void} |
| 244 |
*/ |
| 245 |
function updateMediaPreview() { |
| 246 |
var pictureUrl = String( $( '[data-service-field="picture_url"]' ).val() || '' ).trim(); |
| 247 |
var $image = $( '.wpbc_appointment_services__media_image' ); |
| 248 |
if ( pictureUrl ) { $image.attr( 'src', pictureUrl ); } else { $image.removeAttr( 'src' ); } |
| 249 |
$image.prop( 'hidden', ! pictureUrl ); |
| 250 |
$( '.wpbc_appointment_services__media_placeholder' ).prop( 'hidden', !! pictureUrl ); |
| 251 |
} |
| 252 |
/** Synchronize toolbar and inspector action states with the page state. */ |
| 253 |
function updateControls() { |
| 254 |
var open = state.storageReady && editorIsOpen(); |
| 255 |
var hasPicture = !! String( $( '[data-service-field="picture_url"]' ).val() || '' ).trim(); |
| 256 |
var show_save = open && 'true' === $( '#wpbc_tab_service_settings' ).attr( 'aria-selected' ); |
| 257 |
$( '.wpbc_appointment_services__add' ).prop( 'hidden', false ).prop( 'disabled', ! state.storageReady || state.busy ); |
| 258 |
$( '.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 ); |
| 260 |
$( '.wpbc_appointment_services__duplicate, .wpbc_appointment_services__archive' ).prop( 'disabled', ! open || ! state.selectedId || state.busy ); |
| 261 |
$( '.wpbc_appointment_services__media_preview, .wpbc_appointment_services__select_image' ).prop( 'disabled', ! open || state.busy ); |
| 262 |
$( '.wpbc_appointment_services__remove_image' ).prop( 'disabled', ! open || ! hasPicture || state.busy ); |
| 263 |
} |
| 264 |
/** Mark the page busy during a mutating request and refresh controls. */ |
| 265 |
function setBusy( value ) { state.busy = value; $( '.wpbc_appointment_services_page' ).toggleClass( 'is-busy', value ); updateControls(); } |
| 266 |
/** Enable or disable all fields in the Service inspector. */ |
| 267 |
function setFieldsEnabled( enabled ) { $( '[data-service-field], [data-service-range-field], [data-service-status-choice]' ).prop( 'disabled', ! enabled ); updateControls(); } |
| 268 |
/** Return defaults for a new unsaved Service. */ |
| 269 |
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: [] }; |
| 271 |
} |
| 272 |
/** Populate the inspector from a normalized Service response. */ |
| 273 |
function fillEditor( service ) { |
| 274 |
service = $.extend( blankService(), service || {} ); |
| 275 |
state.selectedId = Number( service.service_id || 0 ); |
| 276 |
$.each( service, function ( key, value ) { $( '[data-service-field="' + key + '"]' ).val( value ); } ); |
| 277 |
sync_status_radios(); |
| 278 |
sync_all_numeric_ranges(); |
| 279 |
updateMediaPreview(); |
| 280 |
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' ); |
| 283 |
capture_editor_snapshot(); |
| 284 |
} |
| 285 |
/** Collect the current Service inspector values for saving. */ |
| 286 |
function collectEditor() { |
| 287 |
var service = { service_id: state.selectedId }; |
| 288 |
$( '[data-service-field]' ).each( function () { service[ $( this ).data( 'service-field' ) ] = $( this ).val(); } ); |
| 289 |
return service; |
| 290 |
} |
| 291 |
/** |
| 292 |
* Store the current editor values as the last loaded or saved state. |
| 293 |
* |
| 294 |
* @return {void} |
| 295 |
*/ |
| 296 |
function capture_editor_snapshot() { |
| 297 |
state.editor_snapshot = JSON.stringify( collectEditor() ); |
| 298 |
} |
| 299 |
/** |
| 300 |
* Determine whether the open Service editor contains unsaved changes. |
| 301 |
* |
| 302 |
* @return {boolean} True when current fields differ from the captured state. |
| 303 |
*/ |
| 304 |
function is_editor_dirty() { |
| 305 |
return editorIsOpen() && state.editor_snapshot !== JSON.stringify( collectEditor() ); |
| 306 |
} |
| 307 |
/** |
| 308 |
* Confirm before replacing an editor that contains unsaved Service changes. |
| 309 |
* |
| 310 |
* @return {boolean} True when replacing the current editor may continue. |
| 311 |
*/ |
| 312 |
function can_replace_editor() { |
| 313 |
return ! is_editor_dirty() || w.confirm( config.i18n.confirm_discard || 'Discard unsaved Service changes?' ); |
| 314 |
} |
| 315 |
/** Reflect the selected Service in the admin URL without reloading. */ |
| 316 |
function updateUrl( serviceId ) { |
| 317 |
if ( ! w.history || ! w.URL ) { return; } |
| 318 |
var url = new w.URL( w.location.href ); |
| 319 |
if ( serviceId ) { url.searchParams.set( 'service_id', serviceId ); } else { url.searchParams.delete( 'service_id' ); } |
| 320 |
w.history.replaceState( {}, '', url.toString() ); |
| 321 |
} |
| 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 |
/** Index Provider presentation records by booking resource ID. */ |
| 332 |
function indexProviders( providers ) { |
| 333 |
state.providers = {}; |
| 334 |
$.each( providers || [], function ( index, provider ) { state.providers[ String( provider.id ) ] = provider; } ); |
| 335 |
} |
| 336 |
/** Update status and Provider counters above the Service table. */ |
| 337 |
function updateSummary( counts, providerCount ) { |
| 338 |
counts = $.extend( { all: 0, active: 0, inactive: 0, archived: 0 }, counts || {} ); |
| 339 |
state.providerCount = Number( providerCount || 0 ); |
| 340 |
$.each( counts, function ( status, count ) { $( '[data-service-count="' + status + '"]' ).text( Number( count || 0 ) ); } ); |
| 341 |
$( '[data-provider-count]' ).text( state.providerCount ); |
| 342 |
$( '.wpbc_appointment_services__provider_notice' ).prop( 'hidden', 0 !== state.providerCount ); |
| 343 |
} |
| 344 |
/** Format a normalized Service cost using the configured currency symbol. */ |
| 345 |
function formatCost( cost ) { |
| 346 |
var amount = Number( cost || 0 ); |
| 347 |
var symbol = config.currency_symbol || '$'; |
| 348 |
return symbol + amount.toFixed( 2 ); |
| 349 |
} |
| 350 |
/** Build compact Provider avatar nodes for one Service row. */ |
| 351 |
function providerNodes( service ) { |
| 352 |
var ids = $.map( service.resource_ids || [], function ( value ) { return Number( value || 0 ); } ); |
| 353 |
var $stack = $( '<div>', { 'class': 'wpbc_appointment_services__provider_stack' } ); |
| 354 |
if ( ! ids.length ) { return $( '<span>', { 'class': 'wpbc_appointment_services__no_provider', text: config.i18n.no_provider || 'No Providers assigned' } ); } |
| 355 |
$.each( ids.slice( 0, 3 ), function ( index, id ) { |
| 356 |
var provider = state.providers[ String( id ) ] || { id: id, title: 'Provider #' + id, initials: 'P', avatar_url: '' }; |
| 357 |
var has_availability = false !== provider.has_weekly_availability; |
| 358 |
var provider_title = provider.title || 'Provider #' + id; |
| 359 |
var avatar_title = provider_title; |
| 360 |
var avatar_attributes; |
| 361 |
var $avatar; |
| 362 |
|
| 363 |
if ( ! has_availability ) { avatar_title += ' — ' + ( config.i18n.no_availability || 'No weekly availability' ); } |
| 364 |
avatar_attributes = { |
| 365 |
'class': 'wpbc_appointment_services__provider_avatar' + ( has_availability ? '' : ' has-no-availability' ), |
| 366 |
title: avatar_title |
| 367 |
}; |
| 368 |
if ( provider.availability_url ) { |
| 369 |
avatar_title = String( config.i18n.edit_availability || 'Edit availability for %s' ).replace( '%s', provider_title ); |
| 370 |
avatar_attributes.href = provider.availability_url; |
| 371 |
avatar_attributes.title = avatar_title; |
| 372 |
avatar_attributes[ 'aria-label' ] = avatar_title; |
| 373 |
$avatar = $( '<a>', avatar_attributes ); |
| 374 |
} else { |
| 375 |
$avatar = $( '<span>', avatar_attributes ); |
| 376 |
} |
| 377 |
if ( provider.avatar_url ) { $( '<img>', { src: provider.avatar_url, alt: '', loading: 'lazy' } ).appendTo( $avatar ); } |
| 378 |
else { $avatar.text( provider.initials || 'P' ); } |
| 379 |
$stack.append( $avatar ); |
| 380 |
} ); |
| 381 |
if ( ids.length > 3 ) { $( '<span>', { 'class': 'wpbc_appointment_services__provider_more', text: '+' + ( ids.length - 3 ), title: ( ids.length - 3 ) + ' ' + ( config.i18n.more_providers || 'more Providers' ) } ).appendTo( $stack ); } |
| 382 |
return $stack; |
| 383 |
} |
| 384 |
/** |
| 385 |
* Build the Service thumbnail used in the management table. |
| 386 |
* |
| 387 |
* @param {Object} service Normalized Service response. |
| 388 |
* @return {jQuery} Thumbnail wrapper containing an image or placeholder icon. |
| 389 |
*/ |
| 390 |
function serviceThumbnailNode( service ) { |
| 391 |
var pictureUrl = String( service.picture_url || '' ).trim(); |
| 392 |
var service_title = String( service.title || config.i18n.untitled || 'Untitled Service' ); |
| 393 |
var service_description = String( service.description || '' ).trim() || config.i18n.no_description || 'No description'; |
| 394 |
var tooltip_format = String( config.i18n.service_thumbnail_tooltip || 'Title: %1$s\nDescription: %2$s' ); |
| 395 |
var tooltip_text = tooltip_format.replace( '%1$s', service_title ).replace( '%2$s', service_description ); |
| 396 |
var $thumbnail = $( '<span>', { |
| 397 |
'class': 'wpbc_appointment_services__service_thumbnail tooltip_top', |
| 398 |
'data-original-title': tooltip_text, |
| 399 |
'aria-label': tooltip_text, |
| 400 |
role: 'img', |
| 401 |
tabindex: '0' |
| 402 |
} ); |
| 403 |
if ( pictureUrl ) { |
| 404 |
$( '<img>', { src: pictureUrl, alt: '', loading: 'lazy', decoding: 'async' } ).appendTo( $thumbnail ); |
| 405 |
} else { |
| 406 |
$( '<i>', { 'class': 'menu_icon icon-1x wpbc-bi-image-fill', 'aria-hidden': 'true' } ).appendTo( $thumbnail ); |
| 407 |
} |
| 408 |
return $thumbnail; |
| 409 |
} |
| 410 |
/** |
| 411 |
* Destroy Service thumbnail tooltips before AJAX replaces their elements. |
| 412 |
* |
| 413 |
* @return {void} |
| 414 |
*/ |
| 415 |
function destroy_service_thumbnail_tooltips() { |
| 416 |
$( '.wpbc_appointment_services__service_thumbnail' ).each( function () { |
| 417 |
if ( this._tippy && 'function' === typeof this._tippy.destroy ) { |
| 418 |
this._tippy.destroy(); |
| 419 |
} |
| 420 |
} ); |
| 421 |
} |
| 422 |
/** |
| 423 |
* Initialize Service thumbnail tooltips after an AJAX listing render. |
| 424 |
* |
| 425 |
* The native title attribute is used only when the Booking Calendar Tippy |
| 426 |
* helper is unavailable, avoiding duplicate browser and Tippy tooltips. |
| 427 |
* |
| 428 |
* @return {void} |
| 429 |
*/ |
| 430 |
function refresh_service_thumbnail_tooltips() { |
| 431 |
var listing_selector = '#wpbc_ui_listing_appointment_services_catalog '; |
| 432 |
var $thumbnails = $( listing_selector + '.wpbc_appointment_services__service_thumbnail' ); |
| 433 |
var tooltips_initialized = false; |
| 434 |
|
| 435 |
if ( 'function' === typeof w.wpbc_define_tippy_tooltips ) { |
| 436 |
tooltips_initialized = w.wpbc_define_tippy_tooltips( listing_selector ); |
| 437 |
} |
| 438 |
if ( tooltips_initialized ) { |
| 439 |
return; |
| 440 |
} |
| 441 |
$thumbnails.each( function () { |
| 442 |
$( this ).attr( 'title', $( this ).attr( 'data-original-title' ) || '' ); |
| 443 |
} ); |
| 444 |
} |
| 445 |
/** |
| 446 |
* Build the compact duration and before/after buffer summary for one Service. |
| 447 |
* |
| 448 |
* @param {Object} service Normalized Service response. |
| 449 |
* @return {jQuery} Duration details wrapper for the listing column. |
| 450 |
*/ |
| 451 |
function service_duration_node( service ) { |
| 452 |
var duration_minutes = Math.max( 0, Number( service.duration_minutes || 0 ) ); |
| 453 |
var buffer_before_minutes = Math.max( 0, Number( service.buffer_before_minutes || 0 ) ); |
| 454 |
var buffer_after_minutes = Math.max( 0, Number( service.buffer_after_minutes || 0 ) ); |
| 455 |
var duration_format = String( config.i18n.duration_minutes || '%s min' ); |
| 456 |
var buffers_format = String( config.i18n.buffers_summary || 'Buffers: %1$s / %2$s min' ); |
| 457 |
var buffers_tooltip_format = String( config.i18n.buffers_tooltip || 'Buffer before: %1$s min; Buffer after: %2$s min' ); |
| 458 |
var buffers_summary = buffers_format.replace( '%1$s', buffer_before_minutes ).replace( '%2$s', buffer_after_minutes ); |
| 459 |
var buffers_tooltip = buffers_tooltip_format.replace( '%1$s', buffer_before_minutes ).replace( '%2$s', buffer_after_minutes ); |
| 460 |
var $duration_details = $( '<span>', { 'class': 'wpbc_appointment_services__duration_details' } ); |
| 461 |
|
| 462 |
$( '<strong>', { |
| 463 |
'class': 'wpbc_appointment_services__duration_value', |
| 464 |
text: duration_format.replace( '%s', duration_minutes ) |
| 465 |
} ).appendTo( $duration_details ); |
| 466 |
$( '<span>', { |
| 467 |
'class': 'wpbc_appointment_services__buffers_summary', |
| 468 |
text: buffers_summary, |
| 469 |
title: buffers_tooltip, |
| 470 |
'aria-label': buffers_tooltip |
| 471 |
} ).appendTo( $duration_details ); |
| 472 |
|
| 473 |
return $duration_details; |
| 474 |
} |
| 475 |
/** |
| 476 |
* Return assigned Providers with recurring availability on one weekday. |
| 477 |
* |
| 478 |
* @param {Object} service Normalized Service response. |
| 479 |
* @param {string} day Weekday key from mon through sun. |
| 480 |
* @return {Array<Object>} Matching Provider presentation records. |
| 481 |
*/ |
| 482 |
function providers_available_on( service, day ) { |
| 483 |
var available_providers = []; |
| 484 |
$.each( service.resource_ids || [], function ( index, id ) { |
| 485 |
var provider = state.providers[ String( Number( id || 0 ) ) ]; |
| 486 |
if ( provider && provider.weekdays && provider.weekdays[ day ] ) { |
| 487 |
available_providers.push( provider ); |
| 488 |
} |
| 489 |
} ); |
| 490 |
|
| 491 |
return available_providers; |
| 492 |
} |
| 493 |
/** |
| 494 |
* Build compact Provider-specific links below the weekly availability dots. |
| 495 |
* |
| 496 |
* @param {Object} service Normalized Service response. |
| 497 |
* @return {jQuery} Availability links, or an empty collection when unavailable. |
| 498 |
*/ |
| 499 |
function availability_edit_links( service ) { |
| 500 |
var $links = $( '<div>', { |
| 501 |
'class': 'wpbc_appointment_services__availability_links', |
| 502 |
'aria-label': config.i18n.edit_provider_availability || 'Edit Provider availability' |
| 503 |
} ); |
| 504 |
|
| 505 |
$.each( service.resource_ids || [], function ( index, id ) { |
| 506 |
var provider = state.providers[ String( Number( id || 0 ) ) ]; |
| 507 |
var provider_title; |
| 508 |
var link_title; |
| 509 |
|
| 510 |
if ( ! provider || ! provider.availability_url ) { |
| 511 |
return; |
| 512 |
} |
| 513 |
provider_title = provider.title || 'Provider #' + Number( id || 0 ); |
| 514 |
link_title = String( config.i18n.edit_availability || 'Edit availability for %s' ).replace( '%s', provider_title ); |
| 515 |
$( '<a>', { |
| 516 |
'class': 'wpbc_appointment_services__availability_link', |
| 517 |
href: provider.availability_url, |
| 518 |
text: provider.initials || 'P', |
| 519 |
title: link_title, |
| 520 |
'aria-label': link_title |
| 521 |
} ).appendTo( $links ); |
| 522 |
} ); |
| 523 |
|
| 524 |
return $links.children().length ? $links : $(); |
| 525 |
} |
| 526 |
/** Convert a stored Service status to its translated UI label. */ |
| 527 |
function statusLabel( status ) { |
| 528 |
if ( 'inactive' === status ) { return config.i18n.draft || 'Draft'; } |
| 529 |
if ( 'archived' === status ) { return config.i18n.archived || 'Archived'; } |
| 530 |
return config.i18n.active || 'Active'; |
| 531 |
} |
| 532 |
/** Format the translated table pagination summary. */ |
| 533 |
function showingText( from, to, total ) { |
| 534 |
var format = config.i18n.showing || 'Showing %1$s–%2$s of %3$s Services'; |
| 535 |
return format.replace( '%1$s', from ).replace( '%2$s', to ).replace( '%3$s', total ); |
| 536 |
} |
| 537 |
/** |
| 538 |
* Apply the server-authoritative listing preference to state and controls. |
| 539 |
* |
| 540 |
* @param {Object} listing_settings Shared listing client settings. |
| 541 |
* @return {void} |
| 542 |
*/ |
| 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 ) ); |
| 548 |
} |
| 549 |
/** |
| 550 |
* Synchronize sortable table headers with the server-authoritative ordering. |
| 551 |
* |
| 552 |
* @param {Object} sorting Normalized sort key and direction. |
| 553 |
* @return {void} |
| 554 |
*/ |
| 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'; |
| 558 |
|
| 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' ); |
| 563 |
|
| 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' ); |
| 567 |
} ); |
| 568 |
$( '.wpbc_appointment_services__table th' ).each( function () { |
| 569 |
var $column = $( this ); |
| 570 |
if ( ! $column.find( '.wpbc_ui_listing__sort_link' ).length ) { |
| 571 |
return; |
| 572 |
} |
| 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' ); |
| 575 |
} ); |
| 576 |
} |
| 577 |
/** |
| 578 |
* Synchronize direct-page and previous/next controls with the server response. |
| 579 |
* |
| 580 |
* @param {Object} pagination Server-authoritative pagination metadata. |
| 581 |
* @return {void} |
| 582 |
*/ |
| 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 ); |
| 589 |
} |
| 590 |
/** |
| 591 |
* Request the valid page entered in the shared direct-page control. |
| 592 |
* |
| 593 |
* @param {jQuery} $page_control Direct-page number input. |
| 594 |
* @return {void} |
| 595 |
*/ |
| 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(); |
| 603 |
} |
| 604 |
/** |
| 605 |
* Render one server-selected page of Services in the management table. |
| 606 |
* |
| 607 |
* @param {Array} services Services returned for the requested page only. |
| 608 |
* @param {Object} pagination Server-authoritative pagination metadata. |
| 609 |
* @return {void} |
| 610 |
*/ |
| 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 ); |
| 631 |
|
| 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 ); |
| 648 |
} |
| 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 ); |
| 663 |
} ); |
| 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 ); |
| 667 |
} |
| 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 ); |
| 676 |
} ); |
| 677 |
|
| 678 |
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 ); |
| 683 |
} |
| 684 |
/** Load one Service and open it in the right inspector. */ |
| 685 |
function loadOne( serviceId ) { |
| 686 |
if ( ! serviceId || state.busy ) { return; } |
| 687 |
if ( editorIsOpen() && state.selectedId === serviceId ) { expand_service_inspector(); focus_requested_service_section(); return; } |
| 688 |
if ( ! can_replace_editor() ) { return; } |
| 689 |
setBusy( true ); |
| 690 |
request( config.actions.load, { service_id: serviceId } ).done( function ( response ) { |
| 691 |
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 |
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 ); } ); |
| 694 |
} |
| 695 |
/** Reload Services and Provider presentation data for the active filters. */ |
| 696 |
function loadList( save_items_per_page ) { |
| 697 |
var request_data = { |
| 698 |
search: $( '#wpbc_service_search' ).val() || '', |
| 699 |
status: state.status, |
| 700 |
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 |
| 705 |
}; |
| 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 ); } ); |
| 724 |
} |
| 725 |
/** Archive one Service after confirmation, then refresh the list. */ |
| 726 |
function archiveService( serviceId ) { |
| 727 |
if ( ! serviceId || state.busy || ! w.confirm( config.i18n.confirm_archive || 'Archive this Service?' ) ) { return; } |
| 728 |
setBusy( true ); |
| 729 |
request( config.actions.archive, { service_id: serviceId } ).done( function ( response ) { |
| 730 |
if ( response && response.success ) { |
| 731 |
if ( state.selectedId === serviceId ) { state.selectedId = 0; setFieldsEnabled( false ); updateUrl( 0 ); } |
| 732 |
notify( response.data.message, 'success' ); loadList(); return; |
| 733 |
} |
| 734 |
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 ); } ); |
| 736 |
} |
| 737 |
|
| 738 |
$( document ).on( 'click', '.wpbc_appointment_services__rightbar_tabs [role="tab"]', function ( event ) { event.preventDefault(); switchRightPanel( $( this ) ); } ); |
| 739 |
$( 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 ) ); } ); |
| 743 |
$( document ).on( 'click', '.wpbc_appointment_services__row_archive', function () { archiveService( Number( $( this ).data( 'service-id' ) || 0 ) ); } ); |
| 744 |
$( document ).on( 'click', '.wpbc_appointment_services__status_filter', function () { |
| 745 |
state.status = String( $( this ).data( 'service-status' ) || 'all' ); |
| 746 |
state.page = 1; |
| 747 |
$( '.wpbc_appointment_services__status_filter' ).removeClass( 'is-active' ).attr( 'aria-pressed', 'false' ); |
| 748 |
$( 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 |
loadList( true ); |
| 773 |
} ); |
| 774 |
$( document ).on( 'click', '.wpbc_appointment_services__add', function () { |
| 775 |
if ( ! state.storageReady || state.busy || ! can_replace_editor() ) { return; } |
| 776 |
fillEditor( blankService() ); |
| 777 |
updateUrl( 0 ); |
| 778 |
open_add_service_inspector(); |
| 779 |
} ); |
| 780 |
$( document ).on( 'click', '.wpbc_appointment_services__save', function () { |
| 781 |
if ( ! state.storageReady || state.busy ) { return; } |
| 782 |
setBusy( true ); |
| 783 |
request( config.actions.save, { service: collectEditor() } ).done( function ( response ) { |
| 784 |
if ( response && response.success && response.data && response.data.service ) { fillEditor( response.data.service ); updateUrl( state.selectedId ); notify( response.data.message, 'success' ); loadList(); return; } |
| 785 |
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 ); } ); |
| 787 |
} ); |
| 788 |
$( document ).on( 'click', '.wpbc_appointment_services__duplicate', function () { |
| 789 |
if ( ! state.selectedId || state.busy ) { return; } |
| 790 |
setBusy( true ); |
| 791 |
request( config.actions.duplicate, { service_id: state.selectedId } ).done( function ( response ) { |
| 792 |
if ( response && response.success && response.data && response.data.service ) { fillEditor( response.data.service ); updateUrl( state.selectedId ); notify( response.data.message, 'success' ); loadList(); return; } |
| 793 |
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 ); } ); |
| 795 |
} ); |
| 796 |
$( document ).on( 'click', '.wpbc_appointment_services__archive', function () { archiveService( state.selectedId ); } ); |
| 797 |
$( document ).on( 'input change', '[data-service-range-field]', function () { |
| 798 |
var field_id = String( $( this ).data( 'service-range-field' ) || '' ); |
| 799 |
if ( field_id ) { $( '[data-service-field="' + field_id + '"]' ).val( $( this ).val() ).trigger( 'input' ); } |
| 800 |
} ); |
| 801 |
$( document ).on( 'input change', 'input[type="number"][data-service-field]', function () { sync_numeric_range( String( $( this ).data( 'service-field' ) || '' ) ); } ); |
| 802 |
$( document ).on( 'change', '[data-service-status-choice]', function () { |
| 803 |
if ( this.checked ) { $( '[data-service-field="status"]' ).val( this.value ).trigger( 'change' ); } |
| 804 |
} ); |
| 805 |
$( document ).on( 'input change wpbc_media_upload_url_set', '[data-service-field="picture_url"]', function () { updateMediaPreview(); updateControls(); } ); |
| 806 |
$( document ).on( 'click', '.wpbc_appointment_services__remove_image', function () { |
| 807 |
if ( $( this ).prop( 'disabled' ) ) { return; } |
| 808 |
$( '[data-service-field="picture_url"]' ).val( '' ).trigger( 'input' ).trigger( 'change' ); |
| 809 |
} ); |
| 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(); } } ); |
| 813 |
} )( window, jQuery ); |
| 814 |
|