File: /mnt/data/dreamsai-wp/wp-content/dreams-ai/assets/scripts.js
jQuery(function ($) {
function dtShowToast(message, type) {
var $container = $("#dt-toast-container");
if (!$container.length) {
$("body").append(
'<div class="toast-container position-fixed top-50 end-0 p-0" id="dt-toast-container" style="z-index:1080;"></div>'
);
$container = $("#dt-toast-container");
}
var id = "dt-toast-" + Date.now();
var bgClass = "text-bg-secondary";
if (type === "success") bgClass = "text-bg-success";
if (type === "error") bgClass = "text-bg-danger";
var html =
'<div id="' +
id +
'" class="toast align-items-center ' +
bgClass +
'" role="alert" aria-live="assertive" aria-atomic="true">\
<div class="d-flex">\
<div class="toast-body">' +
(message || "") +
'</div>\
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>\
</div>\
</div>';
$container.append(html);
var el = document.getElementById(id);
if (window.bootstrap && window.bootstrap.Toast) {
var toast = new window.bootstrap.Toast(el, { delay: 3000 });
toast.show();
} else {
$(el)
.fadeIn(150)
.delay(3000)
.fadeOut(300, function () {
$(this).remove();
});
}
}
// Initialize Select2 if present
function initSelect2() {
if (!$.fn.select2) return;
$("select.form-select, select.select").each(function () {
var $el = $(this);
if ($el.data("select2")) return;
$el.select2({ width: "100%" });
});
// ===== Add Service: Gallery uploader =====
var dsGalleryFrame;
$(document).on("click", "#gallery_media_uploader", function (e) {
e.preventDefault();
if (dsGalleryFrame) {
dsGalleryFrame.open();
return;
}
dsGalleryFrame = wp.media({
title: "Select Images",
button: { text: "Use selected" },
multiple: true,
});
dsGalleryFrame.on("select", function () {
var selection = dsGalleryFrame.state().get("selection");
var $wrap = $(".gallery-preview");
selection.each(function (att) {
var a = att.toJSON();
var url =
a.sizes && a.sizes.thumbnail ? a.sizes.thumbnail.url : a.url;
var $box = $(
'<div class="gallery-upload-img me-2 mb-2 position-relative">'
);
$box.append(
$("<img>").attr("src", url).css({
width: "100px",
height: "100px",
objectFit: "cover",
borderRadius: "4px",
})
);
$box.append(
'<span class="trash-icon d-flex align-items-center justify-content-center text-danger gallery-trash" data-image-id="' +
a.id +
'"><i class="isax isax-trash"></i></span>'
);
$box.append(
'<input type="hidden" name="gallery_images[]" value="' +
a.id +
'" />'
);
$wrap.append($box);
});
});
dsGalleryFrame.open();
});
$(document).on("click", ".gallery-trash", function () {
var $box = $(this).closest(".gallery-upload-img");
$box.remove();
});
// ===== Add Service: Add-ons rows =====
function addonRowTemplate() {
return (
'<div class="col-12 ds-addon-row">\
<div class="row g-2 align-items-end">\
<div class="col-md-4">\
<label class="form-label">Title</label>\
<input type="text" class="form-control" name="addons_title[]" />\
</div>\
<div class="col-md-3">\
<label class="form-label">Price</label>\
<div class="input-group">\
<span class="input-group-text">' +
wcCurrency.symbol +
' </span>\
<input type="text" class="form-control" name="addons_price[]" />\
</div>\
</div>\
<div class="col-md-2">\
<label class="form-label">Hrs</label>\
<select class="form-select" name="addons_dur_h[]">' +
(function () {
var o = "\n<option value>—</option>";
for (var h = 0; h <= 12; h++) {
o += '\n<option value="' + h + '">' + h + "</option>";
}
return o;
})() +
'</select>\
</div>\
<div class="col-md-2">\
<label class="form-label">Mins</label>\
<select class="form-select" name="addons_dur_m[]">' +
(function () {
var o = "\n<option value>—</option>";
for (var m = 0; m <= 59; m++) {
o += '\n<option value="' + m + '">' + m + "</option>";
}
return o;
})() +
'</select>\
</div>\
<div class="col-md-1">\
<button type="button" class="btn btn-outline-danger ds-remove-addon">×</button>\
</div>\
</div>\
</div>'
);
}
$(document).on("click", "#ds-add-addon-btn", function () {
var $list = $(".ds-addons-list");
if (!$list.length) return;
$list.append(addonRowTemplate());
});
$(document).on("click", ".ds-remove-addon", function () {
var $row = $(this).closest(".ds-addon-row");
$row.remove();
});
// ===== Add Service: FAQ add/edit =====
var $faqEditing = null;
$(document).on("click", "#add_faq_btn", function () {
var q = $.trim($("#faq_question_input").val());
var a = $.trim($("#faq_answer_input").val());
if (!q || !a) {
dtShowToast("Please enter both question and answer.", "error");
return;
}
var $card = $(
'<div class="card shadow-none mb-0 mt-3 faq-item">\
<div class="card-body px-3 py-2">\
<div class="d-flex align-items-center justify-content-between flex-wrap row-gap-3">\
<h6><a href="javascript:void(0);" class="faq-question-text"></a></h6>\
<div class="d-flex align-items-center">\
<a href="javascript:void(0);" data-bs-toggle="modal" data-bs-target="#edit_faq" class="rounded-edit d-flex align-items-center justify-content-center me-2 edit-faq-btn"><i class="isax isax-edit-2"></i></a>\
<a href="javascript:void(0);" class="trash-icon d-flex align-items-center justify-content-center remove-faq"><i class="isax isax-trash text-danger"></i></a>\
</div>\
</div>\
</div>\
</div>'
);
$card.find(".faq-question-text").text(q);
var $hiddenQ = $('<input type="hidden" name="faq_question[]" />').val(q);
var $hiddenA = $(
'<textarea name="faq_answer[]" style="display:none;"></textarea>'
).text(a);
$(".faq_append").append($card).append($hiddenQ).append($hiddenA);
$("#add_faq").modal("hide");
$("#faq_question_input").val("");
$("#faq_answer_input").val("");
});
$(document).on("click", ".edit-faq-btn", function () {
var $card = $(this).closest(".faq-item");
var $qInput = $card.nextAll("input[name='faq_question[]']").first();
var $aInput = $qInput.length
? $qInput.nextAll("textarea[name='faq_answer[]']").first()
: $();
var q = $qInput.length
? $qInput.val()
: $card.find(".faq-question-text").text();
var a = $aInput.length ? $aInput.val() : "";
$("#edit_faq_question_input").val(q);
$("#edit_faq_answer_input").val(a);
$faqEditing = $card;
});
$(document).on("click", "#save_faq_btn", function () {
if (!$faqEditing) return;
var nq = $.trim($("#edit_faq_question_input").val());
var na = $.trim($("#edit_faq_answer_input").val());
if (!nq || !na) {
dtShowToast("Please enter both question and answer.", "error");
return;
}
$faqEditing.find(".faq-question-text").text(nq);
var $qInput = $faqEditing.nextAll("input[name='faq_question[]']").first();
var $aInput = $qInput.length
? $qInput.nextAll("textarea[name='faq_answer[]']").first()
: $();
if ($qInput.length) $qInput.val(nq);
else
$faqEditing.after(
$('<input type="hidden" name="faq_question[]" />').val(nq)
);
if ($aInput.length) $aInput.val(na);
else
$faqEditing.after(
$(
'<textarea name="faq_answer[]" style="display:none;"></textarea>'
).text(na)
);
$("#edit_faq").modal("hide");
$faqEditing = null;
});
$(document).on("click", ".remove-faq", function () {
var $card = $(this).closest(".faq-item");
var $qInput = $card.nextAll("input[name='faq_question[]']").first();
var $aInput = $qInput.length
? $qInput.nextAll("textarea[name='faq_answer[]']").first()
: $();
$card.remove();
if ($qInput.length) $qInput.remove();
if ($aInput.length) $aInput.remove();
});
}
function showFormErrors(messages) {
var $box = $("#ds-form-errors");
if (!$box.length) return;
var $list = $("#ds-form-errors-list");
if (!$list.length) {
$list = $('<ul class="mb-0" id="ds-form-errors-list"></ul>');
$box.append($list);
}
$list.empty();
if (!messages || !messages.length) {
$box.addClass("d-none");
return;
}
messages.forEach(function (msg) {
$list.append($("<li></li>").text(msg));
});
$box.removeClass("d-none");
var top = $box.offset() ? $box.offset().top - 120 : 0;
if (top >= 0) {
window.scrollTo({ top: top, behavior: "smooth" });
}
}
// Ensure global staff loader exists for Assign Staff
function feLoadStaffGlobal() {
var $staff = $("#fe_staff_users");
if (!$staff.length || typeof DSAjax === "undefined") return;
var cat = $("select[name='category']").val();
var branches = $("select[name='branch[]']").val() || [];
var preselected = $staff.val() || [];
$staff.prop("disabled", true);
$staff.trigger("change.select2");
$.post(
DSAjax.ajaxUrl,
{
action: "ds_load_staff",
_ajax_nonce: DSAjax.nonce,
categories: cat ? [parseInt(cat, 10)] : [],
branches: (branches || []).map(function (v) {
return parseInt(v, 10);
}),
},
function (resp) {
if (!resp || !resp.success) return;
$staff.empty();
var users = resp.data && resp.data.users ? resp.data.users : [];
users.forEach(function (u) {
var opt = $("<option>").val(u.ID).text(u.name);
if (
preselected.includes(String(u.ID)) ||
preselected.includes(parseInt(u.ID, 10))
) {
opt.attr("selected", "selected");
}
$staff.append(opt);
});
$staff.prop("disabled", false).trigger("change");
}
);
}
// Debounce helper
function debounce(fn, wait) {
var t;
return function () {
var ctx = this,
args = arguments;
clearTimeout(t);
t = setTimeout(function () {
fn.apply(ctx, args);
}, wait);
};
}
// Bind and initializations
var feLoadStaffDebounced = debounce(feLoadStaffGlobal, 250);
$(document).on(
"change",
"select[name='category'], select[name='branch[]']",
feLoadStaffDebounced
);
feLoadStaffGlobal();
initSelect2();
function renderSlots(slots) {
var $wrap = $("#ds_generated_slots");
var $inputs = $("#ds_generated_slots_inputs");
$wrap.empty();
$inputs.empty();
if (!slots || !slots.length) {
$wrap.append($("<em>").text("No slots generated yet."));
return;
}
$.each(slots, function (_, s) {
$wrap.append($('<span class="ds-slot-chip">').text(s));
$inputs.append(
$('<input type="hidden" name="ds_generated_slots[]" />').val(s)
);
});
// ===== Frontend: Load staff filtered by Category & Branches =====
function feLoadStaff() {
var $staff = $("#fe_staff_users");
if (!$staff.length) return;
var cat = $("select[name='category']").val();
var branches = $("select[name='branch[]']").val() || [];
var preselected = $staff.val() || [];
var payload = {
action: "ds_load_staff",
_ajax_nonce: DSAjax.nonce,
categories: cat ? [parseInt(cat, 10)] : [],
branches: (branches || []).map(function (v) {
return parseInt(v, 10);
}),
};
$.post(DSAjax.ajaxUrl, payload, function (resp) {
if (!resp || !resp.success) return;
$staff.empty();
var users = resp.data && resp.data.users ? resp.data.users : [];
users.forEach(function (u) {
var opt = $("<option>").val(u.ID).text(u.name);
if (
preselected.includes(String(u.ID)) ||
preselected.includes(parseInt(u.ID, 10))
) {
opt.attr("selected", "selected");
}
$staff.append(opt);
});
$staff.trigger("change");
});
}
// Bind when category or branches change
$(document).on(
"change",
"select[name='category'], select[name='branch[]']",
feLoadStaff
);
// Initial load on page ready
feLoadStaff();
initSelect2();
}
function generateSlots() {
var start = $("#ds_start_time").val();
var end = $("#ds_end_time").val();
var interval = parseInt($("#ds_interval").val(), 10) || 30;
if (!start || !end) return; // wait until both are set
$.post(DSAjax.ajaxUrl, {
action: "ds_generate_slots",
_ajax_nonce: DSAjax.nonce,
start: start,
end: end,
interval: interval,
}).done(function (resp) {
if (resp && resp.success) {
renderSlots(resp.data.slots || []);
}
});
}
// Button
$(document).on("click", "#ds_generate_slots_btn", function (e) {
e.preventDefault();
generateSlots();
});
// Auto-generate on change if both set
$(document).on(
"change",
"#ds_start_time, #ds_end_time, #ds_interval",
function () {
generateSlots();
}
);
// ===== Frontend Add Service: Weekly Slots UI =====
function parseHM(v) {
if (!v) return null;
var p = v.split(":");
if (p.length < 2) return null;
var h = parseInt(p[0], 10);
var m = parseInt(p[1], 10);
if (isNaN(h) || isNaN(m)) return null;
return h * 60 + m;
}
function fmtAMPM(total) {
var h = Math.floor(total / 60),
m = total % 60;
var suf = h >= 12 ? "PM" : "AM";
var h12 = h % 12;
if (h12 === 0) h12 = 12;
var mm = (m < 10 ? "0" : "") + m;
return h12 + ":" + mm + suf;
}
function buildDaySlotsWithGap(start, end, durMin, interval) {
var out = [];
var s = parseHM(start),
e = parseHM(end);
var D = parseInt(durMin, 10) || 0;
var I = parseInt(interval, 10) || 0;
if (s == null || e == null || e <= s || D <= 0) return out;
var t = s;
while (t + D <= e) {
var st = t,
en = t + D;
out.push(fmtAMPM(st) + " to " + fmtAMPM(en));
t = I <= 0 ? en : en + I;
}
return out;
}
function renderDayGenerated(day, slots) {
var $wrap = $("#fe-day-slots-" + day);
if (!$wrap.length) return;
$wrap.empty();
if (!slots.length) {
$wrap.append("<em>No slots generated yet.</em>");
return;
}
slots.forEach(function (s) {
$wrap.append(
'<span class="badge bg-light text-dark border position-relative pe-4">' +
s +
'<button type="button" class="btn btn-link p-0 position-absolute top-0 end-0 fe-remove-slot" data-day="' +
day +
'" data-slot="' +
s +
'">×</button></span>'
);
$wrap.append(
'<input type="hidden" name="service_day[' +
day +
'][time_slots][]" value="' +
s +
'" />'
);
});
}
function generateForDay(day) {
var s = $(".fe-day-start[data-day='" + day + "']").val();
var e = $(".fe-day-end[data-day='" + day + "']").val();
var iv = $(".fe-day-interval[data-day='" + day + "']").val();
var dh = parseInt($("select[name='duration_hours']").val(), 10) || 0;
var dm = parseInt($("select[name='duration_minutes']").val(), 10) || 0;
var D = dh * 60 + dm;
var slots = buildDaySlotsWithGap(s, e, D, iv);
renderDayGenerated(day, slots);
}
// Accordion init
(function initWeeklySlots() {
$(".dc-subpanelcontent_2").hide();
var $first = $(".dc-childaccordion_2 .dc-subpaneltitle_2").first();
if ($first.length) {
$first.addClass("active");
var tg = $first.attr("data-bs-target");
$first.find("input.day-toggle").prop("checked", true);
if (tg) $(tg).show();
}
$(".dc-childaccordion_2").on("change", "input.day-toggle", function () {
var header = $(this).closest(".dc-subpaneltitle_2");
var target = header.attr("data-bs-target");
var checked = $(this).is(":checked");
header.toggleClass("active", checked);
if (target) {
var $content = $(target);
if (checked) $content.stop(true, true).slideDown("fast");
else $content.stop(true, true).slideUp("fast");
}
});
$(".dc-childaccordion_2").on("click", ".dc-subpaneltitle_2", function (e) {
if ($(e.target).is("input,button,a,label,.slider")) return;
var $cb = $(this).find("input.day-toggle");
$cb.prop("checked", !$cb.prop("checked")).trigger("change");
});
})();
// ===== Dashboard Agent Reviews: Reply toggle + AJAX =====
(function initAgentReviews() {
// Toggle reply form
$(document).on("click", ".add-reply", function (e) {
e.preventDefault();
var target = $(this).data("target");
if (!target) return;
var $box = $("#" + target);
if ($box.length) {
$box.toggle();
}
});
// AJAX submit reply
$(document).on("submit", ".reply-form", function (e) {
e.preventDefault();
var $form = $(this);
var content = $.trim(
$form.find('textarea[name="reply_content"]').val() || ""
);
var parentId =
parseInt($form.find('input[name="parent_comment_id"]').val(), 10) || 0;
var postId =
parseInt($form.find('input[name="salon_post_id"]').val(), 10) || 0;
if (!content || !parentId || !postId) {
dtShowToast("Please enter a reply.", "error");
return;
}
var payload = {
action: "ds_reply_review",
_ajax_nonce: window.DSAjax && DSAjax.nonce ? DSAjax.nonce : "",
parent_comment_id: parentId,
post_id: postId,
content: content,
};
$form.find('button[type="submit"]').prop("disabled", true);
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", payload)
.done(function (resp) {
if (resp && resp.success) {
dtShowToast(
resp.data && resp.data.message
? resp.data.message
: "Reply posted.",
"success"
);
window.location.reload();
} else {
dtShowToast(
resp && resp.data && resp.data.message
? resp.data.message
: "Failed",
"error"
);
}
})
.fail(function () {
dtShowToast("Failed", "error");
})
.always(function () {
$form.find('button[type="submit"]').prop("disabled", false);
});
});
})();
// ===== Customer Reviews: Edit/Delete via AJAX =====
(function initCustomerReviews() {
// Fill Edit modal
$(document).on("click", ".js-open-edit", function () {
var id = $(this).data("comment-id");
var content = $(this).data("content") || "";
var rating = $(this).data("rating") || "";
$("#edit-comment-id").val(id);
$("#edit-content").val(content);
$("#edit-rating").val(rating);
});
// Submit Edit
$(document).on("click", "#edit-review-submit", function () {
var id = $("#edit-comment-id").val();
var content = $("#edit-content").val();
var rating = $("#edit-rating").val();
var payload = {
action: "ds_update_review",
_ajax_nonce: window.DSAjax && DSAjax.nonce ? DSAjax.nonce : "",
comment_id: id,
content: content,
rating: rating,
};
var $btn = $(this).prop("disabled", true);
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", payload)
.done(function (resp) {
if (resp && resp.success) {
location.reload();
} else {
dtShowToast(
resp && resp.data && resp.data.message
? resp.data.message
: "Update failed",
"error"
);
}
})
.fail(function () {
dtShowToast("Update failed", "error");
})
.always(function () {
$btn.prop("disabled", false);
});
});
// Fill Delete modal
$(document).on("click", ".js-open-delete", function () {
var id = $(this).data("comment-id");
$("#delete-comment-id").val(id);
});
// Submit Delete
$(document).on("click", "#delete-review-submit", function () {
var id = $("#delete-comment-id").val();
var payload = {
action: "ds_delete_review",
_ajax_nonce: window.DSAjax && DSAjax.nonce ? DSAjax.nonce : "",
comment_id: id,
};
var $btn = $(this).prop("disabled", true);
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", payload)
.done(function (resp) {
if (resp && resp.success) {
location.reload();
} else {
dtShowToast(
resp && resp.data && resp.data.message
? resp.data.message
: "Delete failed",
"error"
);
}
})
.fail(function () {
dtShowToast("Delete failed", "error");
})
.always(function () {
$btn.prop("disabled", false);
});
});
})();
// ===== Agent Replies: Inline Edit/Delete via AJAX =====
(function initAgentReplyActions() {
$(document).on("click", ".js-reply-edit", function (e) {
e.preventDefault();
var id = $(this).data("comment-id");
var curr = $(this).data("content") || "";
var content = window.prompt("Edit reply", curr);
if (content === null) return;
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", {
action: "ds_update_review",
_ajax_nonce: window.DSAjax && DSAjax.nonce ? DSAjax.nonce : "",
comment_id: id,
content: content,
})
.done(function (resp) {
if (resp && resp.success) location.reload();
else
dtShowToast(
(resp && resp.data && resp.data.message) || "Failed",
"error"
);
})
.fail(function () {
dtShowToast("Failed", "error");
});
});
$(document).on("click", ".js-reply-delete", function (e) {
e.preventDefault();
if (!window.confirm("Delete this reply?")) return;
var id = $(this).data("comment-id");
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", {
action: "ds_delete_review",
_ajax_nonce: window.DSAjax && DSAjax.nonce ? DSAjax.nonce : "",
comment_id: id,
})
.done(function (resp) {
if (resp && resp.success) location.reload();
else
dtShowToast(
(resp && resp.data && resp.data.message) || "Failed",
"error"
);
})
.fail(function () {
dtShowToast("Failed", "error");
});
});
})();
// ===== Single Service: Review & Enquiry submissions =====
(function initSingleServiceForms() {
// Review form
$(document).on("submit", "#ds-add-review-form", function (e) {
e.preventDefault();
var $form = $(this);
var data = {
action: "ds_submit_review",
_ajax_nonce:
$form.find('input[name="_ajax_nonce"]').val() ||
(window.DSAjax && DSAjax.nonce) ||
"",
post_id: $form.find('input[name="post_id"]').val(),
rating: $form.find('input[name="rating"]:checked').val(),
author: $form.find('input[name="author"]').val(),
email: $form.find('input[name="email"]').val(),
content: $form.find('textarea[name="content"]').val(),
};
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", data)
.done(function (resp) {
if (resp && resp.success) {
try {
$("#add_review").modal
? $("#add_review").modal("hide")
: window.bootstrap
? new window.bootstrap.Modal(
document.getElementById("add_review")
).hide()
: null;
} catch (_) {}
dtShowToast(
resp.data && resp.data.message ? resp.data.message : "Submitted",
"success"
);
window.location.reload();
} else {
dtShowToast(
resp && resp.data && resp.data.message
? resp.data.message
: "Failed",
"error"
);
}
})
.fail(function () {
dtShowToast("Failed", "error");
});
});
// Enquiry form
$(document).on("submit", "#ds-enquire-form", function (e) {
e.preventDefault();
var $form = $(this);
var phoneInput = $form.find('input[name="phone"]');
var phone = $.trim(phoneInput.val());
var phonePattern = /^\+?[0-9\s-]{7,15}$/;
if (!phonePattern.test(phone)) {
dtShowToast(
window.DSAjax && DSAjax.i18n && DSAjax.i18n.invalidPhone
? DSAjax.i18n.invalidPhone
: "Please enter a valid phone number (7-15 digits).",
"error"
);
phoneInput.focus();
return;
}
var data = {
action: "ds_submit_enquiry",
_ajax_nonce:
$form.find('input[name="_ajax_nonce"]').val() ||
(window.DSAjax && DSAjax.nonce) ||
"",
service_id: $form.find('input[name="service_id"]').val(),
name: $form.find('input[name="name"]').val(),
email: $form.find('input[name="email"]').val(),
phone: phone,
message: $form.find('textarea[name="message"]').val(),
};
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", data)
.done(function (resp) {
if (resp && resp.success) {
try {
$("#enquire_modal").modal
? $("#enquire_modal").modal("hide")
: window.bootstrap
? new window.bootstrap.Modal(
document.getElementById("enquire_modal")
).hide()
: null;
} catch (_) {}
dtShowToast(
resp.data && resp.data.message ? resp.data.message : "Submitted",
"success"
);
if (typeof $form[0].reset === "function") {
$form[0].reset();
}
// restore logged-in autofill if needed
var nameVal = $form.data("prefill-name");
var emailVal = $form.data("prefill-email");
if (nameVal) {
$form.find('input[name="name"]').val(nameVal);
}
if (emailVal) {
$form.find('input[name="email"]').val(emailVal);
}
} else {
dtShowToast(
resp && resp.data && resp.data.message
? resp.data.message
: "Failed",
"error"
);
}
})
.fail(function () {
dtShowToast("Failed", "error");
});
});
// View Details: force open modal even if Bootstrap JS not auto-wired
$(document).on("click", "[data-view-details]", function (e) {
var $a = $(this);
// allow default data-bs-toggle to work if Bootstrap is present
// if not, manually open modal
try {
if (window.bootstrap && typeof window.bootstrap.Modal === "function") {
e.preventDefault();
var el = document.getElementById("schedule_modal");
if (el) {
new window.bootstrap.Modal(el).show();
}
} else if ($.fn.modal) {
e.preventDefault();
$("#schedule_modal").modal("show");
}
} catch (_) {}
});
// Helper: get query string parameter from current URL
function getQueryParam(name) {
try {
var url = window.location.href;
if (window.URL && URL.prototype.searchParams) {
var u = new URL(url);
return u.searchParams.get(name);
}
var query = url.split("?")[1] || "";
var pairs = query.split("&");
for (var i = 0; i < pairs.length; i++) {
var p = pairs[i].split("=");
if (decodeURIComponent(p[0]) === name) {
return typeof p[1] !== "undefined"
? decodeURIComponent(p[1].replace(/\+/g, " "))
: "";
}
}
return null;
} catch (e) {
return null;
}
}
// Load More (dashboard bookings only: requires data-role)
$(document).on("click", ".load-more-custom", function (e) {
var $btn = $(this);
var role = $btn.data("role");
// If no role attribute, this is likely a normal front-end Load More link; let browser handle navigation
if (!role) {
return;
}
e.preventDefault();
if ($btn.prop("disabled")) return;
var page = parseInt($btn.data("page"), 10) || 1;
var nextPage = page + 1;
var payload = {
action: "ds_dashboard_load_more",
_ajax_nonce: window.DSAjax && DSAjax.nonce ? DSAjax.nonce : "",
role: role,
page: nextPage,
search: getQueryParam("search") || getQueryParam("s") || "",
status: getQueryParam("status") || "",
from: getQueryParam("from") || "",
to: getQueryParam("to") || "",
};
if (role === "agent" || role === "staff") {
payload.sort = getQueryParam("sort") || "newest";
}
if (role === "agent") {
delete payload.search;
delete payload.from;
delete payload.to;
}
$btn.prop("disabled", true).addClass("opacity-50");
$.post(window.DSAjax && DSAjax.ajaxUrl ? DSAjax.ajaxUrl : "", payload)
.done(function (resp) {
if (!resp || !resp.success) {
return;
}
var html = resp.data && resp.data.html ? resp.data.html : "";
if (html) {
$("#ds-bookings-list").append(html);
}
var hasMore = !!(resp.data && resp.data.has_more);
if (hasMore) {
$btn
.data("page", nextPage)
.prop("disabled", false)
.removeClass("opacity-50");
} else {
$btn.closest(".load-more").remove();
}
})
.fail(function () {
$btn.prop("disabled", false).removeClass("opacity-50");
});
});
})();
// Events
$(document).on("click", ".fe-day-generate", function () {
var day = $(this).data("day");
generateForDay(day);
});
$(document).on(
"change",
".fe-day-start, .fe-day-end, .fe-day-interval",
function () {
var day = $(this).data("day");
generateForDay(day);
}
);
$(document).on(
"change",
"select[name='duration_hours'], select[name='duration_minutes']",
function () {
$(".fe-day-generate").each(function () {
generateForDay($(this).data("day"));
});
}
);
$(document).on("click", ".fe-day-delete-all", function () {
var day = $(this).data("day");
var $wrap = $("#fe-day-slots-" + day);
$wrap.empty();
$(
"#ds-add-service-form input[name='service_day[" +
day +
"][time_slots][]']"
).remove();
var $start = $(".fe-day-start[data-day='" + day + "']");
var $end = $(".fe-day-end[data-day='" + day + "']");
var $interval = $(".fe-day-interval[data-day='" + day + "']");
var $spaces = $(".fe-day-spaces[data-day='" + day + "']");
$start.val("").trigger("change");
$end.val("").trigger("change");
if (!$interval.find("option[value='']").length) {
$interval.prepend('<option value="">—</option>');
}
$interval.val("").trigger("change");
$spaces.val("");
$wrap.append('<em class="text-muted">No slots generated yet.</em>');
});
$(document).on("click", ".fe-remove-slot", function () {
var day = $(this).data("day");
var slot = $(this).data("slot");
$(this).closest("span.badge").remove();
$(
"input[name='service_day[" +
day +
"][time_slots][]'][value='" +
slot +
"']"
).remove();
var $wrap = $("#fe-day-slots-" + day);
if (
!$wrap.find("input[name='service_day[" + day + "][time_slots][]']").length
) {
$wrap.append("<em>No slots generated yet.</em>");
}
});
// Validation on submit (only for Add Service form)
$(document).on("submit", "#ds-add-service-form", function (e) {
var errors = [];
var serviceName = $.trim($("input[name='service_name']").val());
if (!serviceName) {
errors.push("Service name is required.");
}
var descEditor =
window.tinyMCE && window.tinyMCE.get("service_description")
? window.tinyMCE.get("service_description")
: null;
var descText = descEditor
? descEditor.getContent({ format: "text" })
: $("#service_description").val();
if (!descText || !descText.replace(/\s+/g, "").length) {
errors.push("Description is required.");
}
var price = $.trim($("input[name='price']").val());
if (!price) {
errors.push("Price is required.");
}
var category = $("select[name='category']").val();
if (!category) {
errors.push("Please select a category.");
}
var branches = $("select[name='branch[]']").val() || [];
var hasBranch = branches.some(function (branch) {
return branch !== null && String(branch).trim() !== "";
});
if (!hasBranch) {
errors.push("Please select at least one branch.");
}
var dh = parseInt($("select[name='duration_hours']").val(), 10) || 0;
var dm = parseInt($("select[name='duration_minutes']").val(), 10) || 0;
if (dh + dm === 0) {
errors.push("Please select a service duration (hours/minutes).");
}
var missingTime = false;
var missingDuration = false;
var invalidSlots = false;
var hasAtLeastOneSlot = false;
$(".dc-childaccordion_2 .dc-subpaneltitle_2").each(function () {
var header = $(this);
var day = (header.attr("id") || "").replace("heading-", "");
var checked = header.find("input.day-toggle").is(":checked");
if (!day || !checked) return;
var s = $(".fe-day-start[data-day='" + day + "']").val();
var e2 = $(".fe-day-end[data-day='" + day + "']").val();
var iv = $(".fe-day-interval[data-day='" + day + "']").val();
if (!s || !e2) {
missingTime = true;
return;
}
var D = dh * 60 + dm;
if (D <= 0) {
missingDuration = true;
return;
}
var $wrap = $("#fe-day-slots-" + day);
var slotInputs = $wrap.find(
"input[name='service_day[" + day + "][time_slots][]']"
);
if (slotInputs.length) {
hasAtLeastOneSlot = true;
return;
}
var start = parseHM(s),
end = parseHM(e2),
I = parseInt(iv, 10) || 0;
var out = [];
if (!(start == null || end == null || end <= start)) {
var t = start;
while (t + D <= end) {
var st = t,
en = t + D;
out.push(st + "-" + en);
t = I <= 0 ? en : en + I;
}
}
if (!out.length) {
invalidSlots = true;
return;
}
hasAtLeastOneSlot = true;
generateForDay(day);
});
if (missingTime) {
errors.push("Please set start and end time for each enabled day.");
}
if (missingDuration) {
errors.push("Duration must be greater than 0 before generating slots.");
}
if (invalidSlots) {
errors.push("Please generate valid slots for the enabled days.");
}
if (!hasAtLeastOneSlot) {
errors.push("Please add slots for at least one day.");
}
if (errors.length) {
e.preventDefault();
showFormErrors(errors);
return false;
}
showFormErrors([]);
});
// ===== Frontend Booking Page: Branch -> Categories -> Services (AJAX) =====
(function initFrontendBooking() {
var $branch = $("#ds-branch-select");
if (typeof DSFE === "undefined") return;
// If not on bookings page, still allow running on cart page
var onCartPage = (function () {
try {
if (document.querySelector("form.woocommerce-cart-form")) return true;
var href = window.location.href;
var path =
window.location.pathname +
window.location.search +
window.location.hash;
if (window.DSFE && DSFE.cartUrl && href.indexOf(DSFE.cartUrl) === 0)
return true;
return /(\/|^)cart(\/|\?|#|$)/i.test(path);
} catch (e) {
return false;
}
})();
if (!$branch.length && !onCartPage) return;
var $tabs = $("#ds-category-tabs");
var $grid = $("#ds-services-grid");
var $categoryLoader = $("#ds-category-loader");
var $servicesLoader = $("#ds-services-loader");
var $drawer = $("#ds-booking-drawer");
var $drawerBody = $("#ds-booking-drawer-body");
var $overlay = $("#ds-drawer-overlay");
var $branchTitle = $("#ds-branch-title");
var $branchLocation = $("#ds-branch-location");
function getSelectedBranchData() {
if (!$branch.length) {
return { id: 0, title: "", location: "", image: "" };
}
var $opt = $branch.find("option:selected");
if (!$opt.length) {
return { id: 0, title: "", location: "", image: "" };
}
return {
id: parseInt($opt.val(), 10) || 0,
title: $opt.data("title") || $opt.text() || "",
location: $opt.data("location") || "",
image: $opt.data("image") || "",
};
}
var currentBranch = getSelectedBranchData();
function setLoading($el, on) {
if (on) {
$el.addClass("opacity-50 pointer-events-none");
} else {
$el.removeClass("opacity-50 pointer-events-none");
}
}
function toggleOverlay($overlay, on) {
if (!$overlay.length) return;
if (on) {
$overlay.removeClass("d-none");
} else {
$overlay.addClass("d-none");
}
}
function renderInlineLoader(message) {
var text = message
? '<span class="mt-2 text-muted">' + message + "</span>"
: "";
return (
'<div class="ds-inline-loader" role="status">' +
'<span class="ds-spinner" aria-hidden="true"></span>' +
text +
"</div>"
);
}
function loadCategories(branchId) {
setLoading($tabs, true);
toggleOverlay($categoryLoader, true);
$.post(DSFE.ajaxUrl, {
action: "ds_get_branch_categories",
_ajax_nonce: DSFE.nonce,
branch_id: parseInt(branchId, 10) || 0,
})
.done(function (resp) {
$tabs.empty();
if (!resp || !resp.success) {
$tabs.append(
'<div class="nav-item"><span class="category-nav-link disabled">No categories</span></div>'
);
$grid.html(
'<div class="row g-3"><div class="col-12"><em>Unable to load services for this branch.</em></div></div>'
);
return;
}
var cats =
resp.data && resp.data.categories ? resp.data.categories : [];
if (!cats.length) {
$tabs.append(
'<div class="nav-item"><span class="category-nav-link disabled">No categories</span></div>'
);
$grid.html(
'<div class="row g-3"><div class="col-12"><em>No services found for this branch.</em></div></div>'
);
return;
}
cats.forEach(function (c, idx) {
var active = idx === 0 ? "active" : "";
var li = $(
'<div class="card category-booking-card d-flex justify-content-center align-items-center mb-0 ' +
active +
'"><div class="nav-item card-body text-center p-3"></div></div>'
);
var a = $(
'<a class="category-nav-link text-center ' +
active +
'" href="#" data-cat-id="' +
c.id +
'">'
);
a.html(
$("<h5>").text(c.name)[0].outerHTML +
'<small class="text-muted">' +
(c.count || 0) +
" Services</small>"
);
li.find(".card-body").append(a);
$tabs.append(li);
});
// auto-load first category
var first = cats[0];
if (first) {
loadServices(branchId, first.id);
}
})
.fail(function () {
$tabs
.empty()
.append(
'<div class="nav-item"><span class="category-nav-link disabled">Failed to load categories.</span></div>'
);
})
.always(function () {
setLoading($tabs, false);
toggleOverlay($categoryLoader, false);
});
}
function loadServices(branchId, categoryId) {
setLoading($grid, true);
toggleOverlay($servicesLoader, true);
$.post(DSFE.ajaxUrl, {
action: "ds_get_services",
_ajax_nonce: DSFE.nonce,
branch_id: parseInt(branchId, 10) || 0,
category_id: parseInt(categoryId, 10) || 0,
})
.done(function (resp) {
if (!resp || !resp.success) {
$grid.html(
'<div class="col-12"><em>Failed to load services.</em></div>'
);
return;
}
$grid.html(resp.data && resp.data.html ? resp.data.html : "");
// Mark already selected services as Edit with tick
if (window.DSBookingCart) {
Object.keys(window.DSBookingCart).forEach(function (key) {
var sid = parseInt(key, 10);
if (sid) {
markServiceCardSelected(sid, true);
}
});
}
updateSummaryBar();
})
.fail(function () {
$grid.html(
'<div class="col-12"><em>Failed to load services.</em></div>'
);
})
.always(function () {
setLoading($grid, false);
toggleOverlay($servicesLoader, false);
});
}
// Branch change
$branch.on("change", function () {
var bid = $(this).val();
// Update header title/location from selected option data attributes
var $opt = $(this).find("option:selected");
var t = $opt.data("title") || $opt.text();
var loc = $opt.data("location") || "";
var img = $opt.data("image") || "";
if ($branchTitle.length) $branchTitle.text(t);
if ($branchLocation.length) $branchLocation.text(loc);
if ($("#ds-branch-image").length && img) {
$("#ds-branch-image").attr("src", img).attr("alt", t);
}
currentBranch = getSelectedBranchData();
loadCategories(bid);
});
// Category click
$tabs.on("click", "a.category-nav-link", function (e) {
e.preventDefault();
var $a = $(this);
$tabs.find("div.category-booking-card").removeClass("active");
$a.closest("div.category-booking-card").addClass("active");
$tabs.find("a.category-nav-link").removeClass("active");
$a.addClass("active");
var catId = $a.data("cat-id");
var bid = $branch.val();
loadServices(bid, catId);
});
// Drawer helpers
function openDrawer() {
$overlay.show();
requestAnimationFrame(function () {
$drawer.css("right", "0");
});
}
function closeDrawer() {
$drawer.css("right", "-420px");
$overlay.hide();
}
$(document).on("click", ".ds-drawer-close", function () {
closeDrawer();
});
$overlay.on("click", function () {
closeDrawer();
});
// In-memory cart for multi-service selection
var DSBookingCart = window.DSBookingCart || {};
// Restore from localStorage
try {
var saved = localStorage.getItem("DSBookingCart");
if (saved) {
DSBookingCart = JSON.parse(saved) || {};
window.DSBookingCart = DSBookingCart;
} else {
window.DSBookingCart = DSBookingCart;
}
} catch (e) {
window.DSBookingCart = DSBookingCart;
}
function parseNumber(v) {
var n = parseFloat(String(v).replace(/[^0-9.\-]/g, ""));
return isNaN(n) ? 0 : n;
}
function formatMoney(n) {
var sym = window.DSFE && DSFE.currencySymbol ? DSFE.currencySymbol : "$";
return sym + (Math.round(n * 100) / 100).toFixed(2);
}
function formatDuration(mins) {
mins = parseInt(mins, 10) || 0;
var h = Math.floor(mins / 60);
var m = mins % 60;
return h + " Hrs " + m + " Mins";
}
$(document).on("click", "#ds-summary-continue", function (e) {
e.preventDefault();
e.stopPropagation();
try {
var payload = JSON.stringify(DSBookingCart || {});
var keys = Object.keys(DSBookingCart || {});
if (!keys.length) {
window.location.href =
window.DSFE && DSFE.cartUrl ? DSFE.cartUrl : window.location.href;
return;
}
for (var i = 0; i < keys.length; i++) {
var it = DSBookingCart[keys[i]];
if (!it || !it.date || !it.slot) {
dtShowToast(
"Please select a date and time slot for all selected services before continuing.",
"error"
);
return;
}
}
$.post(DSFE.ajaxUrl, {
action: "ds_wc_add_cart",
_ajax_nonce: DSFE.nonce,
cart: payload,
})
.done(function (resp) {
if (resp && resp.success) {
try {
localStorage.removeItem("DSBookingCart");
} catch (_) {}
window.DSBookingCart = {};
if (window.DSFE && DSFE.cartUrl)
window.location.href = DSFE.cartUrl;
else window.location.reload();
} else {
var msg =
resp && resp.data && resp.data.message
? resp.data.message
: "Failed to add to cart.";
dtShowToast(msg, "error");
}
})
.fail(function () {
dtShowToast("Failed to add to cart. Please try again.", "error");
});
} catch (err) {
dtShowToast("Failed to add to cart.", "error");
}
});
function computeTotals() {
var count = 0,
totalMin = 0,
totalCost = 0;
Object.keys(DSBookingCart || {}).forEach(function (key) {
var it = DSBookingCart[key];
if (!it) return;
count++;
var basePrice = parseNumber(it.meta && it.meta.price);
var durMin = parseInt(it.meta && it.meta.durMin, 10) || 0;
var addonsPrice = 0,
addonsMin = 0;
(it.addonsCustom || []).forEach(function (a) {
addonsPrice += parseNumber(a.price);
addonsMin +=
(parseInt(a.dur_h, 10) || 0) * 60 + (parseInt(a.dur_m, 10) || 0);
});
totalMin += durMin + addonsMin;
totalCost += basePrice + addonsPrice;
});
return { count: count, totalMin: totalMin, totalCost: totalCost };
}
function updateSummaryBar() {
var elCount = document.getElementById("ds-summary-count");
var elDur = document.getElementById("ds-summary-duration");
var elTot = document.getElementById("ds-summary-total");
if (!(elCount && elDur && elTot)) return;
var t = computeTotals();
elCount.textContent = String(t.count);
elDur.textContent = formatDuration(t.totalMin);
elTot.textContent = formatMoney(t.totalCost);
var bar = document.getElementById("ds-summary-bar");
if (bar) {
if (t.count > 0) {
bar.style.display = ""; // Removes inline display to show the element
bar.classList.add("d-flex"); // Ensure d-flex class is applied
} else {
bar.style.display = "none"; // Hide the element
bar.classList.remove("d-flex"); // Remove d-flex class when hiding
}
}
}
function todayISO() {
var d = new Date();
var mm = (d.getMonth() + 1).toString().padStart(2, "0");
var dd = d.getDate().toString().padStart(2, "0");
return d.getFullYear() + "-" + mm + "-" + dd;
}
function setDrawerDefaults(sid) {
// Default date
var $date = $drawerBody.find(".ds-date-picker");
if ($date.length) {
var iso = todayISO();
if (!$date.val()) $date.val(iso);
$date.attr("min", iso);
}
// Prefill from cart
var item = DSBookingCart[sid];
if (!item) return;
// Addons (custom)
if (item.addonsCustom && item.addonsCustom.length) {
$drawerBody.find(".ds-addon-chip[data-addon]").each(function () {
try {
var data = $(this).data("addon");
if (typeof data === "string") data = JSON.parse(data);
var title = data && data.title ? data.title : null;
if (
title &&
item.addonsCustom.some(function (a) {
return a.title === title;
})
) {
$(this).addClass("active");
}
} catch (e) {}
});
}
// Amenities (taxonomy)
if (item.amenities && item.amenities.length) {
$drawerBody.find(".ds-addon-chip[data-term-id]").each(function () {
var tid = parseInt($(this).data("term-id"), 10);
if (item.amenities.indexOf(tid) !== -1) $(this).addClass("active");
});
}
// Staff
if (item.staff) {
$drawerBody.find(".ds-staff-chip").each(function () {
var uid = parseInt($(this).data("user-id"), 10);
if (uid === item.staff) $(this).addClass("active");
});
}
// Date
if (item.date && $date.length) {
$date.val(item.date);
}
// Slot
if (item.slot) {
$drawerBody.find(".ds-slot-chip").each(function () {
if ($(this).data("slot") === item.slot) $(this).addClass("active");
});
}
// After defaults, if date exists, load slots
var chosen = $drawerBody.find(".ds-date-picker").val();
if (chosen) {
fetchSlotsForDate(sid, chosen);
}
}
function markServiceCardSelected(sid, selected) {
var $card = $grid
.find('.ds-add-to-book[data-service-id="' + sid + '"]')
.closest(".card");
if (!$card.length) return;
var $btn = $card.find(".ds-add-to-book");
if (selected) {
$card.addClass("ds-selected");
// add tick badge if not exists
if (!$card.find(".ds-selected-badge").length) {
var badge = $('<span class="ds-selected-badge" />')
.html("✓")
.css({
position: "absolute",
top: "8px",
right: "8px",
background: "#16a34a",
color: "#fff",
width: "22px",
height: "22px",
borderRadius: "50%",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontSize: "12px",
});
$card.css("position", "relative");
$card.append(badge);
}
//$btn.text("Edit");
$btn.html('<i class="ti ti-edit"></i>');
} else {
$card.removeClass("ds-selected");
$card.find(".ds-selected-badge").remove();
$btn.text("+");
}
}
// Helper: read query param
function getQuery(name) {
try {
return new URLSearchParams(window.location.search).get(name);
} catch (e) {
return null;
}
}
// Reusable: open drawer and load booking content for a service id
function openServiceDrawer(sid) {
sid = parseInt(sid, 10) || 0;
if (!sid) return;
$drawerBody.html(renderInlineLoader("Loading service details..."));
openDrawer();
$.post(
DSFE.ajaxUrl,
{
action: "ds_get_service_booking",
_ajax_nonce: DSFE.nonce,
service_id: sid,
},
function (resp) {
if (!resp || !resp.success) {
$drawerBody.html(
renderInlineLoader("Failed to load service. Please try again.")
);
return;
}
$drawerBody.html(resp.data && resp.data.html ? resp.data.html : "");
setDrawerDefaults(sid);
$drawerBody.data("service-id", sid);
// Stamp currently selected branch metadata onto hidden meta block for later persistence
var $meta = $drawerBody.find("#ds-service-meta");
if ($meta.length && currentBranch) {
$meta.attr("data-branch-id", currentBranch.id || 0);
$meta.attr("data-branch-title", currentBranch.title || "");
$meta.attr("data-branch-location", currentBranch.location || "");
}
// init flatpickr if available
if (window.flatpickr) {
var $inline = $drawerBody.find(".ds-calendar-inline");
var $dp = $drawerBody.find(".ds-date-picker");
var today = todayISO();
if ($inline.length) {
var inlineFp = flatpickr($inline[0], {
inline: true,
dateFormat: "Y-m-d",
minDate: today,
defaultDate: $dp.val() || today,
monthSelectorType: "static",
disableMobile: true,
onChange: function (selectedDates, dateStr) {
if (dateStr) {
$dp.val(dateStr);
fetchSlotsForDate(sid, dateStr);
}
},
});
try {
var initDate = $dp.val() || today;
if (inlineFp && inlineFp.setDate)
inlineFp.setDate(initDate, true);
} catch (_) {}
} else if ($dp.length) {
var fp = flatpickr($dp[0], {
dateFormat: "Y-m-d",
minDate: today,
defaultDate: $dp.val() || today,
monthSelectorType: "static",
disableMobile: true,
onChange: function (selectedDates, dateStr) {
if (dateStr) {
fetchSlotsForDate(sid, dateStr);
}
},
});
try {
var initDate2 = $dp.val() || today;
if (fp && fp.setDate) {
fp.setDate(initDate2, true);
} else {
$dp.val(initDate2);
fetchSlotsForDate(sid, initDate2);
}
} catch (_) {}
}
}
// Show proper step
var $activeTab = $drawerBody.find(".btn-group [data-step].active");
$drawerBody.find(".ds-step").hide();
if ($activeTab.length) {
var step = $activeTab.data("step");
if (step === "addons") $drawerBody.find(".ds-step-addons").show();
else $drawerBody.find(".ds-step-datetime").show();
} else {
$drawerBody.find(".ds-step-addons").show();
}
}
);
}
// Open drawer on + click using helper
$grid.on("click", ".ds-add-to-book", function () {
var sid = $(this).data("service-id");
openServiceDrawer(sid);
});
// Allow image/title triggers to open drawer as well
function isDrawerTriggerKey(evt) {
var code = evt.key || evt.keyCode;
return code === "Enter" || code === " " || code === 13 || code === 32;
}
$grid.on("click", ".ds-service-trigger", function (e) {
e.preventDefault();
var sid = $(this).data("service-id");
openServiceDrawer(sid);
});
$grid.on("keydown", ".ds-service-trigger", function (e) {
if (!isDrawerTriggerKey(e)) return;
e.preventDefault();
var sid = $(this).data("service-id");
openServiceDrawer(sid);
});
// Helper: format slot label like '09:00 AM' from '09:00-09:30'
function formatSlotLabel(s) {
try {
if (!s) return "";
var raw = (s + "").trim();
// Normalize common range separators to '-'
raw = raw.replace(/\s+to\s+/i, "-").replace(/[–—]/g, "-");
// Extract the first time token (supports 9, 09, 9:00, 09:00, with or without AM/PM)
var match = raw.match(/(\d{1,2})(?::?(\d{2}))?\s*(AM|PM)?/i);
if (!match) return raw;
var h = parseInt(match[1], 10);
var m = match[2] ? match[2] : "00";
var ap = match[3] ? match[3].toUpperCase() : null;
if (!ap) {
ap = h >= 12 ? "PM" : "AM";
}
if (h === 0) h = 12;
else if (h > 12) h = h - 12;
var hh = h < 10 ? "0" + h : "" + h;
return hh + ":" + (m.length === 1 ? "0" + m : m) + " " + ap;
} catch (e) {
return s;
}
}
function formatDateLabel(dateStr) {
if (!dateStr) return "";
try {
var parts = dateStr.split("-");
if (parts.length !== 3) return dateStr;
var dateObj = new Date(parts[0], parts[1] - 1, parts[2]);
return dateObj.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
} catch (e) {
return dateStr;
}
}
function syncDrawerDateInputs(dateStr) {
if (!dateStr) return;
var $dateInput = $drawerBody.find(".ds-date-picker");
if ($dateInput.length && $dateInput.val() !== dateStr) {
$dateInput.val(dateStr);
}
var inlineCal = $drawerBody.find(".ds-calendar-inline");
if (inlineCal.length && inlineCal[0]._flatpickr) {
inlineCal[0]._flatpickr.setDate(dateStr, false);
} else if (
$dateInput.length &&
$dateInput[0] &&
$dateInput[0]._flatpickr
) {
$dateInput[0]._flatpickr.setDate(dateStr, false);
}
}
// Fetch slots for a given date
function fetchSlotsForDate(sid, dateStr) {
var $wrap = $drawerBody.find(".ds-slots-wrap");
if (!$wrap.length) return;
$wrap.empty().append(renderInlineLoader("Loading slots..."));
var staffId = 0;
var $activeStaff = $drawerBody.find(".ds-staff-chip.active");
if ($activeStaff.length) {
staffId = parseInt($activeStaff.first().data("user-id"), 10) || 0;
}
$.post(DSFE.ajaxUrl, {
action: "ds_get_service_slots_for_date",
_ajax_nonce: DSFE.nonce,
service_id: sid,
date: dateStr,
staff_id: staffId,
}).done(function (resp) {
$wrap.empty();
if (!resp || !resp.success) {
$wrap.append("<em>No slots available.</em>");
return;
}
var data = resp.data || {};
var slots = data.slots || [];
var assignedStaff = data.assigned_staff_id || 0;
var fallbackDate = data.next_available_date || "";
if (fallbackDate && fallbackDate !== dateStr) {
var info =
'<div class="alert alert-info w-100 small">' +
"No slots on " +
formatDateLabel(dateStr) +
". Showing next available on " +
formatDateLabel(fallbackDate) +
" instead." +
"</div>";
$wrap.append(info);
syncDrawerDateInputs(fallbackDate);
dateStr = fallbackDate;
}
if (!staffId && assignedStaff) {
var $chips = $drawerBody.find(".ds-staff-chip");
if ($chips.length) {
$chips.removeClass("active");
var $match = $chips.filter(
'[data-user-id="' + assignedStaff + '"]'
);
if ($match.length) {
$match.addClass("active");
}
}
}
if (!slots.length) {
if (!fallbackDate) {
$wrap.append("<em>No slots available.</em>");
}
return;
}
slots.forEach(function (s) {
var label = formatSlotLabel(s);
$wrap.append(
'<button type="button" class="btn border col-4 ds-slot-chip" data-slot="' +
s +
'">' +
label +
"</button>"
);
});
});
}
// Bind date change
$(document).on("change", "#ds-booking-drawer .ds-date-picker", function () {
var sid = $drawerBody.data("service-id");
var dateStr = $(this).val();
if (sid && dateStr) {
fetchSlotsForDate(sid, dateStr);
}
});
// Step switching inside drawer
$(document).on(
"click",
"#ds-booking-drawer .btn-group [data-step]",
function () {
var $btn = $(this);
var step = $btn.data("step");
var $grp = $btn.closest(".btn-group");
$grp.find(".btn").removeClass("active");
$btn.addClass("active");
var $wrap = $btn.closest("#ds-booking-drawer");
$wrap.find(".ds-step").hide();
if (step === "addons") {
$wrap.find(".ds-step-addons").show();
} else {
var $staffChips = $wrap.find(".ds-staff-chip");
if (
$staffChips.length &&
!$wrap.find(".ds-staff-chip.active").length
) {
if (typeof dtShowToast === "function") {
dtShowToast(
"Please select a staff member or tap Auto Assign before continuing.",
"info"
);
}
$grp.find(".btn[data-step='addons']").addClass("active");
$btn.removeClass("active");
$wrap.find(".ds-step-addons").show();
return;
}
$wrap.find(".ds-step-datetime").show();
// Ensure date defaults and slots load when switching to Date & Time
try {
var sid = $drawerBody.data("service-id");
var $dp = $wrap.find(".ds-date-picker");
var $inline = $wrap.find(".ds-calendar-inline");
var today = todayISO();
if ($inline.length && $inline[0]._flatpickr) {
var inlineInst = $inline[0]._flatpickr;
var current = $dp.val() || today;
inlineInst.setDate(current, true);
} else if ($dp.length) {
var inst = $dp[0] && $dp[0]._flatpickr ? $dp[0]._flatpickr : null;
var val = $dp.val();
var targetDate = val || today;
if (inst && inst.setDate) {
inst.setDate(targetDate, true);
} else {
$dp.val(targetDate).attr("min", today);
if (sid && targetDate) fetchSlotsForDate(sid, targetDate);
}
}
} catch (e) {}
}
}
);
// Basic interactions
// Addons: multi-select toggle
$(document).on("click", "#ds-booking-drawer .ds-addon-chip", function () {
$(this).toggleClass("active");
});
// Slots: single-select
$(document).on("click", "#ds-booking-drawer .ds-slot-chip", function () {
var $chip = $(this);
var $wrap = $chip.closest(".ds-slots-wrap");
$wrap.find(".ds-slot-chip").removeClass("active");
$chip.addClass("active");
});
// Single-select staff chips and refresh slots
$(document).on("click", "#ds-booking-drawer .ds-staff-chip", function () {
var $chip = $(this);
var wasActive = $chip.hasClass("active");
$chip
.closest("#ds-booking-drawer")
.find(".ds-staff-chip")
.removeClass("active");
if (!wasActive) $chip.addClass("active");
var sid = $drawerBody.data("service-id");
var dateStr = $drawerBody.find(".ds-date-picker").val();
if (sid && dateStr) fetchSlotsForDate(sid, dateStr);
});
// Auto-assign: pick first staff with available slots for chosen date
$(document).on("click", "#ds-booking-drawer .ds-auto-assign", function () {
var $btn = $(this);
var sid = $drawerBody.data("service-id");
var dateStr = $drawerBody.find(".ds-date-picker").val() || todayISO();
var $chips = $drawerBody.find(".ds-staff-chip");
if (!$chips.length || !sid) return;
var idx = 0;
var found = false;
var originalHtml = $btn.html();
$btn.prop("disabled", true).html("Finding...");
var $slotsWrap = $drawerBody.find(".ds-slots-wrap");
$slotsWrap
.empty()
.append(renderInlineLoader("Checking staff availability..."));
function finish() {
$btn.prop("disabled", false).html(originalHtml);
}
function tryNext() {
if (found) {
finish();
return;
}
if (idx >= $chips.length) {
// none found; clear selection and refresh generic slots
$drawerBody.find(".ds-staff-chip").removeClass("active");
fetchSlotsForDate(sid, dateStr);
finish();
return;
}
var $c = $chips.eq(idx++);
var staffId = parseInt($c.data("user-id"), 10) || 0;
$.post(DSFE.ajaxUrl, {
action: "ds_get_service_slots_for_date",
_ajax_nonce: DSFE.nonce,
service_id: sid,
date: dateStr,
staff_id: staffId,
})
.done(function (resp) {
var slots =
resp && resp.success && resp.data && resp.data.slots
? resp.data.slots
: [];
if (slots && slots.length) {
found = true;
// Trigger the normal selection flow so all handlers run
$drawerBody.find(".ds-staff-chip").removeClass("active");
$c.trigger("click");
// Render slots immediately for responsiveness
$slotsWrap.empty();
slots.forEach(function (s) {
var label = formatSlotLabel(s);
$slotsWrap.append(
'<button type="button" class="btn border ds-slot-chip" data-slot="' +
s +
'">' +
label +
"</button>"
);
});
finish();
} else {
tryNext();
}
})
.fail(function () {
tryNext();
});
}
tryNext();
});
$(document).on("click", "#ds-booking-drawer .ds-reset", function () {
$(
"#ds-booking-drawer .ds-addon-chip, #ds-booking-drawer .ds-staff-chip, #ds-booking-drawer .ds-slot-chip"
).removeClass("active");
});
$(document).on("click", "#ds-booking-drawer .ds-add-to-cart", function () {
var sid = $drawerBody.data("service-id");
if (!sid) return;
var item = {
serviceId: sid,
addonsCustom: [],
amenities: [],
staff: null,
date: null,
slot: null,
meta: {},
};
// meta from drawer
var metaEl = $drawerBody.find("#ds-service-meta");
if (metaEl.length) {
item.meta = {
title: metaEl.data("title") || "",
price: metaEl.data("price") || 0,
durMin: metaEl.data("dur-min") || 0,
thumb: metaEl.data("thumb") || "",
};
}
// collect add-ons custom
$drawerBody.find(".ds-addon-chip.active[data-addon]").each(function () {
try {
var data = $(this).data("addon");
if (typeof data === "string") data = JSON.parse(data);
if (data && data.title)
item.addonsCustom.push({
title: data.title,
price: data.price || "",
dur_h: data.dur_h || 0,
dur_m: data.dur_m || 0,
});
} catch (e) {}
});
// collect amenities
$drawerBody.find(".ds-addon-chip.active[data-term-id]").each(function () {
var tid = parseInt($(this).data("term-id"), 10);
if (tid) item.amenities.push(tid);
});
// staff
var $st = $drawerBody.find(".ds-staff-chip.active").first();
if ($st.length) item.staff = parseInt($st.data("user-id"), 10);
// date
var $date = $drawerBody.find(".ds-date-picker");
var dateVal = $date.length ? $date.val() : "";
var $slot = $drawerBody.find(".ds-slot-chip.active").first();
if (!dateVal) {
dtShowToast(
"Please select a date before adding this service.",
"error"
);
return;
}
if (!$slot.length) {
dtShowToast(
"Please select a time slot before adding this service.",
"error"
);
return;
}
item.date = dateVal;
item.slot = $slot.data("slot");
if (!currentBranch || !currentBranch.id) {
currentBranch = getSelectedBranchData();
}
item.branchId = currentBranch.id;
item.branchTitle = currentBranch.title;
item.branchLocation = currentBranch.location;
DSBookingCart[sid] = item;
try {
localStorage.setItem("DSBookingCart", JSON.stringify(DSBookingCart));
} catch (e) {}
markServiceCardSelected(sid, true);
closeDrawer();
updateSummaryBar();
});
// initial load
var initBranch = $branch.val();
if (initBranch) {
loadCategories(initBranch);
}
// On initial load, update summary from persisted cart
updateSummaryBar();
// Auto-open drawer if URL has ?service_id=ID
var preselectSid = getQuery("service_id");
if (preselectSid) {
openServiceDrawer(preselectSid);
}
// ===== Cart Page Render =====
function isCartPage() {
try {
if (document.querySelector("form.woocommerce-cart-form")) return true;
var href = window.location.href;
var path =
window.location.pathname +
window.location.search +
window.location.hash;
if (window.DSFE && DSFE.cartUrl && href.indexOf(DSFE.cartUrl) === 0)
return true;
return /(\/|^)cart(\/|\?|#|$)/i.test(path);
} catch (e) {
return false;
}
}
function renderAppointmentsCart() {
if (!isCartPage()) return;
var host = document.querySelector("#ds-appointments-cart");
if (!host) {
host = document.createElement("div");
host.id = "ds-appointments-cart";
var target =
document.querySelector(".woocommerce") ||
document.querySelector("#primary") ||
document.querySelector("main") ||
document.body;
target.insertBefore(host, target.firstChild);
}
var t = computeTotals();
var sym = window.DSFE && DSFE.currencySymbol ? DSFE.currencySymbol : "$";
var html = "";
html +=
'<a href="javascript:history.back()" class="d-inline-block mb-3">← Back</a>';
html += '<h3 class="mb-3">Your Appointments</h3>';
var keys = Object.keys(DSBookingCart || {});
if (!keys.length) {
html +=
'<div class="alert alert-info">No appointments in your cart yet.</div>';
}
keys.forEach(function (key) {
var it = DSBookingCart[key];
if (!it) return;
var dur = parseInt(it.meta && it.meta.durMin, 10) || 0;
(it.addonsCustom || []).forEach(function (a) {
dur +=
(parseInt(a.dur_h, 10) || 0) * 60 + (parseInt(a.dur_m, 10) || 0);
});
var price = parseNumber(it.meta && it.meta.price);
(it.addonsCustom || []).forEach(function (a) {
price += parseNumber(a.price);
});
html +=
'\n<div class="card mb-3">\n <div class="card-body d-flex justify-content-between align-items-start">';
html += '<div class="d-flex">';
if (it.meta && it.meta.thumb) {
html +=
'<img src="' +
it.meta.thumb +
'" width="72" height="72" class="rounded me-3" style="object-fit:cover;" />';
}
html += "<div>";
html +=
'<div class="fw-semibold">' +
(it.meta && it.meta.title ? it.meta.title : "Service") +
"</div>";
if (it.staff) {
html += '<div class="text-muted small">Staff #' + it.staff + "</div>";
}
if (it.date) {
html += '<div class="text-muted small">' + it.date + "</div>";
}
if (it.slot) {
html += '<div class="text-muted small">' + it.slot + "</div>";
}
html +=
'<div class="text-muted small">' + formatDuration(dur) + "</div>";
html += "</div></div>";
html += '<div class="text-end">';
html +=
'<div class="fw-semibold">' +
sym +
(Math.round(price * 100) / 100).toFixed(2) +
"</div>";
html += '<div class="mt-2">';
html +=
'<button class="btn btn-sm btn-outline-danger" data-ds-remove="' +
key +
'">Delete</button>';
html += "</div></div>";
html += "</div></div>";
});
html +=
'<div class="border-top pt-3 d-flex justify-content-between align-items-center">';
html +=
"<div>Services : " +
t.count +
" Duration : " +
formatDuration(t.totalMin) +
" Est. Total : " +
formatMoney(t.totalCost) +
"</div>";
html +=
'<a href="' +
(window.DSFE && DSFE.checkoutUrl ? DSFE.checkoutUrl : "#") +
'" class="btn btn-warning">Checkout</a>';
html += "</div>";
host.innerHTML = html;
}
document.addEventListener("click", function (e) {
var btn = e.target.closest("[data-ds-remove]");
if (!btn) return;
var sid = btn.getAttribute("data-ds-remove");
if (sid && DSBookingCart[sid]) {
delete DSBookingCart[sid];
try {
localStorage.setItem("DSBookingCart", JSON.stringify(DSBookingCart));
} catch (err) {}
updateSummaryBar();
// renderAppointmentsCart();
}
});
// Render if on cart page
// renderAppointmentsCart();
})();
});