File: /mnt/data/dreamssalon-wp/wp-content/plugins/dreamsalon-core/inc/functions.php
<?php
if (! defined('ABSPATH')) exit;
if (! function_exists('ds_get_booking_page_url')){
/**
* Resolve the booking page URL (optionally scoped to a specific service ID).
*/
function ds_get_booking_page_url($service_id = 0){
static $base_url = null;
if ($base_url === null){
$booking_page_id = function_exists('dreamsalon_fl_framework_getoptions')
? (int) dreamsalon_fl_framework_getoptions('booking_page')
: 0;
if ($booking_page_id){
$base_url = get_permalink($booking_page_id);
}
if (! $base_url){
$booking_page = get_page_by_path('bookings');
if ($booking_page){
$base_url = get_permalink($booking_page);
}
}
if (! $base_url){
$base_url = home_url('/bookings/');
}
}
$url = $base_url;
if ($url && $service_id){
$url = add_query_arg('service_id', (int) $service_id, $url);
}
return $url ?: '';
}
}
// Ensure Staff role exists
add_action('init', function(){
if (! get_role('staff')) {
add_role('staff', __('Staff', 'dreamsalon-core'), [
'read' => true,
]);
}
function ds_resolve_service_slots_for_date($service_id, $date, $staff_id = 0, $post = null) {
$result = [
'slots' => array(),
'assigned_staff_id' => 0,
];
if (! $post) {
$post = get_post($service_id);
}
if (! $post || $post->post_type !== 'service') {
return $result;
}
if ($staff_id > 0) {
$result['slots'] = ds_get_available_slots_for_service_date($service_id, $date, $staff_id, $post);
$result['assigned_staff_id'] = $staff_id;
return $result;
}
$service_staff_ids = (array) get_post_meta($service_id, 'service_staff_users', true);
$service_staff_ids = array_filter(array_map('intval', $service_staff_ids));
if (!empty($service_staff_ids)) {
foreach ($service_staff_ids as $candidate_id) {
$candidate_slots = ds_get_available_slots_for_service_date($service_id, $date, $candidate_id, $post);
if (!empty($candidate_slots)) {
$result['slots'] = $candidate_slots;
$result['assigned_staff_id'] = $candidate_id;
return $result;
}
}
}
$result['slots'] = ds_get_available_slots_for_service_date($service_id, $date, 0, $post);
return $result;
}
// Service Amenities (flat, non-hierarchical)
if (! taxonomy_exists('service_amenities')){
register_taxonomy('service_amenities', ['service','product'], [
'labels' => [
'name' => __('Amenities', 'dreamsalon-core'),
'singular_name' => __('Amenity', 'dreamsalon-core'),
'search_items' => __('Search Amenities', 'dreamsalon-core'),
'all_items' => __('All Amenities', 'dreamsalon-core'),
'edit_item' => __('Edit Amenity', 'dreamsalon-core'),
'update_item' => __('Update Amenity', 'dreamsalon-core'),
'add_new_item' => __('Add New Amenity', 'dreamsalon-core'),
'new_item_name' => __('New Amenity Name', 'dreamsalon-core'),
'menu_name' => __('Amenities', 'dreamsalon-core'),
],
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'amenity', 'with_front' => false ],
]);
}
// Why Book With Us (flat)
if (! taxonomy_exists('service_why_book')){
register_taxonomy('service_why_book', ['service','product'], [
'labels' => [
'name' => __('Why Book With Us', 'dreamsalon-core'),
'singular_name' => __('Why Book Item', 'dreamsalon-core'),
'search_items' => __('Search Why Book Items', 'dreamsalon-core'),
'all_items' => __('All Why Book Items', 'dreamsalon-core'),
'edit_item' => __('Edit Why Book Item', 'dreamsalon-core'),
'update_item' => __('Update Why Book Item', 'dreamsalon-core'),
'add_new_item' => __('Add New Why Book Item', 'dreamsalon-core'),
'new_item_name' => __('New Why Book Item Name', 'dreamsalon-core'),
'menu_name' => __('Why Book With Us', 'dreamsalon-core'),
],
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'why-book', 'with_front' => false ],
]);
}
});
// Register reusable taxonomies for Services (and Products)
add_action('init', function(){
// Service Categories (hierarchical)
if (! taxonomy_exists('service_category')){
register_taxonomy('service_category', ['service','product'], [
'labels' => [
'name' => __('Service Categories', 'dreamsalon-core'),
'singular_name' => __('Service Category', 'dreamsalon-core'),
'search_items' => __('Search Service Categories', 'dreamsalon-core'),
'all_items' => __('All Service Categories', 'dreamsalon-core'),
'parent_item' => __('Parent Service Category', 'dreamsalon-core'),
'parent_item_colon' => __('Parent Service Category:', 'dreamsalon-core'),
'edit_item' => __('Edit Service Category', 'dreamsalon-core'),
'update_item' => __('Update Service Category', 'dreamsalon-core'),
'add_new_item' => __('Add New Service Category', 'dreamsalon-core'),
'new_item_name' => __('New Service Category Name', 'dreamsalon-core'),
'menu_name' => __('Service Categories', 'dreamsalon-core'),
],
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'service-category', 'with_front' => false ],
]);
}
// Service Branches (locations) - hierarchical to allow region > branch
if (! taxonomy_exists('service_branch')){
register_taxonomy('service_branch', ['service','product'], [
'labels' => [
'name' => __('Service Branches', 'dreamsalon-core'),
'singular_name' => __('Service Branch', 'dreamsalon-core'),
'search_items' => __('Search Service Branches', 'dreamsalon-core'),
'all_items' => __('All Service Branches', 'dreamsalon-core'),
'parent_item' => __('Parent Branch', 'dreamsalon-core'),
'parent_item_colon' => __('Parent Branch:', 'dreamsalon-core'),
'edit_item' => __('Edit Branch', 'dreamsalon-core'),
'update_item' => __('Update Branch', 'dreamsalon-core'),
'add_new_item' => __('Add New Branch', 'dreamsalon-core'),
'new_item_name' => __('New Branch Name', 'dreamsalon-core'),
'menu_name' => __('Service Branches', 'dreamsalon-core'),
],
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'service-branch', 'with_front' => false ],
]);
}
});
// Enqueue admin scripts/styles for service edit screen
add_action('wp_enqueue_scripts', function(){
$base_url = plugin_dir_url(dirname(__FILE__));
$theme_url = get_stylesheet_directory_uri();
// Styles
wp_enqueue_style('dreamsalon-core-frontend', $base_url . 'assets/admin.css', [], '1.0');
wp_enqueue_style('select2', $theme_url . '/assets/plugins/select2/css/select2.min.css', [], '4.1.0');
// Scripts
wp_enqueue_script('dreamsalon-core-frontend', $base_url . 'assets/scripts.js', ['jquery'], '1.1', true);
wp_enqueue_script('dreamsalon-core-wishlists', $base_url . 'assets/js/wishlists.js', ['jquery'], '1.0', true);
// Optional Select2 (only if file exists in your theme)
wp_enqueue_script('select2', $theme_url . '/assets/plugins/select2/js/select2.min.js', ['jquery'], '4.1.0', true);
$currency_symbol = get_woocommerce_currency_symbol();
// Pass the currency symbol to JavaScript
wp_localize_script( 'dreamsalon-core-frontend', 'wcCurrency', array(
'symbol' => $currency_symbol,
) );
$wishlist_count = is_user_logged_in() ? DreamService_get_wishlist_count(get_current_user_id()) : 0;
$current_user = wp_get_current_user();
$is_customer = ($current_user && in_array('customer', (array) $current_user->roles, true));
// Localize to the SAME handle as the enqueued JS above
wp_localize_script('dreamsalon-core-frontend', 'DSAjax', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('ds_admin_nonce'),
'wishlist_nonce' => wp_create_nonce('DreamService_wishlist'),
'wishlist_count' => (int) $wishlist_count,
'is_logged_in' => is_user_logged_in(),
'is_customer' => $is_customer,
]);
$cart_url = function_exists('wc_get_cart_url') ? wc_get_cart_url() : home_url('/cart');
$checkout_url = function_exists('wc_get_checkout_url') ? wc_get_checkout_url() : home_url('/checkout');
$currency_symbol = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
wp_localize_script('dreamsalon-core-frontend', 'DSFE', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('ds_admin_nonce'),
'cartUrl' => $cart_url,
'checkoutUrl' => $checkout_url,
'currencySymbol' => $currency_symbol,
]);
});
// AJAX: Load staff filtered by categories/branches
add_action('wp_ajax_ds_load_staff', function(){
if (!isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (!is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
$cats = isset($_POST['categories']) ? (array) $_POST['categories'] : array();
$brs = isset($_POST['branches']) ? (array) $_POST['branches'] : array();
$cats = array_filter(array_map('intval', $cats));
$brs = array_filter(array_map('intval', $brs));
$args = array(
'role' => 'staff',
'number' => 200,
'orderby' => 'display_name',
'order' => 'ASC',
'fields' => array('ID','display_name'),
);
$users = get_users($args);
$out = array();
foreach ($users as $u){
// optional status filter
$status = get_user_meta($u->ID, 'staff_status', true);
if ($status && $status !== 'active') continue;
$ucats = (array) get_user_meta($u->ID, 'staff_categories', true);
$ubrs = (array) get_user_meta($u->ID, 'staff_branches', true);
$ucats = array_filter(array_map('intval', $ucats));
$ubrs = array_filter(array_map('intval', $ubrs));
$ok = true;
if (!empty($cats)){
$ok = (count(array_intersect($cats, $ucats)) > 0);
}
if ($ok && !empty($brs)){
$ok = (count(array_intersect($brs, $ubrs)) > 0);
}
if ($ok){
$out[] = array('ID' => (int) $u->ID, 'name' => $u->display_name ?: ('User #'.$u->ID));
}
}
wp_send_json_success(array('users' => $out));
});
// Register meta boxes for Service post type
add_action('add_meta_boxes', function(){
add_meta_box(
'ds_service_scheduling',
__('Service Scheduling', 'dreamsalon-core'),
'ds_render_service_scheduling_metabox',
'service',
'normal',
'high'
);
add_meta_box(
'ds_service_assignment',
__('Branch & Staff Assignment', 'dreamsalon-core'),
'ds_render_service_assignment_metabox',
'service',
'normal',
'default'
);
});
function ds_render_service_scheduling_metabox($post){
wp_nonce_field('ds_save_service_meta', 'ds_service_meta_nonce');
$start_time = get_post_meta($post->ID, 'service_start_time', true);
$end_time = get_post_meta($post->ID, 'service_end_time', true);
$interval = (int) get_post_meta($post->ID, 'service_slot_interval', true) ?: 30;
$dur_hours = (int) get_post_meta($post->ID, 'service_duration_hours', true);
$dur_minutes = (int) get_post_meta($post->ID, 'service_duration_minutes', true);
$slots_times = (array) get_post_meta($post->ID, 'service_slots_times', true);
?>
<div class="ds-field-row">
<label for="ds_start_time" class="ds-label"><?php esc_html_e('Start Time', 'dreamsalon-core'); ?></label>
<input type="time" id="ds_start_time" name="ds_start_time" value="<?php echo esc_attr($start_time); ?>" />
<label for="ds_end_time" class="ds-label ms-3"><?php esc_html_e('End Time', 'dreamsalon-core'); ?></label>
<input type="time" id="ds_end_time" name="ds_end_time" value="<?php echo esc_attr($end_time); ?>" />
<label for="ds_interval" class="ds-label ms-3"><?php esc_html_e('Interval', 'dreamsalon-core'); ?></label>
<select id="ds_interval" name="ds_interval">
<option value="30" <?php selected($interval, 30); ?>><?php esc_html_e('30 minutes', 'dreamsalon-core'); ?></option>
<option value="60" <?php selected($interval, 60); ?>><?php esc_html_e('60 minutes', 'dreamsalon-core'); ?></option>
</select>
<button type="button" class="button button-primary ms-2" id="ds_generate_slots_btn"><?php esc_html_e('Generate Slots', 'dreamsalon-core'); ?></button>
</div>
<div class="ds-field-row mt-2">
<label class="ds-label"><?php esc_html_e('Duration Hours', 'dreamsalon-core'); ?></label>
<select name="ds_duration_hours" id="ds_duration_hours">
<option value="">—</option>
<?php for ($h=1;$h<=12;$h++): ?>
<option value="<?php echo (int)$h; ?>" <?php selected($dur_hours, $h); ?>><?php echo (int)$h; ?></option>
<?php endfor; ?>
</select>
<label class="ds-label ms-3"><?php esc_html_e('Duration Minutes', 'dreamsalon-core'); ?></label>
<select name="ds_duration_minutes" id="ds_duration_minutes">
<option value="">—</option>
<?php for ($m=1;$m<=59;$m++): ?>
<option value="<?php echo (int)$m; ?>" <?php selected($dur_minutes, $m); ?>><?php echo (int)$m; ?></option>
<?php endfor; ?>
</select>
</div>
<div class="ds-field-row mt-3">
<strong><?php esc_html_e('Generated Slots', 'dreamsalon-core'); ?></strong>
<div id="ds_generated_slots" class="ds-slots-wrap">
<?php if (!empty($slots_times) && is_array($slots_times)):
foreach ($slots_times as $slot): ?>
<span class="ds-slot-chip"><?php echo esc_html($slot); ?></span>
<?php endforeach; else: ?>
<em><?php esc_html_e('No slots generated yet.', 'dreamsalon-core'); ?></em>
<?php endif; ?>
</div>
<div id="ds_generated_slots_inputs">
<?php if (!empty($slots_times) && is_array($slots_times)):
foreach ($slots_times as $slot): ?>
<input type="hidden" name="ds_generated_slots[]" value="<?php echo esc_attr($slot); ?>" />
<?php endforeach; endif; ?>
</div>
</div>
<?php
}
function ds_render_service_assignment_metabox($post){
$selected_branches = [];
$selected_staff = (array) get_post_meta($post->ID, 'service_staff_users', true);
// Branch field: prefer taxonomy `service_branch`, else CPT `branch`
$use_tax = taxonomy_exists('service_branch');
if ($use_tax) {
$selected_branches = wp_get_object_terms($post->ID, 'service_branch', ['fields' => 'ids']);
$terms = get_terms(['taxonomy' => 'service_branch', 'hide_empty' => false]);
echo '<p><label class="ds-label">' . esc_html__('Branch Locations', 'dreamsalon-core') . '</label><br/>';
echo '<select multiple name="ds_branch_terms[]" id="ds_branch_terms" style="min-width:280px;">';
if (!is_wp_error($terms)){
foreach ($terms as $t){
printf('<option value="%d" %s>%s</option>', (int)$t->term_id, selected(in_array($t->term_id, (array)$selected_branches, true), true, false), esc_html($t->name));
}
}
echo '</select></p>';
} else {
// Fallback to CPT `branch`
$selected_branches = (array) get_post_meta($post->ID, 'service_branches', true);
$branches = get_posts(['post_type'=>'branch','numberposts'=>-1,'post_status'=>'publish']);
echo '<p><label class="ds-label">' . esc_html__('Branch Locations', 'dreamsalon-core') . '</label><br/>';
echo '<select multiple name="ds_branch_posts[]" id="ds_branch_posts" style="min-width:280px;">';
foreach ($branches as $b){
printf('<option value="%d" %s>%s</option>', (int)$b->ID, selected(in_array($b->ID, (array)$selected_branches, true), true, false), esc_html($b->post_title));
}
echo '</select></p>';
}
// Staff multi-select
echo '<p><label class="ds-label">' . esc_html__('Assign Staff', 'dreamsalon-core') . '</label><br/>';
echo '<select multiple name="ds_staff_users[]" id="ds_staff_users" style="min-width:280px;">';
// Prefill saved selections
if (!empty($selected_staff)){
$users = get_users(['include' => array_map('intval', $selected_staff)]);
foreach ($users as $u){
printf('<option value="%d" selected>%s</option>', (int)$u->ID, esc_html($u->display_name));
}
}
echo '</select></p>';
// Helper for dynamic filtering via AJAX
$category_terms = wp_get_object_terms($post->ID, 'service_category', ['fields' => 'ids']);
?>
<script>
jQuery(function($){
function loadStaff(){
var data = {
action: 'ds_load_staff',
_ajax_nonce: DSAjax.nonce,
categories: <?php echo wp_json_encode(array_map('intval', (array)$category_terms)); ?>,
branches: $('#ds_branch_terms').length ? $('#ds_branch_terms').val() : $('#ds_branch_posts').val()
};
$.post(DSAjax.ajaxUrl, data, function(resp){
if (!resp || !resp.success) return;
var $sel = $('#ds_staff_users');
var preselected = (<?php echo wp_json_encode(array_map('intval', (array)$selected_staff)); ?>) || [];
$sel.empty();
$.each(resp.data.users || [], function(_, u){
var selected = preselected.includes(parseInt(u.ID));
var opt = $('<option>').val(u.ID).text(u.name);
if (selected) opt.attr('selected','selected');
$sel.append(opt);
});
});
}
$('#ds_branch_terms, #ds_branch_posts').on('change', loadStaff);
// Initial load
loadStaff();
});
</script>
<?php
}
// Save meta box data
add_action('save_post_service', function($post_id, $post, $update){
if (! isset($_POST['ds_service_meta_nonce']) || ! wp_verify_nonce($_POST['ds_service_meta_nonce'], 'ds_save_service_meta')) return;
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (! current_user_can('edit_post', $post_id)) return;
// Times & interval
$start = isset($_POST['ds_start_time']) ? sanitize_text_field($_POST['ds_start_time']) : '';
$end = isset($_POST['ds_end_time']) ? sanitize_text_field($_POST['ds_end_time']) : '';
$interval = isset($_POST['ds_interval']) ? (int) $_POST['ds_interval'] : 30;
update_post_meta($post_id, 'service_start_time', $start);
update_post_meta($post_id, 'service_end_time', $end);
update_post_meta($post_id, 'service_slot_interval', $interval);
// Duration
$dh = isset($_POST['ds_duration_hours']) ? (int) $_POST['ds_duration_hours'] : 0;
$dm = isset($_POST['ds_duration_minutes']) ? (int) $_POST['ds_duration_minutes'] : 0;
update_post_meta($post_id, 'service_duration_hours', max(0, $dh));
update_post_meta($post_id, 'service_duration_minutes', max(0, min(59, $dm)));
// Generated slots
$slots = isset($_POST['ds_generated_slots']) ? array_map('sanitize_text_field', (array) $_POST['ds_generated_slots']) : [];
update_post_meta($post_id, 'service_slots_times', $slots);
// Branch save
if (taxonomy_exists('service_branch') && isset($_POST['ds_branch_terms'])){
$terms = array_map('intval', (array) $_POST['ds_branch_terms']);
wp_set_object_terms($post_id, $terms, 'service_branch');
} elseif (isset($_POST['ds_branch_posts'])){
$posts = array_map('intval', (array) $_POST['ds_branch_posts']);
update_post_meta($post_id, 'service_branches', $posts);
}
// Staff users
if (isset($_POST['ds_staff_users'])){
$staff = array_map('intval', (array) $_POST['ds_staff_users']);
update_post_meta($post_id, 'service_staff_users', $staff);
}
}, 10, 3);
// AJAX: generate slots between start and end at interval
add_action('wp_ajax_ds_generate_slots', function(){
check_ajax_referer('ds_admin_nonce');
$start = isset($_POST['start']) ? sanitize_text_field($_POST['start']) : '';
$end = isset($_POST['end']) ? sanitize_text_field($_POST['end']) : '';
$interval = isset($_POST['interval']) ? (int) $_POST['interval'] : 30;
$slots = ds_build_time_slots($start, $end, $interval);
wp_send_json_success(['slots' => $slots]);
});
function ds_build_time_slots($start, $end, $interval){
$out = [];
if (!$start || !$end) return $out;
// Expect format HH:MM
$s_parts = explode(':', $start);
$e_parts = explode(':', $end);
if (count($s_parts) < 2 || count($e_parts) < 2) return $out;
$s = ((int)$s_parts[0]) * 60 + (int)$s_parts[1];
$e = ((int)$e_parts[0]) * 60 + (int)$e_parts[1];
if ($e <= $s || $interval <= 0) return $out;
for ($m = $s; $m + $interval <= $e; $m += $interval){
$h = floor($m / 60); $min = $m % 60;
$h2 = floor(($m + $interval) / 60); $min2 = ($m + $interval) % 60;
$out[] = sprintf('%02d:%02d-%02d:%02d', $h, $min, $h2, $min2);
}
return $out;
}
// AJAX: load staff based on selected categories and branches
add_action('wp_ajax_ds_load_staff', function(){
check_ajax_referer('ds_admin_nonce');
$category_ids = isset($_POST['categories']) ? array_map('intval', (array) $_POST['categories']) : [];
$branch_ids = isset($_POST['branches']) ? array_map('intval', (array) $_POST['branches']) : [];
// Basic strategy: users with role 'staff' and user meta arrays containing intersection
$args = [
'role__in' => ['staff','editor','author','administrator'], // allow customization
'number' => 200,
'fields' => ['ID','display_name'],
'meta_query' => [ 'relation' => 'AND' ]
];
// Filter by branches if provided (meta: staff_branches as array of IDs)
if (!empty($branch_ids)){
$args['meta_query'][] = [
'key' => 'staff_branches',
'value' => array_map('strval', $branch_ids),
'compare' => 'IN'
];
}
// Filter by categories if provided (meta: staff_categories)
if (!empty($category_ids)){
$args['meta_query'][] = [
'key' => 'staff_categories',
'value' => array_map('strval', $category_ids),
'compare' => 'IN'
];
}
$users = get_users($args);
$out = [];
foreach ($users as $u){
$out[] = ['ID' => $u->ID, 'name' => $u->display_name];
}
wp_send_json_success(['users' => $out]);
});
// AJAX: load branches based on selected categories
// ===== Frontend Booking: Branch -> Categories =====
add_action('wp_ajax_ds_get_branch_categories', 'ds_ajax_get_branch_categories');
add_action('wp_ajax_nopriv_ds_get_branch_categories', 'ds_ajax_get_branch_categories');
function ds_ajax_get_branch_categories(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
$branch_id = isset($_POST['branch_id']) ? (int) $_POST['branch_id'] : 0;
$service_ids = [];
if ($branch_id > 0){
$q = new WP_Query([
'post_type' => 'service',
'post_status' => 'publish',
'posts_per_page' => 300,
'fields' => 'ids',
'tax_query' => [[
'taxonomy' => 'service_branch',
'field' => 'term_id',
'terms' => [$branch_id],
]],
]);
$service_ids = $q->posts;
}
if (empty($service_ids)){
wp_send_json_success(['categories' => []]);
}
$term_query = new WP_Term_Query([
'taxonomy' => 'service_category',
'hide_empty' => false,
'object_ids' => $service_ids,
'orderby' => 'name',
'order' => 'ASC',
]);
$cats_out = [];
if (! is_wp_error($term_query) && ! empty($term_query->terms)){
foreach ($term_query->terms as $t){
$cnt_q = new WP_Query([
'post_type' => 'service',
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'service_branch',
'field' => 'term_id',
'terms' => [$branch_id],
],
[
'taxonomy' => 'service_category',
'field' => 'term_id',
'terms' => [(int) $t->term_id],
],
],
]);
$nm = $t->name;
if (function_exists('mb_convert_case')) {
$nm = mb_convert_case($nm, MB_CASE_TITLE, 'UTF-8');
} else {
$nm = ucwords(strtolower($nm));
}
$cats_out[] = [
'id' => (int) $t->term_id,
'name' => $nm,
'count' => (int) $cnt_q->found_posts,
];
}
}
wp_send_json_success(['categories' => $cats_out]);
}
// ===== Frontend Booking: Load Services grid =====
add_action('wp_ajax_ds_get_services', 'ds_ajax_get_services');
add_action('wp_ajax_nopriv_ds_get_services', 'ds_ajax_get_services');
function ds_ajax_get_services(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
$branch_id = isset($_POST['branch_id']) ? (int) $_POST['branch_id'] : 0;
$category_id = isset($_POST['category_id']) ? (int) $_POST['category_id'] : 0;
$tax_query = [];
if ($branch_id > 0){
$tax_query[] = [
'taxonomy' => 'service_branch',
'field' => 'term_id',
'terms' => [$branch_id],
];
}
if ($category_id > 0){
$tax_query[] = [
'taxonomy' => 'service_category',
'field' => 'term_id',
'terms' => [$category_id],
];
}
if (count($tax_query) > 1){ $tax_query['relation'] = 'AND'; }
$q = new WP_Query([
'post_type' => 'service',
'post_status' => 'publish',
'posts_per_page' => 24,
'orderby' => 'date',
'order' => 'DESC',
'tax_query' => $tax_query,
]);
ob_start();
echo '<div class="row g-3">';
if ($q->have_posts()){
while ($q->have_posts()){ $q->the_post();
$sid = get_the_ID();
$title = get_the_title();
if ($title !== ''){
if (function_exists('mb_convert_case')) { $title = mb_convert_case($title, MB_CASE_TITLE, 'UTF-8'); }
else { $title = ucwords(strtolower($title)); }
}
$price = get_post_meta($sid, 'service_price', true);
$offer = get_post_meta($sid, 'service_offer_price', true);
$dur_h = (int) get_post_meta($sid, 'service_duration_hours', true);
$dur_m = (int) get_post_meta($sid, 'service_duration_minutes', true);
// Branch name (first service_branch term)
$branch_name = '';
$branch_terms = function_exists('get_the_terms') ? get_the_terms($sid, 'service_branch') : [];
if ($branch_terms && ! is_wp_error($branch_terms)) {
$branch_name = $branch_terms[0]->name;
}
// Reviews: compute average rating and count from comment meta 'rating'
$avg_rating = 0;
$rating_count = 0;
$comments = get_approved_comments($sid);
if (! empty($comments)) {
$sum = 0; $n = 0;
foreach ($comments as $c) {
$r = get_comment_meta($c->comment_ID, 'rating', true);
if ($r !== '' && $r !== null) {
$sum += floatval($r);
$n++;
}
}
if ($n > 0) {
$avg_rating = $sum / $n;
$rating_count = $n;
}
}
$thumb = '';
$gallery_ids = get_post_meta($sid, 'service_gallery', true);
if (is_array($gallery_ids) && !empty($gallery_ids)){
$first_id = (int) reset($gallery_ids);
$img = wp_get_attachment_image_url($first_id, 'medium');
if ($img) { $thumb = $img; }
}
if (!$thumb){
$fi = get_the_post_thumbnail_url($sid, 'medium');
if ($fi) { $thumb = $fi; }
}
if (! $thumb){ $thumb = 'https://via.placeholder.com/300x200?text=Service'; }
$dur_parts = [];
if ($dur_h > 0){ $dur_parts[] = $dur_h . ' Hrs'; }
if ($dur_m > 0){ $dur_parts[] = $dur_m . ' Mins'; }
$dur_text = !empty($dur_parts) ? implode(' ', $dur_parts) : '';
echo '<div class="col col-lg-3 col-md-6 col-sm-6 col-12 mb-3 ds-service-item" data-service-id="' . (int)$sid . '">';
echo '<div class="card">';
echo ' <div class="card-body service-card p-3">';
echo ' <img class="rounded-3 overflow-hidden ds-service-trigger" role="button" tabindex="0" data-service-id="' . (int)$sid . '" src="' . esc_url($thumb) . '" style="width: 100%; height: 150px; object-fit: cover; cursor: pointer;" alt="' . esc_attr($title) . '" />';
echo ' <div class="mt-3">';
echo ' <h5 class="fw-semibold mb-1 ds-service-trigger" role="button" tabindex="0" data-service-id="' . (int)$sid . '">' . esc_html($title) . '</h5>';
if ($branch_name !== ''){
echo ' <p class="text-muted small mb-1"> <i class="ti ti-map-pin me-1"></i>' . esc_html($branch_name) . '</p>';
}
if ($dur_text){ echo ' <p class="text-muted small mb-2"><i class="ti ti-clock me-1"></i>' . esc_html($dur_text) . '</p>'; }
// Reviews row
echo ' <div class="d-flex align-items-center mb-2 ratingsstar">';
$stars = round($avg_rating);
for ($i = 1; $i <= 5; $i++){
$cls = ($i <= $stars) ? 'text-warning' : 'text-muted';
echo ' <i class="ti ti-star-filled ' . esc_attr($cls) . '"></i>';
}
echo ' <span class="small ms-1">' . esc_html(number_format_i18n($avg_rating, 1)) . ' (' . esc_html(number_format_i18n($rating_count)) . ')</span>';
echo ' </div>';
echo ' <div class="d-flex justify-content-between align-items-center">';
// Prices: show offer price (highlighted) with base price crossed-out when applicable
$base_amount = ($price !== '' ? (float)$price : 0);
$offer_amount = ($offer !== '' ? (float)$offer : 0);
if ($offer_amount > 0 || $base_amount > 0){
$sym = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
if ($offer_amount > 0){
if (function_exists('wc_price')){
$offer_formatted = wp_kses_post(wc_price($offer_amount));
$base_formatted = ($base_amount > 0 ? wp_kses_post(wc_price($base_amount)) : '');
} else {
$offer_formatted = esc_html($sym . number_format_i18n($offer_amount, 2));
$base_formatted = ($base_amount > 0 ? esc_html($sym . number_format_i18n($base_amount, 2)) : '');
}
echo ' <span class="price text-danger fw-bold">' . $offer_formatted . '</span>';
if ($base_amount > 0 && $base_amount !== $offer_amount){
echo ' <span class="text-muted text-decoration-line-through small ms-1">' . $base_formatted . '</span>';
}
} else {
if (function_exists('wc_price')){
$base_formatted = wp_kses_post(wc_price($base_amount));
} else {
$base_formatted = esc_html($sym . number_format_i18n($base_amount, 2));
}
echo ' <span class="price fw-bold">' . $base_formatted . '</span>';
}
} else {
echo ' <span class="price"> </span>';
}
echo ' <button type="button" class="btn btn-sm btn-dark ds-add-to-book" data-service-id="' . (int)$sid . '"><i class="ti ti-plus"></i></button>';
echo ' </div>';
echo ' </div>';
echo ' </div>';
echo '</div>';
echo '</div>';
}
wp_reset_postdata();
} else {
echo '<div class="col-12"><em>' . esc_html__('No services found for this category.', 'dreamsalon-core') . '</em></div>';
}
echo '</div>';
$html = ob_get_clean();
wp_send_json_success(['html' => $html]);
}
// ===== Single Service: Submit Review =====
add_action('wp_ajax_ds_submit_review', 'ds_ajax_submit_review');
add_action('wp_ajax_nopriv_ds_submit_review', 'ds_ajax_submit_review');
function ds_ajax_submit_review(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
$post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
$rating = isset($_POST['rating']) ? (int) $_POST['rating'] : 0;
$content = isset($_POST['content']) ? sanitize_textarea_field(wp_unslash($_POST['content'])) : '';
if (! $post_id || $content === '' || $rating < 1 || $rating > 5){
wp_send_json_error(['message' => __('Please provide rating and review content.', 'dreamsalon-core')], 400);
}
$post = get_post($post_id);
if (! $post || $post->post_type !== 'service'){
wp_send_json_error(['message' => __('Invalid service.', 'dreamsalon-core')], 404);
}
if (is_user_logged_in()){
$user = wp_get_current_user();
$author = $user->display_name ?: $user->user_login;
$email = $user->user_email;
$user_id = (int) $user->ID;
} else {
$author = isset($_POST['author']) ? sanitize_text_field($_POST['author']) : '';
$email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
$user_id = 0;
}
// Ensure comments are open
if ('open' !== get_post_field('comment_status', $post_id)){
wp_update_post(['ID' => $post_id, 'comment_status' => 'open']);
}
$commentdata = array(
'comment_post_ID' => $post_id,
'comment_content' => $content,
'comment_author' => $author,
'comment_author_email' => $email,
'user_id' => $user_id,
'comment_approved' => 1,
);
$comment_id = wp_insert_comment($commentdata);
if (! $comment_id || is_wp_error($comment_id)){
wp_send_json_error(['message' => __('Failed to submit review. Please try again.', 'dreamsalon-core')], 500);
}
if ($rating > 0){
add_comment_meta($comment_id, 'rating', $rating, true);
}
wp_send_json_success(['message' => __('Review submitted successfully.', 'dreamsalon-core')]);
}
// ===== Reviews: Update, Delete, and Reply (AJAX) =====
add_action('wp_ajax_ds_update_review', 'ds_ajax_update_review');
function ds_ajax_update_review(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
$comment_id = isset($_POST['comment_id']) ? (int) $_POST['comment_id'] : 0;
$content = isset($_POST['content']) ? sanitize_textarea_field(wp_unslash($_POST['content'])) : '';
$rating = isset($_POST['rating']) ? (int) $_POST['rating'] : 0;
if (! $comment_id || $content === ''){
wp_send_json_error(['message' => __('Missing fields.', 'dreamsalon-core')], 400);
}
$comment = get_comment($comment_id);
if (! $comment){
wp_send_json_error(['message' => __('Comment not found.', 'dreamsalon-core')], 404);
}
$user = wp_get_current_user();
$can_edit = ((int)$comment->user_id === (int)$user->ID) || current_user_can('edit_comment', $comment_id) || current_user_can('moderate_comments');
if (! $can_edit){
wp_send_json_error(['message' => __('You do not have permission to edit this review.', 'dreamsalon-core')], 403);
}
if($comment_id){
wp_update_comment([
'comment_ID' => $comment_id,
'comment_content' => $content,
]);
} else {
wp_send_json_error(['message' => __('Failed to update review.', 'dreamsalon-core')], 500);
}
if($content) {
wp_update_comment([
]);
}
if ($rating >= 1 && $rating <= 5){
update_comment_meta($comment_id, 'rating', $rating);
} else {
wp_send_json_error(['message' => __('Please select a rating between 1 and 5.', 'dreamsalon-core')], 500);
}
wp_send_json_success(['message' => __('Review updated.', 'dreamsalon-core')]);
}
add_action('wp_ajax_ds_delete_review', 'ds_ajax_delete_review');
function ds_ajax_delete_review(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
$comment_id = isset($_POST['comment_id']) ? (int) $_POST['comment_id'] : 0;
if (! $comment_id){
wp_send_json_error(['message' => __('Missing comment.', 'dreamsalon-core')], 400);
}
$comment = get_comment($comment_id);
if (! $comment){
wp_send_json_error(['message' => __('Comment not found.', 'dreamsalon-core')], 404);
}
$user = wp_get_current_user();
$can_delete = ((int)$comment->user_id === (int)$user->ID) || current_user_can('edit_comment', $comment_id) || current_user_can('moderate_comments');
if (! $can_delete){
wp_send_json_error(['message' => __('You do not have permission to delete this review.', 'dreamsalon-core')], 403);
}
$deleted = wp_trash_comment($comment_id);
if (is_wp_error($deleted) || ! $deleted){
$deleted = wp_delete_comment($comment_id, true);
}
if (! $deleted){
wp_send_json_error(['message' => __('Failed to delete review.', 'dreamsalon-core')], 500);
}
wp_send_json_success(['message' => __('Review deleted.', 'dreamsalon-core')]);
}
add_action('wp_ajax_ds_reply_review', 'ds_ajax_reply_review');
function ds_ajax_reply_review(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
$parent_id = isset($_POST['parent_comment_id']) ? (int) $_POST['parent_comment_id'] : 0;
$post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
$content = isset($_POST['content']) ? sanitize_textarea_field(wp_unslash($_POST['content'])) : '';
if (! $parent_id || ! $post_id || $content === ''){
wp_send_json_error(['message' => __('Missing fields.', 'dreamsalon-core')], 400);
}
$post = get_post($post_id);
if (! $post || $post->post_type !== 'service'){
wp_send_json_error(['message' => __('Invalid service.', 'dreamsalon-core')], 404);
}
$user = wp_get_current_user();
$is_owner = ((int)$post->post_author === (int)$user->ID);
$can_moderate = current_user_can('moderate_comments');
if (! $is_owner && ! $can_moderate){
wp_send_json_error(['message' => __('You are not allowed to reply to this review.', 'dreamsalon-core')], 403);
}
if ('open' !== get_post_field('comment_status', $post_id)){
wp_update_post(['ID' => $post_id, 'comment_status' => 'open']);
}
$author = $user->display_name ?: $user->user_login;
$email = $user->user_email ?: '';
$commentdata = array(
'comment_post_ID' => $post_id,
'comment_parent' => $parent_id,
'comment_content' => $content,
'user_id' => (int) $user->ID,
'comment_author' => $author,
'comment_author_email' => $email,
'comment_approved' => 1,
);
$new_id = wp_insert_comment($commentdata);
if (! $new_id || is_wp_error($new_id)){
wp_send_json_error(['message' => __('Failed to post reply.', 'dreamsalon-core')], 500);
}
wp_send_json_success(['message' => __('Reply posted.', 'dreamsalon-core')]);
}
// ===== WooCommerce: Add appointments to real WC cart =====
add_action('wp_ajax_ds_wc_add_cart', 'ds_wc_add_cart');
add_action('wp_ajax_nopriv_ds_wc_add_cart', 'ds_wc_add_cart');
function ds_wc_add_cart(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! function_exists('WC') || ! WC()->cart){
wp_send_json_error(['message' => 'WooCommerce cart unavailable'], 500);
}
$raw = isset($_POST['cart']) ? wp_unslash($_POST['cart']) : '';
$cart = json_decode($raw, true);
if (! is_array($cart) || empty($cart)){
wp_send_json_error(['message' => $cart], 400);
}
// Try to map service to product. Prefer per-service linked product ID.
// Fallback: a product with SKU "service-appointment".
$fallback_product_id = 0;
if (function_exists('wc_get_product_id_by_sku')){
$pid = wc_get_product_id_by_sku('service-appointment');
if ($pid){ $fallback_product_id = (int) $pid; }
}
// Collect service IDs already present in the WooCommerce cart
$existing_service_ids = array();
foreach (WC()->cart->get_cart() as $cart_item) {
if (isset($cart_item['ds_service']['service_id'])) {
$existing_service_ids[] = (int) $cart_item['ds_service']['service_id'];
}
}
$added_any = false;
foreach ($cart as $key => $item){
$sid = isset($item['serviceId']) ? (int) $item['serviceId'] : 0;
if (! $sid) continue;
// Skip if this service is already in the WooCommerce cart
if (in_array($sid, $existing_service_ids, true)) {
continue;
}
$post = get_post($sid);
if (! $post || $post->post_type !== 'service') continue;
$linked_product_id = (int) get_post_meta($sid, '_linked_product_id', true);
$product_id = 0;
if ($linked_product_id){
$product_id = $linked_product_id;
} elseif (function_exists('wc_get_product_id_by_sku')) {
$by_sku = wc_get_product_id_by_sku('service-' . $sid);
if ($by_sku){ $product_id = (int) $by_sku; }
}
if (! $product_id){ $product_id = $fallback_product_id; }
if (! $product_id){
wp_send_json_error(['message' => 'Missing product to add to cart. Create a WooCommerce product with SKU "service-appointment" or set meta linked_product_id on the service.'], 400);
}
$title = get_the_title($sid);
if ($title !== ''){
if (function_exists('mb_convert_case')) { $title = mb_convert_case($title, MB_CASE_TITLE, 'UTF-8'); }
else { $title = ucwords(strtolower($title)); }
}
$price = get_post_meta($sid, 'service_offer_price', true);
if ($price === '' || $price === null){ $price = get_post_meta($sid, 'service_price', true); }
$price = is_numeric($price) ? (float) $price : 0.0;
$dur_h = (int) get_post_meta($sid, 'service_duration_hours', true);
$dur_m = (int) get_post_meta($sid, 'service_duration_minutes', true);
$dur_min = ($dur_h * 60) + $dur_m;
$addons = array();
if (!empty($item['addonsCustom']) && is_array($item['addonsCustom'])){
foreach ($item['addonsCustom'] as $a){
$ttl = isset($a['title']) ? sanitize_text_field($a['title']) : '';
$apr = isset($a['price']) ? (float) preg_replace('/[^0-9.\-]/','', (string)$a['price']) : 0.0;
$ah = isset($a['dur_h']) ? (int)$a['dur_h'] : 0;
$am = isset($a['dur_m']) ? (int)$a['dur_m'] : 0;
$addons[] = array(
'title' => $ttl,
'price' => $apr,
'dur_h' => $ah,
'dur_m' => $am,
);
}
}
$addons_total_price = 0.0; $addons_total_min = 0;
foreach ($addons as $a){
$addons_total_price += (float) $a['price'];
$addons_total_min += ((int)$a['dur_h'] * 60 + (int)$a['dur_m']);
}
$meta = isset($item['meta']) && is_array($item['meta']) ? $item['meta'] : array();
$thumb = isset($meta['thumb']) ? esc_url_raw($meta['thumb']) : '';
$staff = isset($item['staff']) ? (int) $item['staff'] : 0;
$date = isset($item['date']) ? sanitize_text_field($item['date']) : '';
$slot = isset($item['slot']) ? sanitize_text_field($item['slot']) : '';
$amen = isset($item['amenities']) && is_array($item['amenities']) ? array_map('intval', $item['amenities']) : array();
$branch_id = isset($item['branchId']) ? (int) $item['branchId'] : 0;
$branch_title = isset($item['branchTitle']) ? sanitize_text_field($item['branchTitle']) : '';
$branch_location = isset($item['branchLocation']) ? sanitize_text_field($item['branchLocation']) : '';
$cart_item_data = array(
'ds_service' => array(
'service_id' => $sid,
'service_title' => $title,
'base_price' => $price,
'base_dur_min' => $dur_min,
'addons' => $addons,
'addons_price' => $addons_total_price,
'addons_dur_min' => $addons_total_min,
'staff' => $staff,
'date' => $date,
'slot' => $slot,
'amenities' => $amen,
'thumb' => $thumb,
'branch_id' => $branch_id,
'branch_title' => $branch_title,
'branch_location' => $branch_location,
)
);
$added = WC()->cart->add_to_cart($product_id, 1, 0, array(), $cart_item_data);
if ($added){
$added_any = true;
// Track this service as now present in the cart to avoid adding duplicates in this request
$existing_service_ids[] = $sid;
}
}
if (! $added_any){
wp_send_json_error(['message' => 'Nothing added.'], 400);
}
wp_send_json_success(['message' => 'Added to cart']);
}
function ds_validate_service_cart_items($cart = null){
if (! function_exists('WC')){
return ['valid' => true, 'errors' => []];
}
if ($cart === null && WC()->cart){
$cart = WC()->cart;
}
if (! $cart || ! method_exists($cart, 'get_cart')){
return ['valid' => true, 'errors' => []];
}
$errors = [];
foreach ($cart->get_cart() as $cart_item){
if (empty($cart_item['ds_service']) || ! is_array($cart_item['ds_service'])){
continue;
}
$ds = $cart_item['ds_service'];
$sid = isset($ds['service_id']) ? (int) $ds['service_id'] : 0;
$date = isset($ds['date']) ? sanitize_text_field($ds['date']) : '';
$slot = isset($ds['slot']) ? sanitize_text_field($ds['slot']) : '';
$staff = isset($ds['staff']) ? (int) $ds['staff'] : 0;
$booking_url = ds_get_booking_page_url($sid);
$edit_link = $booking_url ? sprintf(
' <a class="ds-edit-selection" href="%s">%s</a>',
esc_url($booking_url),
esc_html__('Edit selection', 'dreamsalon-core')
) : '';
if (! $sid || $date === '' || $slot === ''){
$errors[] = __('One of your appointments is missing a date or time slot. Please reselect it before placing the order.', 'dreamsalon-core') . $edit_link;
continue;
}
$post = get_post($sid);
if (! $post || $post->post_type !== 'service'){
$errors[] = __('A selected service is no longer available. Please remove it from your cart.', 'dreamsalon-core') . $edit_link;
continue;
}
$available = ds_get_available_slots_for_service_date($sid, $date, $staff, $post);
if (! in_array($slot, $available, true)){
$title = get_the_title($sid);
if ($title !== ''){
$title = function_exists('mb_convert_case') ? mb_convert_case($title, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($title));
} else {
$title = __('service', 'dreamsalon-core');
}
$errors[] = sprintf(
__('The slot %1$s on %2$s for %3$s is no longer available. Please pick a different slot.', 'dreamsalon-core'),
$slot,
$date,
$title
) . $edit_link;
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
];
}
add_action('wp_ajax_ds_validate_cart_slots', 'ds_ajax_validate_cart_slots');
add_action('wp_ajax_nopriv_ds_validate_cart_slots', 'ds_ajax_validate_cart_slots');
function ds_ajax_validate_cart_slots(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
$result = ds_validate_service_cart_items();
if ($result['valid']){
wp_send_json_success();
}
$message = implode(' ', array_map('wp_strip_all_tags', $result['errors']));
if ($message === ''){
$message = __('Selected slots are no longer available. Please update your booking.', 'dreamsalon-core');
}
wp_send_json_error(['message' => $message]);
}
add_action('woocommerce_checkout_process', 'ds_checkout_validate_service_slots');
function ds_checkout_validate_service_slots(){
if (! function_exists('WC')){
return;
}
$result = ds_validate_service_cart_items();
if ($result['valid']){
return;
}
foreach ($result['errors'] as $error){
if (function_exists('wc_add_notice')){
wc_add_notice($error, 'error');
}
}
}
// Display service meta under cart line items
add_filter('woocommerce_get_item_data', function($item_data, $cart_item){
if (isset($cart_item['ds_service']) && is_array($cart_item['ds_service'])){
$ds = $cart_item['ds_service'];
$sid = isset($ds['service_id']) ? (int) $ds['service_id'] : 0;
// Service
if (!empty($ds['service_title'])){
$svc = sanitize_text_field($ds['service_title']);
if ($svc !== ''){ $svc = function_exists('mb_convert_case') ? mb_convert_case($svc, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($svc)); }
$item_data[] = array('name' => __('Service','dreamsalon-core'), 'value' => $svc);
}
// Date / Time
if (!empty($ds['date'])){
$item_data[] = array('name' => __('Date','dreamsalon-core'), 'value' => sanitize_text_field($ds['date']));
}
if (!empty($ds['slot'])){
$item_data[] = array('name' => __('Time Slot','dreamsalon-core'), 'value' => sanitize_text_field($ds['slot']));
}
// Staff name
if (!empty($ds['staff'])){
$name = '';
$u = get_user_by('id', (int)$ds['staff']);
if ($u && !is_wp_error($u)){
$name = $u->display_name ?: ('#'.(int)$ds['staff']);
} else {
$name = '#'.(int)$ds['staff'];
}
if ($name !== ''){ $name = function_exists('mb_convert_case') ? mb_convert_case($name, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($name)); }
$item_data[] = array('name' => __('Staff','dreamsalon-core'), 'value' => sanitize_text_field($name));
}
// Branch (selected)
$branch_label = '';
if (!empty($ds['branch_title'])){
$branch_label = sanitize_text_field($ds['branch_title']);
} elseif ($sid){
$branches = wp_get_object_terms($sid, 'service_branch');
if (!is_wp_error($branches) && !empty($branches)){
$branch_label = $branches[0]->name;
}
}
if ($branch_label !== ''){
if (function_exists('mb_convert_case')){ $branch_label = mb_convert_case($branch_label, MB_CASE_TITLE, 'UTF-8'); }
else { $branch_label = ucwords(strtolower($branch_label)); }
$item_data[] = array('name' => __('Branch','dreamsalon-core'), 'value' => sanitize_text_field($branch_label));
}
// Category (first)
if ($sid){
$cats = wp_get_object_terms($sid, 'service_category');
if (!is_wp_error($cats) && !empty($cats)){
$cn = $cats[0]->name;
if ($cn !== ''){ $cn = function_exists('mb_convert_case') ? mb_convert_case($cn, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($cn)); }
$item_data[] = array('name' => __('Category','dreamsalon-core'), 'value' => sanitize_text_field($cn));
}
}
// Add-ons list
if (!empty($ds['addons']) && is_array($ds['addons'])){
$names = array();
foreach ($ds['addons'] as $a){
$t = isset($a['title']) ? sanitize_text_field($a['title']) : '';
if ($t !== ''){ $t = function_exists('mb_convert_case') ? mb_convert_case($t, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($t)); }
if ($t !== '') $names[] = $t;
}
if (!empty($names)){
$item_data[] = array('name' => __('Add-ons','dreamsalon-core'), 'value' => implode(', ', $names));
}
}
}
return $item_data;
}, 10, 2);
// Adjust cart item price to include base + addons
add_action('woocommerce_before_calculate_totals', function($cart){
if (is_admin() && ! defined('DOING_AJAX')) return;
if (empty($cart) || ! isset($cart->cart_contents)) return;
foreach ($cart->get_cart() as $cart_item_key => $cart_item){
if (isset($cart_item['ds_service']) && is_array($cart_item['ds_service'])){
$ds = $cart_item['ds_service'];
$base = isset($ds['base_price']) ? (float) $ds['base_price'] : 0.0;
$addons = isset($ds['addons_price']) ? (float) $ds['addons_price'] : 0.0;
$price = $base + $addons;
if (isset($cart_item['data']) && is_object($cart_item['data'])){
$cart_item['data']->set_price($price);
}
}
}
}, 20, 1);
// Persist ds_service to order items
add_action('woocommerce_checkout_create_order_line_item', function($item, $cart_item_key, $values, $order){
if (isset($values['ds_service']) && is_array($values['ds_service'])){
$ds = $values['ds_service'];
if (!empty($ds['service_id'])) $item->add_meta_data('ds_service_id', (int)$ds['service_id'], true);
if (!empty($ds['service_title'])) $item->add_meta_data('ds_service_title', sanitize_text_field($ds['service_title']), true);
if (isset($ds['staff'])) $item->add_meta_data('ds_staff_id', (int)$ds['staff'], true);
if (!empty($ds['date'])) $item->add_meta_data('ds_date', sanitize_text_field($ds['date']), true);
if (!empty($ds['slot'])) $item->add_meta_data('ds_slot', sanitize_text_field($ds['slot']), true);
if (isset($ds['addons'])) $item->add_meta_data('ds_addons', wp_json_encode($ds['addons']), true);
if (isset($ds['base_price'])) $item->add_meta_data('ds_base_price', (float)$ds['base_price'], true);
if (isset($ds['addons_price'])) $item->add_meta_data('ds_addons_price', (float)$ds['addons_price'], true);
if (isset($ds['branch_id'])) $item->add_meta_data('ds_branch_id', (int)$ds['branch_id'], true);
if (!empty($ds['branch_title'])) $item->add_meta_data('ds_branch_title', sanitize_text_field($ds['branch_title']), true);
if (!empty($ds['branch_location'])) $item->add_meta_data('ds_branch_location', sanitize_text_field($ds['branch_location']), true);
}
}, 10, 4);
// Insert booking rows into custom table after order is placed
// Ensure ds_bookings table exists at runtime (in case plugin was activated before table code was added)
add_action('init', function(){
global $wpdb;
$table = $wpdb->prefix . 'ds_bookings';
$exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
if ($exists !== $table){
if (! function_exists('dbDelta')){ require_once ABSPATH . 'wp-admin/includes/upgrade.php'; }
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS `$table` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`order_item_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`product_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`service_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`customer_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`addons` longtext NULL,
`staff_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`date` date NULL,
`slot` varchar(50) NULL,
`branch_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`branch_title` varchar(190) NOT NULL DEFAULT '',
`branch_location` varchar(255) NOT NULL DEFAULT '',
`amount` decimal(18,2) NOT NULL DEFAULT 0.00,
`currency` varchar(10) NOT NULL DEFAULT '',
`payment_status` varchar(20) NOT NULL DEFAULT '',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `order_id` (`order_id`),
KEY `service_id` (`service_id`),
KEY `customer_id` (`customer_id`)
) $charset_collate;";
dbDelta($sql);
}
});
// Helper to upsert Woo order items into ds_bookings
if (! function_exists('ds_upsert_order_into_ds_bookings')){
function ds_upsert_order_into_ds_bookings($order_id){
if (! $order_id) return;
if (! function_exists('wc_get_order')) return;
$order = wc_get_order($order_id);
if (! $order) return;
global $wpdb;
$table = $wpdb->prefix . 'ds_bookings';
$customer_id = (int) $order->get_user_id();
$currency = method_exists($order,'get_currency') ? $order->get_currency() : '';
$payment_status = method_exists($order,'get_status') ? $order->get_status() : '';
foreach ($order->get_items() as $item_id => $item){
$product_id = (int) $item->get_product_id();
$service_id = (int) $item->get_meta('ds_service_id', true);
if (! $service_id) continue;
$staff_id = (int) $item->get_meta('ds_staff_id', true);
$date = $item->get_meta('ds_date', true);
$slot = $item->get_meta('ds_slot', true);
$addons_json = $item->get_meta('ds_addons', true);
$branch_id = (int) $item->get_meta('ds_branch_id', true);
$branch_title = $item->get_meta('ds_branch_title', true);
$branch_location = $item->get_meta('ds_branch_location', true);
$base_price = (float) $item->get_meta('ds_base_price', true);
$addons_price = (float) $item->get_meta('ds_addons_price', true);
$line_total = (float) $item->get_total();
$amount = $line_total > 0 ? $line_total : ($base_price + $addons_price);
$exists = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE order_item_id = %d", $item_id));
if ($exists){
$wpdb->update($table, [
'order_id' => (int)$order_id,
'product_id' => $product_id,
'service_id' => $service_id,
'customer_id' => $customer_id,
'addons' => $addons_json,
'staff_id' => $staff_id,
'date' => $date ?: null,
'slot' => $slot ?: null,
'branch_id' => $branch_id,
'branch_title' => $branch_title,
'branch_location' => $branch_location,
'amount' => $amount,
'currency' => $currency,
'payment_status' => $payment_status,
], [ 'order_item_id' => (int)$item_id ], [
'%d','%d','%d','%d','%s','%d','%s','%s','%d','%s','%s','%f','%s','%s'
], [ '%d' ]);
} else {
$wpdb->insert($table, [
'order_id' => (int)$order_id,
'order_item_id' => (int)$item_id,
'product_id' => $product_id,
'service_id' => $service_id,
'customer_id' => $customer_id,
'addons' => $addons_json,
'staff_id' => $staff_id,
'date' => $date ?: null,
'slot' => $slot ?: null,
'branch_id' => $branch_id,
'branch_title' => $branch_title,
'branch_location' => $branch_location,
'amount' => $amount,
'currency' => $currency,
'payment_status' => $payment_status,
], [
'%d','%d','%d','%d','%d','%s','%d','%s','%s','%d','%s','%s','%f','%s','%s'
]);
}
}
}
}
// Primary hook after checkout redirect
add_action('woocommerce_thankyou', function($order_id){ ds_upsert_order_into_ds_bookings($order_id); }, 10, 1);
// Fallback hook: when order status changes (for gateways that skip thankyou)
add_action('woocommerce_order_status_changed', function($order_id){ ds_upsert_order_into_ds_bookings($order_id); }, 10, 1);
// Ensure ds_enquiries table exists at runtime
add_action('init', function(){
global $wpdb;
$table = $wpdb->prefix . 'ds_enquiries';
$exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
if ($exists !== $table){
if (! function_exists('dbDelta')){ require_once ABSPATH . 'wp-admin/includes/upgrade.php'; }
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS `$table` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`service_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`parent_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`user_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`name` varchar(190) NOT NULL DEFAULT '',
`email` varchar(190) NOT NULL DEFAULT '',
`phone` varchar(50) NOT NULL DEFAULT '',
`message` longtext NULL,
`status` varchar(20) NOT NULL DEFAULT 'open',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `service_id` (`service_id`),
KEY `parent_id` (`parent_id`),
KEY `user_id` (`user_id`)
) $charset_collate;";
dbDelta($sql);
}
});
// ===== Single Service: Submit Enquiry =====
add_action('wp_ajax_ds_submit_enquiry', 'ds_ajax_submit_enquiry');
add_action('wp_ajax_nopriv_ds_submit_enquiry', 'ds_ajax_submit_enquiry');
function ds_ajax_submit_enquiry(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
global $wpdb;
$table = $wpdb->prefix . 'ds_enquiries';
$service_id = isset($_POST['service_id']) ? (int) $_POST['service_id'] : 0;
$name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
$email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
$phone = isset($_POST['phone']) ? sanitize_text_field(wp_unslash($_POST['phone'])) : '';
$message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
if (! $service_id || $message === '' || $name === '' || ! is_email($email)){
wp_send_json_error(['message' => __('Please fill all required fields.', 'dreamsalon-core')], 400);
}
$post = get_post($service_id);
if (! $post || $post->post_type !== 'service'){
wp_send_json_error(['message' => __('Invalid service.', 'dreamsalon-core')], 404);
}
$user_id = is_user_logged_in() ? (int) get_current_user_id() : 0;
$inserted = $wpdb->insert(
$table,
[
'service_id' => $service_id,
'parent_id' => 0,
'user_id' => $user_id,
'name' => $name,
'email' => $email,
'phone' => $phone,
'message' => $message,
'status' => 'open',
'created_at' => current_time('mysql'),
],
['%d','%d','%d','%s','%s','%s','%s','%s','%s']
);
if (false === $inserted){
wp_send_json_error(['message' => __('Failed to submit enquiry. Please try again.', 'dreamsalon-core')], 500);
}
wp_send_json_success(['message' => __('Enquiry submitted successfully.', 'dreamsalon-core')]);
}
// ===== Enquiry: Submit Reply (threaded conversation) =====
add_action('wp_ajax_ds_submit_enquiry_reply', 'ds_ajax_submit_enquiry_reply');
add_action('wp_ajax_ds_reply_enquiry', 'ds_ajax_submit_enquiry_reply');
function ds_ajax_submit_enquiry_reply(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
global $wpdb;
$table = $wpdb->prefix . 'ds_enquiries';
$parent_id = isset($_POST['enquiry_id']) ? (int) $_POST['enquiry_id'] : 0;
$message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
if (! $parent_id || $message === ''){
wp_send_json_error(['message' => __('Missing fields.', 'dreamsalon-core')], 400);
}
// Load parent to get service_id
$parent = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $parent_id));
if (! $parent){
wp_send_json_error(['message' => __('Enquiry not found.', 'dreamsalon-core')], 404);
}
$user = wp_get_current_user();
$user_id = (int) $user->ID;
$name = $user->display_name ?: $user->user_login;
$email = $user->user_email ?: '';
// Permission: only service author, editors, or admins
$service_post = get_post( (int) $parent->service_id );
if (! $service_post || $service_post->post_type !== 'service'){
wp_send_json_error(['message' => __('Invalid service.', 'dreamsalon-core')], 404);
}
$can_reply = ((int)$service_post->post_author === $user_id) || current_user_can('edit_post', $service_post->ID) || current_user_can('manage_options');
if (! $can_reply){
wp_send_json_error(['message' => __('You do not have permission to reply to this enquiry.', 'dreamsalon-core')], 403);
}
$inserted = $wpdb->insert(
$table,
[
'service_id' => (int) $parent->service_id,
'parent_id' => $parent_id,
'user_id' => $user_id,
'name' => $name,
'email' => $email,
'phone' => '',
'message' => $message,
'status' => 'open',
'created_at' => current_time('mysql'),
],
['%d','%d','%d','%s','%s','%s','%s','%s','%s']
);
if (false === $inserted){
wp_send_json_error(['message' => __('Failed to save reply. Please try again.', 'dreamsalon-core')], 500);
}
// Email notify original enquirer if email exists
$to = ! empty($parent->email) && is_email($parent->email) ? $parent->email : '';
if ($to){
$service = get_post((int) $parent->service_id);
$service_title = $service ? get_the_title($service) : __('Service','dreamsalon-core');
$blogname = wp_specialchars_decode( get_option('blogname'), ENT_QUOTES );
$subject = sprintf( __('Reply to your enquiry - %s', 'dreamsalon-core'), $service_title );
$body = '<p>' . sprintf( __('Hello %s,', 'dreamsalon-core'), esc_html($parent->name) ) . '</p>';
$body .= '<p>' . __('You have received a reply to your enquiry:', 'dreamsalon-core') . '</p>';
$body .= '<blockquote>' . nl2br( esc_html($message) ) . '</blockquote>';
$link = get_permalink( (int) $parent->service_id );
if ($link){ $body .= '<p><a href="' . esc_url($link) . '">' . __('View Service', 'dreamsalon-core') . '</a></p>'; }
$body .= '<p>' . sprintf( __('Regards, %s', 'dreamsalon-core'), esc_html($blogname) ) . '</p>';
$headers = array('Content-Type: text/html; charset=UTF-8');
wp_mail($to, $subject, $body, $headers);
}
wp_send_json_success(['message' => __('Reply posted.', 'dreamsalon-core')]);
}
// ===== Enquiry: Get Thread (root + replies) =====
add_action('wp_ajax_ds_get_enquiry_thread', 'ds_ajax_get_enquiry_thread');
function ds_ajax_get_enquiry_thread(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
global $wpdb;
$table = $wpdb->prefix . 'ds_enquiries';
$enquiry_id = isset($_POST['enquiry_id']) ? (int) $_POST['enquiry_id'] : 0;
if (! $enquiry_id){
wp_send_json_error(['message' => __('Missing enquiry id.', 'dreamsalon-core')], 400);
}
$root = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $enquiry_id));
if (! $root){
wp_send_json_error(['message' => __('Enquiry not found.', 'dreamsalon-core')], 404);
}
// Permission: only service author, editors, or admins can view
$user = wp_get_current_user();
$service_post = get_post( (int) $root->service_id );
if (! $service_post || $service_post->post_type !== 'service'){
wp_send_json_error(['message' => __('Invalid service.', 'dreamsalon-core')], 404);
}
$can_view = ((int)$service_post->post_author === (int)$user->ID) || current_user_can('edit_post', $service_post->ID) || current_user_can('manage_options');
if (! $can_view){
wp_send_json_error(['message' => __('You do not have permission to view this enquiry.', 'dreamsalon-core')], 403);
}
$replies = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE parent_id = %d ORDER BY created_at ASC", $enquiry_id));
$fmt = function($row){
$ts = strtotime($row->created_at);
$name = (string) ($row->name ?: '');
if (function_exists('dreamsalon_sentencecase')){ $name = dreamsalon_sentencecase($name); }
return array(
'id' => (int) $row->id,
'name' => $name,
'email' => (string) ($row->email ?: ''),
'created' => $ts ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $ts) : '',
'message' => (string) ($row->message ?: ''),
'status' => (string) ($row->status ?: 'open'),
);
};
$payload = array(
'root' => $fmt($root),
'replies' => array_map($fmt, $replies ?: array()),
);
wp_send_json_success($payload);
}
// ===== Enquiry: Set Status (open/closed) =====
add_action('wp_ajax_ds_set_enquiry_status', 'ds_ajax_set_enquiry_status');
function ds_ajax_set_enquiry_status(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
global $wpdb; $table = $wpdb->prefix . 'ds_enquiries';
$enquiry_id = isset($_POST['enquiry_id']) ? (int) $_POST['enquiry_id'] : 0;
$status = isset($_POST['status']) ? sanitize_text_field(wp_unslash($_POST['status'])) : '';
if (! $enquiry_id || ! in_array($status, array('open','closed'), true)){
wp_send_json_error(['message' => __('Invalid parameters.', 'dreamsalon-core')], 400);
}
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $enquiry_id));
if (! $row){
wp_send_json_error(['message' => __('Enquiry not found.', 'dreamsalon-core')], 404);
}
$user = wp_get_current_user();
$post = get_post( (int) $row->service_id );
$can = $post && ($post->post_type==='service') && ( (int)$post->post_author === (int)$user->ID || current_user_can('edit_post', $post->ID) || current_user_can('manage_options') );
if (! $can){
wp_send_json_error(['message' => __('Permission denied.', 'dreamsalon-core')], 403);
}
$ok = $wpdb->update($table, array('status' => $status), array('id' => (int)$enquiry_id), array('%s'), array('%d'));
if ($ok === false){
wp_send_json_error(['message' => __('Failed to update status.', 'dreamsalon-core')], 500);
}
wp_send_json_success(array('status' => $status));
}
// AJAX: fetch bookings list for dashboard
add_action('wp_ajax_ds_get_bookings', function(){
if (! current_user_can('manage_options') && ! current_user_can('agent')){
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
global $wpdb;
$table = $wpdb->prefix . 'ds_bookings';
$rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY id DESC LIMIT 200", ARRAY_A);
wp_send_json_success(['rows' => $rows]);
});
function ds_get_available_slots_for_service_date($service_id, $date, $staff_id = 0, $post = null) {
$service_id = (int) $service_id;
$staff_id = (int) $staff_id;
$date = sanitize_text_field($date);
if (! $service_id || $date === '') {
return array();
}
if (! $post) {
$post = get_post($service_id);
}
if (! $post || $post->post_type !== 'service') {
return array();
}
$ts = strtotime($date);
if (! $ts) {
return array();
}
$weekday_idx = (int) date('N', $ts); // 1 (Mon) .. 7 (Sun)
$keys = [1=>'mon',2=>'tue',3=>'wed',4=>'thu',5=>'fri',6=>'sat',7=>'sun'];
$wkey = isset($keys[$weekday_idx]) ? $keys[$weekday_idx] : 'mon';
$slots_cfg = get_post_meta($service_id, 'service_slots_config', true);
$slots = array();
if (is_array($slots_cfg) && !empty($slots_cfg[$wkey]) && !empty($slots_cfg[$wkey]['time_slots'])) {
$slots = array_values(array_map('sanitize_text_field', (array) $slots_cfg[$wkey]['time_slots']));
} else {
$slots = (array) get_post_meta($service_id, 'service_slots_times', true);
$slots = array_values(array_map('sanitize_text_field', $slots));
}
global $wpdb;
$table = $wpdb->prefix . 'ds_bookings';
$block_statuses = array('pending','processing','on-hold','completed');
$placeholders = implode(',', array_fill(0, count($block_statuses), '%s'));
if ($staff_id > 0) {
$query = $wpdb->prepare(
"SELECT slot FROM {$table} WHERE service_id = %d AND date = %s AND staff_id = %d AND payment_status IN ($placeholders)",
array_merge(array($service_id, $date, $staff_id), $block_statuses)
);
} else {
$query = $wpdb->prepare(
"SELECT slot FROM {$table} WHERE service_id = %d AND date = %s AND payment_status IN ($placeholders)",
array_merge(array($service_id, $date), $block_statuses)
);
}
$booked = $wpdb->get_col($query);
if (!is_wp_error($booked) && !empty($booked)) {
$booked = array_map('sanitize_text_field', (array) $booked);
$slots = array_values(array_diff($slots, $booked));
}
$today = current_time('Y-m-d');
if ($date === $today && !empty($slots)) {
$now_ts = current_time('timestamp');
$current_minutes = (int) date('G', $now_ts) * 60 + (int) date('i', $now_ts);
$filtered = array();
foreach ($slots as $slot) {
$raw = trim((string) $slot);
if ($raw === '') {
continue;
}
$norm = preg_replace('/\s+to\s+/i', '-', $raw);
if (!preg_match('/(\d{1,2})(?::(\d{2}))?\s*(AM|PM)?/i', $norm, $m)) {
$filtered[] = $slot;
continue;
}
$h = (int) $m[1];
$min = isset($m[2]) ? (int) $m[2] : 0;
$ampm = isset($m[3]) ? strtoupper($m[3]) : '';
if ($ampm === 'PM' && $h < 12) {
$h += 12;
} elseif ($ampm === 'AM' && $h === 12) {
$h = 0;
}
$slot_minutes = $h * 60 + $min;
if ($slot_minutes > $current_minutes) {
$filtered[] = $slot;
}
}
$slots = $filtered;
}
return $slots;
}
// ===== Frontend Booking: Get slots for a specific date =====
add_action('wp_ajax_ds_get_service_slots_for_date', 'ds_ajax_get_service_slots_for_date');
add_action('wp_ajax_nopriv_ds_get_service_slots_for_date', 'ds_ajax_get_service_slots_for_date');
function ds_ajax_get_service_slots_for_date(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
$sid = isset($_POST['service_id']) ? (int) $_POST['service_id'] : 0;
$date = isset($_POST['date']) ? sanitize_text_field($_POST['date']) : '';
$staff_id = isset($_POST['staff_id']) ? (int) $_POST['staff_id'] : 0;
if (! $sid || ! $date){
wp_send_json_error(['message' => 'Missing parameters'], 400);
}
$post = get_post($sid);
if (! $post || $post->post_type !== 'service'){
wp_send_json_error(['message' => 'Not found'], 404);
}
$ts = strtotime($date);
if (! $ts){
wp_send_json_error(['message' => 'Invalid date'], 400);
}
$slots_data = ds_resolve_service_slots_for_date($sid, $date, $staff_id, $post);
$slots = $slots_data['slots'];
$assigned_staff_id = $slots_data['assigned_staff_id'];
$next_available_date = '';
if (empty($slots)) {
$ts = strtotime($date);
if ($ts) {
for ($i = 1; $i <= 30; $i++) {
$candidate_ts = strtotime('+' . $i . ' days', $ts);
if (! $candidate_ts) {
continue;
}
$candidate_date = date('Y-m-d', $candidate_ts);
$candidate_data = ds_resolve_service_slots_for_date($sid, $candidate_date, $staff_id, $post);
if (!empty($candidate_data['slots'])) {
$next_available_date = $candidate_date;
break;
}
}
}
}
wp_send_json_success([
'slots' => $slots,
'assigned_staff_id' => $assigned_staff_id,
'next_available_date' => $next_available_date,
]);
}
// ===== Frontend Booking: Build Service Booking Drawer Content =====
add_action('wp_ajax_ds_get_service_booking', 'ds_ajax_get_service_booking');
add_action('wp_ajax_nopriv_ds_get_service_booking', 'ds_ajax_get_service_booking');
function ds_ajax_get_service_booking(){
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')){
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
$sid = isset($_POST['service_id']) ? (int) $_POST['service_id'] : 0;
$post = $sid ? get_post($sid) : null;
if (! $post || $post->post_type !== 'service' || $post->post_status !== 'publish'){
wp_send_json_error(['message' => 'Not found'], 404);
}
$title = get_the_title($sid);
$price = get_post_meta($sid, 'service_price', true);
$offer = get_post_meta($sid, 'service_offer_price', true);
$dur_h = (int) get_post_meta($sid, 'service_duration_hours', true);
$dur_m = (int) get_post_meta($sid, 'service_duration_minutes', true);
$dur_parts = [];
if ($dur_h > 0){ $dur_parts[] = $dur_h . ' Hrs'; }
if ($dur_m > 0){ $dur_parts[] = $dur_m . ' Mins'; }
$dur_text = !empty($dur_parts) ? implode(' ', $dur_parts) : '';
$dur_total_min = ($dur_h * 60) + $dur_m;
// Select display/base price (prefer offer)
$base_price = ($offer !== '' ? $offer : $price);
$currency_symbol = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
// Thumb (prefer first gallery)
$thumb = '';
$gallery_ids = get_post_meta($sid, 'service_gallery', true);
if (is_array($gallery_ids) && !empty($gallery_ids)){
$first_id = (int) reset($gallery_ids);
$img = wp_get_attachment_image_url($first_id, 'medium');
if ($img) { $thumb = $img; }
}
if (!$thumb){
$fi = get_the_post_thumbnail_url($sid, 'medium');
if ($fi) { $thumb = $fi; }
}
if (! $thumb){ $thumb = 'https://via.placeholder.com/300x200?text=Service'; }
$staff_ids = (array) get_post_meta($sid, 'service_staff_users', true);
$staff_ids = array_filter(array_map('intval', $staff_ids));
$staff_users = [];
if (!empty($staff_ids)){
// Load full user objects so we can access custom meta like staff_destination
$staff_users = get_users(['include' => $staff_ids, 'fields' => 'all']);
}
$amenity_terms = get_the_terms($sid, 'service_amenities');
if (is_wp_error($amenity_terms)) $amenity_terms = [];
$slots_flat = (array) get_post_meta($sid, 'service_slots_times', true);
ob_start();
echo '<div class="ds-booking-drawer-content" data-service-id="' . (int)$sid . '">';
echo ' <div class="border-bottom d-flex align-items-center justify-content-between pt-0 px-0 pb-3 mb-3">';
echo ' <h4 class="offcanvas-title">' . esc_html__('Service Booking', 'dreamsalon-core') . '</h5>';
echo ' <button type="button" class="btn1 btn-close btn-link text-dark p-0 ds-drawer-close"><i class="ti ti-x"></i></button>';
echo ' </div>';
// Hidden meta for JS to compute totals and cart render
echo ' <div id="ds-service-meta" class="d-none"'
. ' data-title="' . esc_attr($title) . '"'
. ' data-price="' . esc_attr($base_price) . '"'
. ' data-dur-min="' . esc_attr($dur_total_min) . '"'
. ' data-thumb="' . esc_url($thumb) . '"></div>';
echo ' <div class="btn-group w-100 nav nav-tabs tab-dark nav-justified border-0 gap-3 mb-4" role="group">';
echo '<div class="nav-item"><button type="button" class="btn btn-outline-dark w-100 active" data-step="addons">' . esc_html__('Add Ons & Staff','dreamsalon-core') . '</button></div>';
echo '<div class="nav-item"><button type="button" class="btn btn-outline-dark w-100 " data-step="datetime">' . esc_html__('Date & Time','dreamsalon-core') . '</button></div>';
echo ' </div>';
echo ' <div class="">';
echo ' <div class="mb-3 ds-step ds-step-addons">';
echo ' <h6 class="mb-2">' . esc_html__('Select Add ons', 'dreamsalon-core') . '</h6>';
// Custom Add Ons
echo ' <div class="row mb-3 g-3">';
$service_addons = (array) get_post_meta($sid, 'service_addons', true);
if (!empty($service_addons)){
foreach ($service_addons as $idx => $row){
$ttl = isset($row['title']) ? $row['title'] : '';
if ($ttl !== ''){
if (function_exists('mb_convert_case')) { $ttl = mb_convert_case($ttl, MB_CASE_TITLE, 'UTF-8'); }
else { $ttl = ucwords(strtolower($ttl)); }
}
$pr = isset($row['price']) ? $row['price'] : '';
$currency_symbol = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
$dh = isset($row['dur_h']) ? (int)$row['dur_h'] : 0;
$dm = isset($row['dur_m']) ? (int)$row['dur_m'] : 0;
$dtx = trim(($dh>0?($dh.'h '):'') . ($dm>0?($dm.'m'):'') );
$meta = esc_attr(wp_json_encode($row));
$price_label = '';
if ($pr !== ''){
if (function_exists('wc_price') && is_numeric($pr)){
$price_label = wp_strip_all_tags( wc_price((float)$pr) );
} else {
$price_label = $currency_symbol . $pr;
}
}
echo '<div class="col-6">'
. '<button type="button" class="btn w-100 text-start border ds-addon-chip" data-addon="' . $meta . '">'
. '<div class="d-flex justify-content-between flex-column">'
. '<span>' . esc_html($ttl) . '</span>'
. '<small class="text-muted">' . esc_html($price_label) . ($dtx? ' / '.$dtx : '') . '</small>'
. '</div>'
. '</button>'
. '</div>';
}
} else {
echo '<div class="col-12"><em>' . esc_html__('No custom add-ons configured for this service.', 'dreamsalon-core') . '</em></div>';
}
echo ' </div>';
echo ' <div class="mb-3">';
echo ' <div class="d-flex justify-content-between align-items-center mb-2">';
echo ' <h6 class="mb-0">' . esc_html__('Select Your Specialist', 'dreamsalon-core') . '</h6>';
echo ' <button type="button" class="btn btn-sm btn-light ds-auto-assign">' . esc_html__('Auto Assign','dreamsalon-core') . '</button>';
echo ' </div>';
echo ' <div class="row g-3">';
if (!empty($staff_users)){
foreach ($staff_users as $u){
$avatar = get_avatar_url($u->ID, ['size'=>64]);
$dn = $u->display_name;
if ($dn !== ''){
if (function_exists('mb_convert_case')) { $dn = mb_convert_case($dn, MB_CASE_TITLE, 'UTF-8'); }
else { $dn = ucwords(strtolower($dn)); }
}
// Use custom destination meta (with fallback label)
$dest = get_user_meta($u->ID, 'staff_destination', true);
if ($dest === '') {
$dest = esc_html__('Senior Stylist', 'dreamsalon-core');
}
echo '<div class="col-6 staff-name-booking"><button type="button" class="btn w-100 d-flex align-items-center border ds-staff-chip" data-user-id="' . (int)$u->ID . '"><img src="' . esc_url($avatar) . '" alt="" class="me-2 rounded-circle" width="32" height="32" /><div class="text-start"><div class="fw-semibold">' . esc_html($dn) . '</div><small class="text-muted staff-speciality">' . esc_html($dest) . '</small></div></button></div>';
}
} else {
echo '<div class="col-12"><em>' . esc_html__('No staff assigned for this service.', 'dreamsalon-core') . '</em></div>';
}
echo ' </div>';
echo ' </div>';
echo ' </div>';
echo ' <div class="ds-step ds-step-datetime" style="display:none;">';
echo ' <div class="mb-3">';
echo ' <h6 class="mb-2">' . esc_html__('Choose Date', 'dreamsalon-core') . '</h6>';
echo ' <input type="text" class="form-control ds-date-picker" style="height:0;opacity:0;position:absolute;left:-9999px;" />';
echo ' <div class="ds-calendar-inline"></div>';
echo ' </div>';
echo ' <div class="mb-3">';
echo ' <h6 class="mb-2">' . esc_html__('Available Slot', 'dreamsalon-core') . '</h6>';
echo ' <div class="row1 ds-slots-wrap d-flex p-2 border rounded" data-service-id="' . (int)$sid . '">';
echo ' <em>' . esc_html__('Pick a date to see available slots.', 'dreamsalon-core') . '</em>';
echo ' </div>';
echo ' </div>';
echo ' </div>';
echo ' <div class="border-top pt-3 d-flex justify-content-between">';
echo ' <button type="button" class="btn btn-light ds-reset">' . esc_html__('Reset', 'dreamsalon-core') . '</button>';
echo ' <button type="button" class="btn btn-dark ds-add-to-cart">' . esc_html__('Next', 'dreamsalon-core') . '</button>';
echo ' </div>';
echo '</div>';
$html = ob_get_clean();
wp_send_json_success(['html' => $html]);
}
// ===== Dashboard: Delete Service (admin-post) =====
add_action('admin_post_ds_delete_service', 'ds_handle_delete_service');
function ds_handle_delete_service(){
if (! is_user_logged_in()){
wp_safe_redirect( wp_get_referer() ?: home_url('/') );
exit;
}
$post_id = isset($_GET['post_id']) ? (int) $_GET['post_id'] : 0;
$nonce = isset($_GET['_wpnonce']) ? $_GET['_wpnonce'] : '';
$action = 'ds_delete_service_' . $post_id;
if (! $post_id || ! wp_verify_nonce($nonce, $action)){
wp_safe_redirect( add_query_arg('ds_msg', 'invalid_nonce', (wp_get_referer() ?: home_url('/'))) );
exit;
}
$post = get_post($post_id);
if (! $post || $post->post_type !== 'service'){
wp_safe_redirect( add_query_arg('ds_msg', 'not_found', (wp_get_referer() ?: home_url('/'))) );
exit;
}
$user = wp_get_current_user();
$is_owner = ((int)$post->post_author === (int)$user->ID);
$is_admin = in_array('administrator', (array)$user->roles, true);
$is_agent = in_array('agent', (array)$user->roles, true);
if (! $is_owner && ! $is_admin && ! $is_agent){
wp_safe_redirect( add_query_arg('ds_msg', 'forbidden', (wp_get_referer() ?: home_url('/'))) );
exit;
}
// Prefer trash to allow recovery
$trashed = wp_trash_post($post_id);
$msg = $trashed ? 'deleted' : 'failed';
wp_safe_redirect( add_query_arg('ds_msg', $msg, (wp_get_referer() ?: home_url('/wp-admin/edit.php?post_type=service'))) );
exit;
}
add_action('wp_ajax_ds_dashboard_load_more', 'ds_ajax_dashboard_load_more');
function ds_ajax_dashboard_load_more() {
if (! isset($_POST['_ajax_nonce']) || ! wp_verify_nonce($_POST['_ajax_nonce'], 'ds_admin_nonce')) {
wp_send_json_error(['message' => 'Invalid nonce'], 403);
}
if (! is_user_logged_in()) {
wp_send_json_error(['message' => 'Unauthorized'], 401);
}
$role = isset($_POST['role']) ? sanitize_key(wp_unslash($_POST['role'])) : '';
$page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
global $wpdb;
$book_table = $wpdb->prefix . 'ds_bookings';
// Customer dashboard: group by order_id (same as customer bookings template)
if ($role === 'customer') {
$current_user = wp_get_current_user();
$customer_id = (int) $current_user->ID;
$per_page = 10; // keep in sync with customer bookings template
$where = ['b.customer_id = %d'];
$params = [$customer_id];
$status = isset($_POST['status']) ? sanitize_key(wp_unslash($_POST['status'])) : '';
if ($status !== '') {
$where[] = 'b.payment_status = %s';
$params[] = $status;
}
$where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$count_sql = "SELECT COUNT(DISTINCT b.order_id)
FROM $book_table b
$where_sql";
$total_bookings = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
$offset = ($page - 1) * $per_page;
$rows_sql = "SELECT
MIN(b.id) AS id,
b.order_id,
MIN(b.date) AS date,
MIN(b.slot) AS slot,
MIN(b.staff_id) AS staff_id,
MIN(b.service_id) AS service_id,
MIN(b.payment_status) AS payment_status,
SUM(b.amount) AS amount
FROM $book_table b
$where_sql
GROUP BY b.order_id
ORDER BY MIN(b.date) DESC, MIN(b.slot) ASC
LIMIT %d OFFSET %d";
$rows = $wpdb->get_results($wpdb->prepare($rows_sql, array_merge($params, [$per_page, $offset])));
$format_slot = function ($slot) {
$slot = trim((string) $slot);
// if ($slot === '' || strpos($slot, '-') === false) {
// return esc_html($slot);
// }
list($s, $e) = array_map('trim', explode('-', $slot, 2));
$sf = date_i18n('h:i A', strtotime($s));
$ef = date_i18n('h:i A', strtotime($e));
return $sf . '<span>-</span> ' . $ef;
};
$get_status = function ($order_id, $fallback_status) {
if (! function_exists('wc_get_order')) {
return [
'text' => ucfirst((string) $fallback_status),
'class' => 'badge-soft-info',
];
}
$order = wc_get_order($order_id);
if (! $order) {
return [
'text' => ucfirst((string) $fallback_status),
'class' => 'badge-soft-info',
];
}
$status = $order->get_status();
$status_name = wc_get_order_status_name($status);
$status_class = [
'pending' => 'badge-soft-warning',
'processing' => 'badge-soft-info',
'on-hold' => 'badge-soft-warning',
'completed' => 'badge-soft-success',
'cancelled' => 'badge-soft-danger',
'refunded' => 'badge-soft-danger',
'failed' => 'badge-soft-danger',
];
$class = isset($status_class[$status]) ? $status_class[$status] : 'badge-soft-info';
return ['text' => $status_name, 'class' => $class];
};
ob_start();
if (! empty($rows)) {
foreach ($rows as $row) {
$service_id = (int) $row->service_id;
$service_title = $service_id ? get_the_title($service_id) : __('Service', 'dreamsalon-core');
$service_img = $service_id ? get_the_post_thumbnail_url($service_id, 'thumbnail') : '';
$branch = '';
if ($service_id) {
$terms = wp_get_object_terms($service_id, 'service_branch');
if (! is_wp_error($terms) && ! empty($terms)) {
$branch = $terms[0]->name;
}
}
$date_label = $row->date ? date_i18n('d M, Y', strtotime($row->date)) : '';
$slot_html = $row->slot ? $format_slot($row->slot) : '';
$order_status = $get_status($row->order_id, $row->payment_status);
$badge_txt = $order_status['text'];
$badge_class = $order_status['class'];
$staff_avatar = $row->staff_id ? get_avatar_url((int) $row->staff_id, ['size' => 48]) : '';
$modal_target = '#appointment_modal_' . (int) $row->id;
?>
<div class="appointments-itemone">
<div class="appointments-item appointments-itemone">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="avatar avatar-lg">
<?php
$service_gallery = get_post_meta($service_id, 'service_gallery', true);
if (! empty($service_gallery) && is_array($service_gallery)) {
$first_img_id = (int) $service_gallery[0];
$service_img_src = wp_get_attachment_image_src($first_img_id, 'thumbnail');
if ($service_img_src) {
?>
<img src="<?php echo esc_url($service_img_src[0]); ?>" alt="service" class="rounded-circle">
<?php
} elseif ($service_img) {
?>
<img src="<?php echo esc_url($service_img); ?>" alt="service" class="rounded-circle">
<?php
} else {
?>
<div class="avatar avatar-lg rounded-circle bg-light"></div>
<?php
}
} else {
if ($service_img) {
?>
<img src="<?php echo esc_url($service_img); ?>" alt="service" class="rounded-circle">
<?php
} else {
?>
<div class="avatar avatar-lg rounded-circle bg-light"></div>
<?php
}
}
?>
</div>
<div>
<h6 class="fs-16 fw-semibold mb-1 d-flex align-items-center gap-2 flex-wrap text-capitalize">
<a href="<?php echo esc_url($service_id ? get_permalink($service_id) : '#'); ?>">
<?php
$__st = $service_title;
echo esc_html(function_exists('dreamsalon_sentencecase') ? dreamsalon_sentencecase($__st) : $__st);
?>
</a>
<span class="badge badge-sm <?php echo esc_attr($badge_class); ?>"><?php echo esc_html($badge_txt); ?></span>
</h6>
<?php if ($branch) : ?>
<p class="mb-0 fs-14 text-capitalize">
<i class="ti ti-map-pin-bolt me-1 fs-14"></i>
<?php
$__br = $branch;
echo esc_html(function_exists('dreamsalon_sentencecase') ? dreamsalon_sentencecase($__br) : $__br);
?>
</p>
<?php endif; ?>
</div>
</div>
<div>
<h6 class="mb-1 fs-16 fw-semibold"><?php echo esc_html($date_label); ?></h6>
<p class="fs-14 mb-0 d-flex align-items-center gap-2"><?php echo wp_kses_post($slot_html); ?></p>
</div>
<div>
<p class="fs-14 mb-1"><?php esc_html_e('Assigned Staff', 'dreamsalon-core'); ?></p>
<div class="avatar-list-stacked">
<?php if ($staff_avatar) : ?>
<span class="avatar avatar-sm rounded-circle border-0">
<img src="<?php echo esc_url($staff_avatar); ?>" class="img-fluid rounded-circle border border-white" alt="staff">
</span>
<?php else : ?>
<span class="text-muted"><?php esc_html_e('Not assigned', 'dreamsalon-core'); ?></span>
<?php endif; ?>
</div>
</div>
<div class="text-xl-end">
<a href="#" class="btn btn-sm border border-color btn-hover d-inline-flex align-items-center fs-13 fw-medium" data-bs-toggle="modal" data-bs-target="<?php echo esc_attr($modal_target); ?>">
<?php esc_html_e('View Details', 'dreamsalon-core'); ?>
<i class="ti ti-chevron-right ms-1"></i>
</a>
</div>
</div>
</div>
<?php
}
}
$html = ob_get_clean();
$has_more = ($total_bookings > $page * $per_page);
wp_send_json_success(['html' => $html, 'has_more' => $has_more]);
}
// Agent dashboard: group orders by order_id for services authored by current user
if ($role === 'agent') {
$current_user = wp_get_current_user();
$agent_id = (int) $current_user->ID;
$per_page = 10; // keep in sync with agent bookings template
$status = isset($_POST['status']) ? sanitize_key(wp_unslash($_POST['status'])) : '';
$sort = isset($_POST['sort']) ? sanitize_key(wp_unslash($_POST['sort'])) : 'newest';
$posts = $wpdb->posts;
$where = ['p.post_author = %d'];
$params = [$agent_id];
if ($status !== '') {
$where[] = 'b.payment_status = %s';
$params[] = $status;
}
$where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$order_by_sql = 'ORDER BY MIN(b.date) DESC, MIN(b.slot) ASC';
switch ($sort) {
case 'oldest':
$order_by_sql = 'ORDER BY MIN(b.date) ASC, MIN(b.slot) ASC';
break;
case 'alphabetical_az':
$order_by_sql = 'ORDER BY MAX(p.post_title) ASC';
break;
case 'alphabetical_za':
$order_by_sql = 'ORDER BY MAX(p.post_title) DESC';
break;
default:
$order_by_sql = 'ORDER BY MIN(b.date) DESC, MIN(b.slot) ASC';
break;
}
$count_sql = "SELECT COUNT(DISTINCT b.order_id)
FROM $book_table b
INNER JOIN $posts p ON p.ID = b.service_id
$where_sql";
$total_bookings = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
$offset = ($page - 1) * $per_page;
$rows_sql = "SELECT
MIN(b.id) AS id,
b.order_id,
MIN(b.date) AS date,
MIN(b.slot) AS slot,
MIN(b.staff_id) AS staff_id,
MIN(b.service_id) AS service_id,
MIN(b.customer_id) AS customer_id,
MIN(b.payment_status) AS payment_status,
SUM(b.amount) AS amount,
MAX(p.post_title) AS post_title
FROM $book_table b
INNER JOIN $posts p ON p.ID = b.service_id
$where_sql
GROUP BY b.order_id
$order_by_sql
LIMIT %d OFFSET %d";
$rows = $wpdb->get_results($wpdb->prepare($rows_sql, array_merge($params, [$per_page, $offset])));
$format_slot = function ($slot) {
$slot = trim((string) $slot);
// if ($slot === '' || strpos($slot, '-') === false) {
// return esc_html($slot);
// }
list($s, $e) = array_map('trim', explode('-', $slot, 2));
return date_i18n('h:i A', strtotime($s)) . '<span>-</span> ' . date_i18n('h:i A', strtotime($e));
};
$get_status = function ($order_id, $fallback_status) {
if (! function_exists('wc_get_order')) {
return [
'text' => ucfirst((string) $fallback_status),
'class' => 'badge-soft-info',
];
}
$order = wc_get_order($order_id);
if (! $order) {
return [
'text' => ucfirst((string) $fallback_status),
'class' => 'badge-soft-info',
];
}
$status = $order->get_status();
$status_name = wc_get_order_status_name($status);
$status_class = [
'pending' => 'badge-soft-warning',
'processing' => 'badge-soft-info',
'on-hold' => 'badge-soft-warning',
'completed' => 'badge-soft-success',
'cancelled' => 'badge-soft-danger',
'refunded' => 'badge-soft-danger',
'failed' => 'badge-soft-danger',
];
$class = isset($status_class[$status]) ? $status_class[$status] : 'badge-soft-info';
return ['text' => $status_name, 'class' => $class];
};
ob_start();
if (! empty($rows)) {
foreach ($rows as $row) {
$service_id = (int) $row->service_id;
$service_title = $row->post_title ?: ($service_id ? get_the_title($service_id) : __('Service', 'dreamsalon-core'));
$service_img = $service_id ? get_the_post_thumbnail_url($service_id, 'thumbnail') : '';
$branch = '';
if ($service_id) {
$terms = wp_get_object_terms($service_id, 'service_branch');
if (! is_wp_error($terms) && ! empty($terms)) {
$branch = $terms[0]->name;
}
}
$date_label = $row->date ? date_i18n('d M, Y', strtotime($row->date)) : '';
$slot_html = $row->slot ? $format_slot($row->slot) : '';
$order_status = $get_status($row->order_id, $row->payment_status);
$badge_txt = $order_status['text'];
$badge_class = $order_status['class'];
$staff_avatar = $row->staff_id ? get_avatar_url((int) $row->staff_id, ['size' => 48]) : '';
$modal_target = '#appointment_modal_' . (int) $row->id;
?>
<div class="appointments-itemone">
<div class="appointments-item appointments-itemone">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="avatar avatar-lg">
<?php
$service_gallery = get_post_meta($service_id, 'service_gallery', true);
if (! empty($service_gallery) && is_array($service_gallery)) {
$first_img_id = (int) $service_gallery[0];
$service_img_src = wp_get_attachment_image_src($first_img_id, 'thumbnail');
if ($service_img_src) {
?>
<img src="<?php echo esc_url($service_img_src[0]); ?>" alt="service" class="rounded-circle">
<?php
} elseif ($service_img) {
?>
<img src="<?php echo esc_url($service_img); ?>" alt="service" class="rounded-circle">
<?php
} else {
?>
<div class="avatar avatar-lg rounded-circle bg-light"></div>
<?php
}
} else {
if ($service_img) {
?>
<img src="<?php echo esc_url($service_img); ?>" alt="service" class="rounded-circle">
<?php
} else {
?>
<div class="avatar avatar-lg rounded-circle bg-light"></div>
<?php
}
}
?>
</div>
<div>
<h6 class="fs-16 fw-semibold mb-1 d-flex align-items-center gap-2 flex-wrap text-capitalize">
<a href="<?php echo esc_url($service_id ? get_permalink($service_id) : '#'); ?>">
<?php
$__st = $service_title;
echo esc_html(function_exists('dreamsalon_sentencecase') ? dreamsalon_sentencecase($__st) : $__st);
?>
</a>
<span class="badge badge-sm <?php echo esc_attr($badge_class); ?>"><?php echo esc_html($badge_txt); ?></span>
</h6>
<?php if ($branch) : ?>
<p class="mb-0 fs-14 text-capitalize">
<i class="ti ti-map-pin-bolt me-1 fs-14"></i>
<?php
$__br = $branch;
echo esc_html(function_exists('dreamsalon_sentencecase') ? dreamsalon_sentencecase($__br) : $__br);
?>
</p>
<?php endif; ?>
</div>
</div>
<div>
<h6 class="mb-1 fs-16 fw-semibold"><?php echo esc_html($date_label); ?></h6>
<p class="fs-14 mb-0 d-flex align-items-center gap-2"><?php echo wp_kses_post($slot_html); ?></p>
</div>
<div>
<p class="fs-14 mb-1"><?php esc_html_e('Assigned Staff', 'dreamsalon-core'); ?></p>
<div class="avatar-list-stacked">
<?php if ($staff_avatar) : ?>
<span class="avatar avatar-sm rounded-circle border-0">
<img src="<?php echo esc_url($staff_avatar); ?>" class="img-fluid rounded-circle border border-white" alt="staff">
</span>
<?php else : ?>
<span class="text-muted"><?php esc_html_e('Not assigned', 'dreamsalon-core'); ?></span>
<?php endif; ?>
</div>
</div>
<div class="text-xl-end">
<a href="#" class="btn btn-sm border border-color btn-hover d-inline-flex align-items-center fs-13 fw-medium" data-bs-toggle="modal" data-bs-target="<?php echo esc_attr($modal_target); ?>">
<?php esc_html_e('View Details', 'dreamsalon-core'); ?>
<i class="ti ti-chevron-right ms-1"></i>
</a>
</div>
</div>
</div>
<?php
}
}
$html = ob_get_clean();
$has_more = ($total_bookings > $page * $per_page);
wp_send_json_success(['html' => $html, 'has_more' => $has_more]);
}
// Staff dashboard: rows for this staff member
if ($role === 'staff') {
$current_user = wp_get_current_user();
$staff_id = (int) $current_user->ID;
$per_page = 10; // keep in sync with staff bookings template
$search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
$status = isset($_POST['status']) ? sanitize_key(wp_unslash($_POST['status'])) : '';
$from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : '';
$to = isset($_POST['to']) ? sanitize_text_field(wp_unslash($_POST['to'])) : '';
$posts = $wpdb->posts;
$where = [];
$params = [];
$staff_col = 'staff_id';
$cols = $wpdb->get_col("SHOW COLUMNS FROM {$book_table}", 0);
if ($cols && is_array($cols) && ! in_array('staff_id', $cols, true)) {
$candidates = ['employee_id', 'assigned_staff', 'assigned_staff_id', 'provider_id', 'provider_user_id'];
foreach ($candidates as $cand) {
if (in_array($cand, $cols, true)) {
$staff_col = $cand;
break;
}
}
}
$where[] = "b.`$staff_col` = %d";
$params[] = $staff_id;
if ($search !== '') {
$like = '%' . $wpdb->esc_like($search) . '%';
$where[] = '(p.post_title LIKE %s)';
$params[] = $like;
}
if ($status !== '') {
$where[] = 'b.payment_status = %s';
$params[] = $status;
}
if ($from !== '') {
$where[] = 'b.date >= %s';
$params[] = $from;
}
if ($to !== '') {
$where[] = 'b.date <= %s';
$params[] = $to;
}
$where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$count_sql = "SELECT COUNT(*)
FROM $book_table b
INNER JOIN $posts p ON p.ID = b.service_id
$where_sql";
$total_bookings = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
$offset = ($page - 1) * $per_page;
$rows_sql = "SELECT b.*, p.post_title
FROM $book_table b
INNER JOIN $posts p ON p.ID = b.service_id
$where_sql
ORDER BY b.date DESC, b.slot ASC
LIMIT %d OFFSET %d";
$rows = $wpdb->get_results($wpdb->prepare($rows_sql, array_merge($params, [$per_page, $offset])));
$format_slot = function ($slot) {
$slot = trim((string) $slot);
// if ($slot === '' || strpos($slot, '-') === false) {
// return esc_html($slot);
// }
list($s, $e) = array_map('trim', explode('-', $slot, 2));
return date_i18n('h:i A', strtotime($s)) . '<span>-</span> ' . date_i18n('h:i A', strtotime($e));
};
ob_start();
if (! empty($rows)) {
foreach ($rows as $row) {
$service_id = (int) $row->service_id;
$service_title = $row->post_title ?: ($service_id ? get_the_title($service_id) : __('Service', 'dreamsalon-core'));
$service_img = $service_id ? get_the_post_thumbnail_url($service_id, 'thumbnail') : '';
$branch = '';
if ($service_id) {
$terms = wp_get_object_terms($service_id, 'service_branch');
if (! is_wp_error($terms) && ! empty($terms)) {
$branch = $terms[0]->name;
}
}
$date_label = $row->date ? date_i18n('d M, Y', strtotime($row->date)) : '';
$slot_html = $row->slot ? $format_slot($row->slot) : '';
$badge_txt = 'Scheduled';
$badge_class = 'badge-soft-info';
$ps = strtolower((string) $row->payment_status);
if ($ps === 'completed') {
$badge_txt = 'Completed';
$badge_class = 'badge-soft-success';
} elseif ($ps === 'cancelled' || $ps === 'refunded') {
$badge_txt = 'Cancelled';
$badge_class = 'badge-soft-danger';
}
$cust_name = $row->customer_id ? get_the_author_meta('display_name', (int) $row->customer_id) : ($row->customer_name ?: '');
$modal_target = '#appointment_modal_' . (int) $row->id;
?>
<div class="appointments-itemone">
<div class="appointments-item appointments-itemone">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="avatar avatar-lg">
<?php
$service_gallery = get_post_meta($service_id, 'service_gallery', true);
if (! empty($service_gallery) && is_array($service_gallery)) {
$first_img_id = (int) $service_gallery[0];
$service_img_src = wp_get_attachment_image_src($first_img_id, 'thumbnail');
if ($service_img_src) {
?>
<img src="<?php echo esc_url($service_img_src[0]); ?>" alt="service" class="rounded-circle">
<?php
} elseif ($service_img) {
?>
<img src="<?php echo esc_url($service_img); ?>" alt="service" class="rounded-circle">
<?php
} else {
?>
<div class="avatar avatar-lg rounded-circle bg-light"></div>
<?php
}
} else {
if ($service_img) {
?>
<img src="<?php echo esc_url($service_img); ?>" alt="service" class="rounded-circle">
<?php
} else {
?>
<div class="avatar avatar-lg rounded-circle bg-light"></div>
<?php
}
}
?>
</div>
<div>
<h6 class="fs-16 fw-semibold mb-1 d-flex align-items-center gap-2 flex-wrap text-capitalize">
<a href="<?php echo esc_url($service_id ? get_permalink($service_id) : '#'); ?>">
<?php
$__st = $service_title;
echo esc_html(function_exists('dreamsalon_sentencecase') ? dreamsalon_sentencecase($__st) : $__st);
?>
</a>
<span class="badge badge-sm <?php echo esc_attr($badge_class); ?>"><?php echo esc_html($badge_txt); ?></span>
</h6>
<?php if ($branch) : ?>
<p class="mb-0 fs-14 text-capitalize">
<i class="ti ti-map-pin-bolt me-1 fs-14"></i>
<?php
$__br = $branch;
echo esc_html(function_exists('dreamsalon_sentencecase') ? dreamsalon_sentencecase($__br) : $__br);
?>
</p>
<?php endif; ?>
</div>
</div>
<div>
<h6 class="mb-1 fs-16 fw-semibold"><?php echo esc_html($date_label); ?></h6>
<p class="fs-14 mb-0 d-flex align-items-center gap-2"><?php echo wp_kses_post($slot_html); ?></p>
</div>
<div>
<p class="fs-14 mb-1"><?php esc_html_e('Customer', 'dreamsalon-core'); ?></p>
<div class="fs-14"><?php echo esc_html($cust_name ?: __('Guest', 'dreamsalon-core')); ?></div>
</div>
<div class="text-xl-end">
<a href="#" class="btn btn-sm border border-color btn-hover d-inline-flex align-items-center fs-13 fw-medium" data-bs-toggle="modal" data-bs-target="<?php echo esc_attr($modal_target); ?>">
<?php esc_html_e('View Details', 'dreamsalon-core'); ?>
<i class="ti ti-chevron-right ms-1"></i>
</a>
</div>
</div>
</div>
<?php
}
}
$html = ob_get_clean();
$has_more = ($total_bookings > $page * $per_page);
wp_send_json_success(['html' => $html, 'has_more' => $has_more]);
}
wp_send_json_error(['message' => 'Invalid role']);
}