( function ( w, $ ) { 'use strict'; var config = w.wpbc_appointment_services_config || {}; var state = { storageReady: false, selectedId: Number( config.selected_id || 0 ), requested_focus: String( config.focus_section || '' ), focus_handled: false, busy: false, status: 'all', services: [], providers: {}, providerCount: 0, editor_snapshot: '', page: 1, page_size: Number( config.listing && config.listing.items_per_page ? config.listing.items_per_page : 10 ), total_items: 0, total_pages: 0, sort_by: String( config.listing && config.listing.sort_by ? config.listing.sort_by : 'service_id' ), sort_order: String( config.listing && config.listing.sort_order ? config.listing.sort_order : 'desc' ) }; var searchTimer = 0; var weekdayKeys = [ 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' ]; /** Extract an API message while preserving a caller-provided fallback. */ function messageFrom( response, fallback ) { return response && response.data && response.data.message ? response.data.message : fallback; } /** Display a shared Booking Calendar administrator notice. */ function notify( message, type ) { if ( message && typeof w.wpbc_admin_show_message === 'function' ) { w.wpbc_admin_show_message( message, type || 'info', 5000, false ); } } /** * Show the native Booking Calendar Processing notice. * * The returned element identifies this specific request's notice so * overlapping requests cannot dismiss each other's feedback. * * @return {jQuery} Processing notice wrapper, or an empty collection. */ function show_processing_notice() { if ( 'function' !== typeof w.wpbc_admin_show_message_processing ) { return $(); } w.wpbc_admin_show_message_processing( '' ); return $( '#ajax_working .wpbc_processing.wpbc_spin' ).last().closest( '[id^="wpbc_notice_"]' ); } /** * Hide the Processing notice created for one completed request. * * @param {jQuery} $processing_notice Notice wrapper returned by show_processing_notice(). * @return {void} */ function hide_processing_notice( $processing_notice ) { if ( $processing_notice && $processing_notice.length ) { $processing_notice.stop( true, true ).hide(); } } /** * Send an authenticated Appointment Services AJAX request. * * Every request uses the shared administrator Processing notice and removes * only its own notice after the request settles. * * @param {string} action WordPress AJAX action name. * @param {Object} data Request-specific payload. * @return {jqXHR} jQuery AJAX promise for the request. */ function request( action, data ) { var $processing_notice = show_processing_notice(); return $.ajax( { url: config.ajax_url, type: 'POST', dataType: 'json', data: $.extend( { action: action, nonce: config.nonce }, data || {} ) } ) .always( function () { hide_processing_notice( $processing_notice ); } ); } /** Toggle the loading overlay without hiding the existing Service table. */ function setLoading( isLoading ) { $( '.wpbc_appointment_services__loading' ).toggleClass( 'is-visible', isLoading ).attr( 'aria-hidden', isLoading ? 'false' : 'true' ); $( '.wpbc_appointment_services__content' ).attr( 'aria-busy', isLoading ? 'true' : 'false' ); } /** Activate the Settings or Help panel selected in the right sidebar. */ function switchRightPanel( $tab ) { var panelId = $tab.attr( 'aria-controls' ); var panel = panelId ? document.getElementById( panelId ) : null; var $tabs = $tab.closest( '.wpbc_appointment_services__rightbar_tabs' ).find( '[role="tab"]' ); var $panels = $( '.wpbc_appointment_services__rightbar [role="tabpanel"]' ); if ( ! panel ) { return; } $tabs.attr( 'aria-selected', 'false' ); $tab.attr( 'aria-selected', 'true' ); $panels.prop( 'hidden', true ).attr( 'aria-hidden', 'true' ); $( panel ).prop( 'hidden', false ).attr( 'aria-hidden', 'false' ); updateControls(); } /** * Tell the active Setup Wizard bar that the page workspace changed width. * * The delayed notification runs after the shared sidebar transition so the * setup bar can measure the final inspector boundary. * * @return {void} */ function notify_setup_wizard_layout_changed() { $( document ).trigger( 'wpbc_setup_wizard_layout_changed' ); w.setTimeout( function () { $( document ).trigger( 'wpbc_setup_wizard_layout_changed' ); }, 300 ); } /** * Expand the right sidebar and display the Service Settings inspector. * * @return {void} */ function expand_service_inspector() { var $settings_tab = $( '#wpbc_tab_service_settings' ); if ( 'function' === typeof w.wpbc_admin_ui__sidebar_right__do_max ) { w.wpbc_admin_ui__sidebar_right__do_max(); } if ( $settings_tab.length ) { switchRightPanel( $settings_tab ); } notify_setup_wizard_layout_changed(); } /** * Reveal, expand, highlight, and optionally focus one Service editor group. * * @param {string} fields_selector Group fields selector from the fixed editor markup. * @param {string} focus_selector Optional control selector to focus after scrolling. * @return {boolean} True when the requested inspector group was found. */ function open_service_inspector_group( fields_selector, focus_selector ) { var $group_fields = $( fields_selector ); var $group = $group_fields.closest( '.wpbc_ui__collapsible_group' ); var $group_header = $group.children( '.group__header' ); var group_element = $group.get( 0 ); var focus_element = focus_selector ? document.querySelector( focus_selector ) : null; expand_service_inspector(); if ( ! $group.length ) { return false; } $group.addClass( 'is-open' ); $group_header.attr( 'aria-expanded', 'true' ); $group_fields.prop( 'hidden', false ).attr( 'aria-hidden', 'false' ); $group.removeClass( 'wpbc_appointment_services__focus_pulse' ); if ( group_element ) { void group_element.offsetWidth; $group.addClass( 'wpbc_appointment_services__focus_pulse' ); try { group_element.scrollIntoView( { behavior: 'smooth', block: 'start', inline: 'nearest' } ); } catch ( error ) { group_element.scrollIntoView( true ); } w.setTimeout( function () { $group.removeClass( 'wpbc_appointment_services__focus_pulse' ); }, 900 ); } if ( focus_element && typeof focus_element.focus === 'function' ) { try { focus_element.focus( { preventScroll: true } ); } catch ( error ) { focus_element.focus(); } } return true; } /** * Open, reveal, and highlight the General Service editor section. * * @return {void} */ function open_add_service_inspector() { open_service_inspector_group( '#wpbc_service_general', '[data-service-field="title"]' ); } /** * Apply the one-time inspector focus requested by an administration link. * * @return {void} */ function focus_requested_service_section() { if ( state.focus_handled || 'booking_form' !== state.requested_focus ) { return; } if ( open_service_inspector_group( '#wpbc_service_form', '[data-service-field="booking_form_id"]' ) ) { state.focus_handled = true; if ( w.history && w.URL ) { var url = new w.URL( w.location.href ); url.searchParams.delete( 'wpbc_service_focus' ); w.history.replaceState( {}, '', url.toString() ); } } } /** Expand or collapse one inspector field group. */ function toggleInspectorGroup( $button ) { var $group = $button.closest( '.wpbc_ui__collapsible_group' ); var $fields = $group.find( '> .group__fields' ); var isOpen = $group.hasClass( 'is-open' ); $group.toggleClass( 'is-open', ! isOpen ); $button.attr( 'aria-expanded', isOpen ? 'false' : 'true' ); $fields.prop( 'hidden', isOpen ).attr( 'aria-hidden', isOpen ? 'true' : 'false' ); } /** Determine whether a Service is currently loaded into the editor. */ function editorIsOpen() { return ! $( '[data-service-field="title"]' ).prop( 'disabled' ); } /** * Synchronize one numeric Service field with its range control. * * The slider expands to represent an existing value above its normal visual * range without clamping or rewriting that stored value. * * @param {string} field_id Service field identifier. * @return {void} */ function sync_numeric_range( field_id ) { var $field = $( '[data-service-field="' + field_id + '"]' ); var $range = $( '[data-service-range-field="' + field_id + '"]' ); var value = Number( $field.val() ); var default_min = Number( $range.data( 'service-range-default-min' ) ); var default_max = Number( $range.data( 'service-range-default-max' ) ); var step = Number( $range.attr( 'step' ) || 1 ); var range_max; if ( ! $field.length || ! $range.length || ! isFinite( value ) ) { return; } default_min = isFinite( default_min ) ? default_min : 0; default_max = isFinite( default_max ) ? default_max : 100; step = isFinite( step ) && step > 0 ? step : 1; range_max = value > default_max ? default_min + ( Math.ceil( ( value - default_min ) / step ) * step ) : default_max; $range.attr( { min: default_min, max: range_max } ).val( value ); } /** * Synchronize every numeric Service field with its range control. * * @return {void} */ function sync_all_numeric_ranges() { $( '[data-service-range-field]' ).each( function () { sync_numeric_range( String( $( this ).data( 'service-range-field' ) || '' ) ); } ); } /** * Synchronize the visible Status radios with the stored Service status field. * * @return {void} */ function sync_status_radios() { var status = String( $( '[data-service-field="status"]' ).val() || 'active' ); $( '[data-service-status-choice]' ).each( function () { $( this ).prop( 'checked', String( $( this ).val() ) === status ); } ); } /** * Synchronize the Service image preview with the readonly URL field. * * @return {void} */ function updateMediaPreview() { var pictureUrl = String( $( '[data-service-field="picture_url"]' ).val() || '' ).trim(); var $image = $( '.wpbc_appointment_services__media_image' ); if ( pictureUrl ) { $image.attr( 'src', pictureUrl ); } else { $image.removeAttr( 'src' ); } $image.prop( 'hidden', ! pictureUrl ); $( '.wpbc_appointment_services__media_placeholder' ).prop( 'hidden', !! pictureUrl ); } /** Synchronize toolbar and inspector action states with the page state. */ function updateControls() { var open = state.storageReady && editorIsOpen(); var hasPicture = !! String( $( '[data-service-field="picture_url"]' ).val() || '' ).trim(); var show_save = open && 'true' === $( '#wpbc_tab_service_settings' ).attr( 'aria-selected' ); $( '.wpbc_appointment_services__add' ).prop( 'hidden', false ).prop( 'disabled', ! state.storageReady || state.busy ); $( '.wpbc_appointment_services__right_sidebar_footer, .wpbc_appointment_services__top_actions' ).prop( 'hidden', ! show_save ); $( '.wpbc_appointment_services__save' ).prop( 'hidden', ! show_save ).prop( 'disabled', ! open || state.busy ); $( '.wpbc_appointment_services__duplicate, .wpbc_appointment_services__archive' ).prop( 'disabled', ! open || ! state.selectedId || state.busy ); $( '.wpbc_appointment_services__media_preview, .wpbc_appointment_services__select_image' ).prop( 'disabled', ! open || state.busy ); $( '.wpbc_appointment_services__remove_image' ).prop( 'disabled', ! open || ! hasPicture || state.busy ); } /** Mark the page busy during a mutating request and refresh controls. */ function setBusy( value ) { state.busy = value; $( '.wpbc_appointment_services_page' ).toggleClass( 'is-busy', value ); updateControls(); } /** Enable or disable all fields in the Service inspector. */ function setFieldsEnabled( enabled ) { $( '[data-service-field], [data-service-range-field], [data-service-status-choice]' ).prop( 'disabled', ! enabled ); updateControls(); } /** Return defaults for a new unsaved Service. */ function blankService() { 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: [] }; } /** Populate the inspector from a normalized Service response. */ function fillEditor( service ) { service = $.extend( blankService(), service || {} ); state.selectedId = Number( service.service_id || 0 ); $.each( service, function ( key, value ) { $( '[data-service-field="' + key + '"]' ).val( value ); } ); sync_status_radios(); sync_all_numeric_ranges(); updateMediaPreview(); setFieldsEnabled( state.storageReady ); $( '.wpbc_appointment_services__item' ).removeClass( 'is-selected' ).attr( 'aria-current', 'false' ); $( '.wpbc_appointment_services__item[data-service-id="' + state.selectedId + '"]' ).addClass( 'is-selected' ).attr( 'aria-current', 'true' ); capture_editor_snapshot(); } /** Collect the current Service inspector values for saving. */ function collectEditor() { var service = { service_id: state.selectedId }; $( '[data-service-field]' ).each( function () { service[ $( this ).data( 'service-field' ) ] = $( this ).val(); } ); return service; } /** * Store the current editor values as the last loaded or saved state. * * @return {void} */ function capture_editor_snapshot() { state.editor_snapshot = JSON.stringify( collectEditor() ); } /** * Determine whether the open Service editor contains unsaved changes. * * @return {boolean} True when current fields differ from the captured state. */ function is_editor_dirty() { return editorIsOpen() && state.editor_snapshot !== JSON.stringify( collectEditor() ); } /** * Confirm before replacing an editor that contains unsaved Service changes. * * @return {boolean} True when replacing the current editor may continue. */ function can_replace_editor() { return ! is_editor_dirty() || w.confirm( config.i18n.confirm_discard || 'Discard unsaved Service changes?' ); } /** Reflect the selected Service in the admin URL without reloading. */ function updateUrl( serviceId ) { if ( ! w.history || ! w.URL ) { return; } var url = new w.URL( w.location.href ); if ( serviceId ) { url.searchParams.set( 'service_id', serviceId ); } else { url.searchParams.delete( 'service_id' ); } w.history.replaceState( {}, '', url.toString() ); } /** Render the empty or storage-unavailable state in the central workspace. */ function renderEmpty( message, storageNotice ) { var $empty = $( '.wpbc_appointment_services__empty' ); var noProviders = ! storageNotice && ! message && 0 === state.providerCount; $( '.wpbc_appointment_services__list' ).prop( 'hidden', true ); $empty.prop( 'hidden', false ).toggleClass( 'is-storage-notice', !! storageNotice ); $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' ) ) ); $empty.find( 'p' ).text( message || ( noProviders ? config.i18n.no_providers_help : config.i18n.empty_help ) || '' ); } /** Index Provider presentation records by booking resource ID. */ function indexProviders( providers ) { state.providers = {}; $.each( providers || [], function ( index, provider ) { state.providers[ String( provider.id ) ] = provider; } ); } /** Update status and Provider counters above the Service table. */ function updateSummary( counts, providerCount ) { counts = $.extend( { all: 0, active: 0, inactive: 0, archived: 0 }, counts || {} ); state.providerCount = Number( providerCount || 0 ); $.each( counts, function ( status, count ) { $( '[data-service-count="' + status + '"]' ).text( Number( count || 0 ) ); } ); $( '[data-provider-count]' ).text( state.providerCount ); $( '.wpbc_appointment_services__provider_notice' ).prop( 'hidden', 0 !== state.providerCount ); } /** Format a normalized Service cost using the configured currency symbol. */ function formatCost( cost ) { var amount = Number( cost || 0 ); var symbol = config.currency_symbol || '$'; return symbol + amount.toFixed( 2 ); } /** Build compact Provider avatar nodes for one Service row. */ function providerNodes( service ) { var ids = $.map( service.resource_ids || [], function ( value ) { return Number( value || 0 ); } ); var $stack = $( '
', { 'class': 'wpbc_appointment_services__provider_stack' } ); if ( ! ids.length ) { return $( '', { 'class': 'wpbc_appointment_services__no_provider', text: config.i18n.no_provider || 'No Providers assigned' } ); } $.each( ids.slice( 0, 3 ), function ( index, id ) { var provider = state.providers[ String( id ) ] || { id: id, title: 'Provider #' + id, initials: 'P', avatar_url: '' }; var has_availability = false !== provider.has_weekly_availability; var provider_title = provider.title || 'Provider #' + id; var avatar_title = provider_title; var avatar_attributes; var $avatar; if ( ! has_availability ) { avatar_title += ' — ' + ( config.i18n.no_availability || 'No weekly availability' ); } avatar_attributes = { 'class': 'wpbc_appointment_services__provider_avatar' + ( has_availability ? '' : ' has-no-availability' ), title: avatar_title }; if ( provider.availability_url ) { avatar_title = String( config.i18n.edit_availability || 'Edit availability for %s' ).replace( '%s', provider_title ); avatar_attributes.href = provider.availability_url; avatar_attributes.title = avatar_title; avatar_attributes[ 'aria-label' ] = avatar_title; $avatar = $( '', avatar_attributes ); } else { $avatar = $( '', avatar_attributes ); } if ( provider.avatar_url ) { $( '', { src: provider.avatar_url, alt: '', loading: 'lazy' } ).appendTo( $avatar ); } else { $avatar.text( provider.initials || 'P' ); } $stack.append( $avatar ); } ); if ( ids.length > 3 ) { $( '', { 'class': 'wpbc_appointment_services__provider_more', text: '+' + ( ids.length - 3 ), title: ( ids.length - 3 ) + ' ' + ( config.i18n.more_providers || 'more Providers' ) } ).appendTo( $stack ); } return $stack; } /** * Build the Service thumbnail used in the management table. * * @param {Object} service Normalized Service response. * @return {jQuery} Thumbnail wrapper containing an image or placeholder icon. */ function serviceThumbnailNode( service ) { var pictureUrl = String( service.picture_url || '' ).trim(); var service_title = String( service.title || config.i18n.untitled || 'Untitled Service' ); var service_description = String( service.description || '' ).trim() || config.i18n.no_description || 'No description'; var tooltip_format = String( config.i18n.service_thumbnail_tooltip || 'Title: %1$s\nDescription: %2$s' ); var tooltip_text = tooltip_format.replace( '%1$s', service_title ).replace( '%2$s', service_description ); var $thumbnail = $( '', { 'class': 'wpbc_appointment_services__service_thumbnail tooltip_top', 'data-original-title': tooltip_text, 'aria-label': tooltip_text, role: 'img', tabindex: '0' } ); if ( pictureUrl ) { $( '', { src: pictureUrl, alt: '', loading: 'lazy', decoding: 'async' } ).appendTo( $thumbnail ); } else { $( '', { 'class': 'menu_icon icon-1x wpbc-bi-image-fill', 'aria-hidden': 'true' } ).appendTo( $thumbnail ); } return $thumbnail; } /** * Destroy Service thumbnail tooltips before AJAX replaces their elements. * * @return {void} */ function destroy_service_thumbnail_tooltips() { $( '.wpbc_appointment_services__service_thumbnail' ).each( function () { if ( this._tippy && 'function' === typeof this._tippy.destroy ) { this._tippy.destroy(); } } ); } /** * Initialize Service thumbnail tooltips after an AJAX listing render. * * The native title attribute is used only when the Booking Calendar Tippy * helper is unavailable, avoiding duplicate browser and Tippy tooltips. * * @return {void} */ function refresh_service_thumbnail_tooltips() { var listing_selector = '#wpbc_ui_listing_appointment_services_catalog '; var $thumbnails = $( listing_selector + '.wpbc_appointment_services__service_thumbnail' ); var tooltips_initialized = false; if ( 'function' === typeof w.wpbc_define_tippy_tooltips ) { tooltips_initialized = w.wpbc_define_tippy_tooltips( listing_selector ); } if ( tooltips_initialized ) { return; } $thumbnails.each( function () { $( this ).attr( 'title', $( this ).attr( 'data-original-title' ) || '' ); } ); } /** * Build the compact duration and before/after buffer summary for one Service. * * @param {Object} service Normalized Service response. * @return {jQuery} Duration details wrapper for the listing column. */ function service_duration_node( service ) { var duration_minutes = Math.max( 0, Number( service.duration_minutes || 0 ) ); var buffer_before_minutes = Math.max( 0, Number( service.buffer_before_minutes || 0 ) ); var buffer_after_minutes = Math.max( 0, Number( service.buffer_after_minutes || 0 ) ); var duration_format = String( config.i18n.duration_minutes || '%s min' ); var buffers_format = String( config.i18n.buffers_summary || 'Buffers: %1$s / %2$s min' ); var buffers_tooltip_format = String( config.i18n.buffers_tooltip || 'Buffer before: %1$s min; Buffer after: %2$s min' ); var buffers_summary = buffers_format.replace( '%1$s', buffer_before_minutes ).replace( '%2$s', buffer_after_minutes ); var buffers_tooltip = buffers_tooltip_format.replace( '%1$s', buffer_before_minutes ).replace( '%2$s', buffer_after_minutes ); var $duration_details = $( '', { 'class': 'wpbc_appointment_services__duration_details' } ); $( '', { 'class': 'wpbc_appointment_services__duration_value', text: duration_format.replace( '%s', duration_minutes ) } ).appendTo( $duration_details ); $( '', { 'class': 'wpbc_appointment_services__buffers_summary', text: buffers_summary, title: buffers_tooltip, 'aria-label': buffers_tooltip } ).appendTo( $duration_details ); return $duration_details; } /** * Return assigned Providers with recurring availability on one weekday. * * @param {Object} service Normalized Service response. * @param {string} day Weekday key from mon through sun. * @return {Array} Matching Provider presentation records. */ function providers_available_on( service, day ) { var available_providers = []; $.each( service.resource_ids || [], function ( index, id ) { var provider = state.providers[ String( Number( id || 0 ) ) ]; if ( provider && provider.weekdays && provider.weekdays[ day ] ) { available_providers.push( provider ); } } ); return available_providers; } /** * Build compact Provider-specific links below the weekly availability dots. * * @param {Object} service Normalized Service response. * @return {jQuery} Availability links, or an empty collection when unavailable. */ function availability_edit_links( service ) { var $links = $( '
', { 'class': 'wpbc_appointment_services__availability_links', 'aria-label': config.i18n.edit_provider_availability || 'Edit Provider availability' } ); $.each( service.resource_ids || [], function ( index, id ) { var provider = state.providers[ String( Number( id || 0 ) ) ]; var provider_title; var link_title; if ( ! provider || ! provider.availability_url ) { return; } provider_title = provider.title || 'Provider #' + Number( id || 0 ); link_title = String( config.i18n.edit_availability || 'Edit availability for %s' ).replace( '%s', provider_title ); $( '', { 'class': 'wpbc_appointment_services__availability_link', href: provider.availability_url, text: provider.initials || 'P', title: link_title, 'aria-label': link_title } ).appendTo( $links ); } ); return $links.children().length ? $links : $(); } /** Convert a stored Service status to its translated UI label. */ function statusLabel( status ) { if ( 'inactive' === status ) { return config.i18n.draft || 'Draft'; } if ( 'archived' === status ) { return config.i18n.archived || 'Archived'; } return config.i18n.active || 'Active'; } /** Format the translated table pagination summary. */ function showingText( from, to, total ) { var format = config.i18n.showing || 'Showing %1$s–%2$s of %3$s Services'; return format.replace( '%1$s', from ).replace( '%2$s', to ).replace( '%3$s', total ); } /** * Apply the server-authoritative listing preference to state and controls. * * @param {Object} listing_settings Shared listing client settings. * @return {void} */ function sync_listing_settings( listing_settings ) { var page_size = Number( listing_settings && listing_settings.items_per_page ? listing_settings.items_per_page : state.page_size ); if ( ! isFinite( page_size ) || page_size < 1 ) { return; } state.page_size = page_size; $( '[data-wpbc-listing-items-per-page-control="appointment_services_catalog"]' ).val( String( page_size ) ); } /** * Synchronize sortable table headers with the server-authoritative ordering. * * @param {Object} sorting Normalized sort key and direction. * @return {void} */ function sync_sorting_controls( sorting ) { state.sort_by = String( sorting && sorting.sort_by ? sorting.sort_by : state.sort_by ); state.sort_order = 'desc' === String( sorting && sorting.sort_order ? sorting.sort_order : state.sort_order ) ? 'desc' : 'asc'; $( '[data-wpbc-listing-sort="appointment_services_catalog"]' ).each( function () { var $sort_link = $( this ); var is_active = String( $sort_link.data( 'wpbc-listing-sort-key' ) || '' ) === state.sort_by; var $sort_icon = $sort_link.find( '.wpbc_ui_listing__sort_icon' ); $sort_link.toggleClass( 'is-active', is_active ); $sort_icon.removeClass( 'wpbc_icn_import_export wpbc-bi-arrow-down wpbc-bi-arrow-up' ) .addClass( is_active ? ( 'desc' === state.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export' ); } ); $( '.wpbc_appointment_services__table th' ).each( function () { var $column = $( this ); if ( ! $column.find( '.wpbc_ui_listing__sort_link' ).length ) { return; } var has_active_sort = $column.find( '.wpbc_ui_listing__sort_link.is-active' ).length > 0; $column.attr( 'aria-sort', has_active_sort ? ( 'desc' === state.sort_order ? 'descending' : 'ascending' ) : 'none' ); } ); } /** * Synchronize direct-page and previous/next controls with the server response. * * @param {Object} pagination Server-authoritative pagination metadata. * @return {void} */ function sync_pagination_controls( pagination ) { var total_pages = Math.max( 0, Number( pagination && pagination.total_pages ? pagination.total_pages : 0 ) ); var maximum_page = Math.max( 1, total_pages ); var $page_control = $( '[data-wpbc-listing-page-number-control="appointment_services_catalog"]' ); $page_control.attr( 'max', maximum_page ).val( state.page ).prop( 'disabled', total_pages <= 1 ); $( '[data-wpbc-listing-page-total="appointment_services_catalog"]' ).text( total_pages ); } /** * Request the valid page entered in the shared direct-page control. * * @param {jQuery} $page_control Direct-page number input. * @return {void} */ function request_selected_page( $page_control ) { var requested_page = Number( $page_control.val() ); requested_page = Math.min( Math.max( 1, isFinite( requested_page ) ? requested_page : state.page ), Math.max( 1, state.total_pages ) ); $page_control.val( requested_page ); if ( requested_page === state.page ) { return; } state.page = requested_page; loadList(); } /** * Render one server-selected page of Services in the management table. * * @param {Array} services Services returned for the requested page only. * @param {Object} pagination Server-authoritative pagination metadata. * @return {void} */ function renderList( services, pagination ) { var $list = $( '.wpbc_appointment_services__list' ); var $tbody = $( '.wpbc_appointment_services__table tbody' ); var items_from; var items_to; var page_items; destroy_service_thumbnail_tooltips(); $tbody.empty(); pagination = pagination || {}; state.services = services || []; state.page = Math.max( 1, Number( pagination.page_number || state.page || 1 ) ); state.total_items = Math.max( 0, Number( pagination.total_items || 0 ) ); state.total_pages = Math.max( 0, Number( pagination.total_pages || 0 ) ); sync_pagination_controls( pagination ); $( '.wpbc_appointment_services__empty' ).prop( 'hidden', true ); if ( ! state.services.length ) { renderEmpty( '', false ); return; } items_from = Math.max( 1, Number( pagination.items_from || ( ( state.page - 1 ) * state.page_size ) + 1 ) ); items_to = Math.max( items_from, Number( pagination.items_to || ( items_from + state.services.length - 1 ) ) ); page_items = state.services; $list.prop( 'hidden', false ); $.each( page_items, function ( index, service ) { var id = Number( service.service_id || 0 ); var title = service.title || config.i18n.untitled || 'Untitled Service'; var description = String( service.description || '#' + id ); var status = String( service.status || 'active' ); var $row = $( '', { 'class': 'wpbc_appointment_services__item', 'data-service-id': id, tabindex: '0', 'aria-current': id === state.selectedId ? 'true' : 'false' } ); var $serviceCell = $( '', { 'class': 'column-service', 'data-label': config.i18n.column_service || 'Service' } ).appendTo( $row ); $row.toggleClass( 'is-selected', id === state.selectedId ); var $serviceIdentity = $( '
', { 'class': 'wpbc_appointment_services__service_identity' } ).appendTo( $serviceCell ); var $serviceCopy = $( '', { 'class': 'wpbc_appointment_services__service_copy' } ).appendTo( $serviceIdentity ); serviceThumbnailNode( service ).prependTo( $serviceIdentity ); $( '', { text: title } ).appendTo( $serviceCopy ); $( '', { text: description } ).appendTo( $serviceCopy ); $( '', { 'class': 'column-duration', 'data-label': config.i18n.column_duration || 'Duration' } ).append( service_duration_node( service ) ).appendTo( $row ); if ( config.pricing_available ) { $( '', { 'class': 'column-price', 'data-label': config.i18n.column_price || 'Price', text: formatCost( service.base_cost ) } ).appendTo( $row ); } $( '', { 'class': 'column-providers', 'data-label': config.i18n.column_providers || 'Providers' } ).append( providerNodes( service ) ).appendTo( $row ); var $availabilityCell = $( '', { 'class': 'column-weekdays', 'data-label': config.i18n.column_weekly_availability || 'Weekly Availability' } ).appendTo( $row ); var $availabilityWeek = $( '
', { 'class': 'wpbc_appointment_services__availability_week' } ).appendTo( $availabilityCell ); var hasWeeklyAvailability = false; $.each( weekdayKeys, function ( dayIndex, day ) { var available_providers = providers_available_on( service, day ); var available = available_providers.length > 0; if ( available ) { hasWeeklyAvailability = true; } var dayTitle = config.weekdays && config.weekdays[ dayIndex ] ? config.weekdays[ dayIndex ] : day; var provider_titles = $.map( available_providers, function ( provider ) { return provider.title || ''; } ).filter( function ( title ) { return !! title; } ); var availability_title = available ? String( config.i18n.available_providers || 'Available Providers: %s' ).replace( '%s', provider_titles.join( ', ' ) ) : ( config.i18n.no_available_providers || 'No assigned Providers are available' ); $( '', { 'class': 'wpbc_appointment_services__availability' + ( available ? ' is-available' : '' ), title: dayTitle + ': ' + availability_title, 'aria-label': dayTitle + ': ' + availability_title } ).appendTo( $availabilityWeek ); } ); $availabilityCell.append( availability_edit_links( service ) ); if ( service.resource_ids && service.resource_ids.length && ! hasWeeklyAvailability ) { $( '', { 'class': 'wpbc_appointment_services__availability_empty', text: config.i18n.no_availability || 'No weekly availability' } ).appendTo( $availabilityCell ); } var $status_cell = $( '', { 'class': 'column-status', 'data-label': config.i18n.column_status || 'Status' } ).appendTo( $row ); var $status_identity = $( '
', { 'class': 'wpbc_appointment_services__status_identity' } ).appendTo( $status_cell ); $( '', { 'class': 'wpbc_appointment_services__status status-' + status, text: statusLabel( status ) } ).appendTo( $status_identity ); $( '', { 'class': 'wpbc_appointment_services__id', text: ( config.i18n.column_id || 'ID' ) + ': ' + id } ).appendTo( $status_identity ); var $actions = $( '', { 'class': 'column-actions', 'data-label': config.i18n.column_actions || 'Actions' } ).appendTo( $row ); $( '