(function($) {
/* =========================
* Конфигурация шагов
* ========================= */
const stepConfig = [
{ name: 'quantity', condition: () => true, render: renderQuantityStep, collect: collectQuantity },
{ name: 'ingredients', condition: r => r.hidden_attributes?.length && !answers.skipIngredients, render: renderIngredientsChoice, collect: collectIngredients },
{ name: 'otherCategory', condition: r => r.custom_fields?.offer_other_categories, render: renderOtherCategoriesChoice, collect: collectOtherCategory },
{ name: 'otherProduct', condition: (r,a) => a.otherCategory, render: renderOtherProductStep, collect: collectProductSelection },
{ name: 'sauceList', condition: (r,a) => r.custom_fields?.offer_other_categories && a.otherCategory !== 21,
render: body => renderProductList(() => `/wp-json/custom/v1/products/24`, 'Який соус бажаєте?', body),
collect: collectProductSelection },
{ name: 'drinkCategory', condition: r => r.custom_fields?.offer_drinks, render: renderDrinkCategoryChoice, collect: collectDrinkCategory },
{ name: 'drinkList', condition: (r,a) => a.drinkCategory && a.drinkCategory !== 'none',
render: body => renderProductList(() => `/wp-json/custom/v1/products/${answers.drinkCategory}`, 'Який напій бажаєте?', body),
collect: collectProductSelection },
{ name: 'dessertList', condition: r => r.custom_fields?.offer_donut,
render: body => renderProductListWithNone(() => `/wp-json/custom/v1/products/23`, 'Який пончик з’їш сьогодні?', body, 'Далі'),
collect: collectProductSelection }
];
/* =========================
* Служебные переменные
* ========================= */
let currentStep = 0;
let latestResponse = null;
let productId = null;
const answers = {};
const CART_URL = '/cart/';
let productMeta = { name: '', image: '', link: '#' };
/* =========================
* Утилиты
* ========================= */
function escapeHtml(s=''){return String(s).replace(/[&"']/g, m=>({'&':'&','':'>','"':'"',"'":'''}[m]))}
function debounce(fn, wait){ let t; return function(...a){ clearTimeout(t); t=setTimeout(()=>fn.apply(this,a), wait); }; }
function updateCartItemQty(itemKey, qty){
if (!window.HULI_AJAX || !HULI_AJAX.url || !HULI_AJAX.nonce) {
console.error('[Cart] HULI_AJAX не инициализирован (проверь wp_head/wp_enqueue_scripts).');
const d = $.Deferred(); d.reject({ localError: 'HULI_AJAX_MISSING' }); return d.promise();
}
qty = parseInt(qty, 10); if (isNaN(qty) || qty < 0) qty = 0;
return $.post(HULI_AJAX.url, {
action: 'huli_cart_set_qty',
nonce: HULI_AJAX.nonce,
key: itemKey,
qty: qty
});
}
// «Далі» в футере — обычный переход
function setFooterNext(onNext) {
const $next = $('#productModalNext');
$next.text('Далі').off('click').on('click', onNext).show();
$('#productModalLink').hide();
}
// «Далі» как «Нічого не бажаю» (скип на шагах выбора)
function setFooterNextAsNone(onNone, label) {
const $next = $('#productModalNext');
$next.text(label || 'Далі').off('click').on('click', onNone).show();
$('#productModalLink').hide();
}
// Полностью спрятать футерные кнопки
function hideFooterAll() {
$('#productModalNext').hide().off('click').text('Далі');
$('#productModalLink').hide();
}
// Кнопка «Детальніше» в ТЕЛЕ шага (стили как у «Далі»)
function makeInlineDetailsBtn(href, label) {
const $btn = $(`
${label || 'Детальніше'}
`);
$btn.attr('href', href || '#');
return $btn;
}
function formatStoreAmount(amountStr, meta = {}, opts = {}) {
if (amountStr == null) return '';
const minor = parseInt(amountStr, 10);
if (isNaN(minor)) return '';
const mu = Number(meta.currency_minor_unit ?? 2); // 2 для UAH
const decSep = meta.currency_decimal_separator ?? ','; // из API
const thouSep = meta.currency_thousand_separator ?? ' '; // из API
const prefix = meta.currency_prefix ?? ''; // обычно пусто
const suffix = meta.currency_suffix
?? (meta.currency_symbol ? ` ${meta.currency_symbol}` : ''); // " ₴"
const fixed = (minor / Math.pow(10, mu)).toFixed(mu); // "55.00"
let [intPart, decPart] = fixed.split('.');
// тысячные разряды
intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thouSep);
// заменить точку на локальный разделитель
let number = mu > 0 ? intPart + (decPart ? decSep + decPart : '') : intPart;
// если хочешь скрывать .00 — поставь opts.stripZeros = true
if (opts.stripZeros && decPart && /^0+$/.test(decPart)) {
number = intPart;
}
return `${prefix}${number}${suffix}`;
}
/* =========================
* Рендеры шагов
* ========================= */
// Render and collect functions
function renderQuantityStep($body, resp, answersObj) {
// Определяем, какие метаданные использовать
const meta = resp?.selectedMeta || productMeta;
const title = meta.name || resp?.product_name || '';
const description = meta.shortDesc || resp?.product_short_description || '';
const price = meta.price
? `
${meta.price} ₴
`
: (resp?.product_price ? `
${resp.product_price} ₴
` : '');
// Шапка с картинкой и описанием
const headerHtml = `
`;
// Поле количества
const qtyHtml = `
Кількість:
`;
// Собираем контент
$body.html(headerHtml + qtyHtml);
// Кнопки и управление
const $ctrl = $('
');
if (resp.hidden_attributes && resp.hidden_attributes.length > 0) {
$ctrl.append(`
Змінити інгредієнти
`);
$ctrl.find('#qty-change-ingredients').on('click', () => {
// Для основного товара
if (!resp.selectedMeta) {
answers.skipIngredients = false;
} else {
// Для подтовара
answers.otherSkipIngredients = false;
}
nextStep();
});
}
// Кнопка «Детальніше»
$ctrl.append(makeInlineDetailsBtn(meta.link || productMeta.link));
$body.append($ctrl);
// Кнопка «Далі» в футере
setFooterNext(() => {
if (!resp.selectedMeta) {
// Основной товар
answers.skipIngredients = true;
} else {
// Подтовар
answers.otherSkipIngredients = true;
}
nextStep();
});
}
function renderIngredientsChoice($body, resp) {
let addHtml = '', removeHtml = '';
(resp.hidden_attributes || []).forEach(attr => {
const tpl = `
${attr.name}
`;
if (attr.value === '+') addHtml += tpl; else removeHtml += tpl;
});
$body.html(`
Додати інгредієнти:
Видалити інгредієнти:
`);
// «Детальніше» в теле шага
$('#ingr-inline-controls').append(makeInlineDetailsBtn(productMeta.link));
// «Далі» — в футере
setFooterNext(() => {
const data = collectIngredients();
nextStep(data);
});
}
function fetchCategoryMeta(catId) {
return $.get(`/wp-json/custom/v1/products/${catId}`)
.then(data => {
const name = data.category_name || `Категорія ${catId}`;
const image = (data.category_image && data.category_image.trim())
? data.category_image
: (data.products && data.products.length ? data.products[0].image : null);
return { id: catId, name, image };
})
.catch(() => ({ id: catId, name: `Категорія ${catId}`, image: null }));
}
function renderDrinkCategoryChoice($body /*, resp, answers */) {
const catIds = [26];
$body.html(`
`);
Promise.all(catIds.map(fetchCategoryMeta)).then(list => {
let html = `
Оберіть категорію напоїв:
`;
html += list.map(o => `
${o.image ? `
`
: `
(без зображення)
`}
${o.name}
`).join('');
html += `
Далі
`;
$body.html(html);
$body.find('.drink-cat').on('click', e => nextStep($(e.currentTarget).data('id')));
$body.find('#drink-none').on('click', () => nextStep('none'));
// ФУТЕР: «Далі» = «Нічого не бажаю»
setFooterNextAsNone(() => nextStep('none'), 'Далі');
});
}
function renderOtherCategoriesChoice($body, resp) {
let html = '
Що бажаєте додати? ';
(resp.custom_fields?.other_categories_array || []).forEach(cat => {
html += `
${cat.image ? `
` : ''}
${cat.name}
`;
});
html += `
`;
$body.html(html);
$body.find('.other-category').on('click', e => nextStep($(e.currentTarget).data('id')));
$body.find('#other-none').on('click', () => nextStep(null));
// ФУТЕР: «Далі» = «Нічого не бажаю»
setFooterNextAsNone(() => nextStep(null), 'Далі');
}
function renderOtherProductStep($body) {
renderProductList(
() => `/wp-json/custom/v1/products/${answers.otherCategory}`,
'Оберіть товар:',
$body
);
// setFooterNextAsNone будет подключен внутри renderProductList
}
function renderProductListWithNone(urlFn, title, $body, noneLabel = 'Далі') {
return renderProductList(urlFn, title, $body, noneLabel);
}
function renderProductList(urlFn, title, $body, noneLabel = 'Далі') {
$.get(urlFn(), data => {
let html = `
${title}
`;
(data.products || []).forEach((p) => {
html += `
${p.name}
${p.price} ₴
`;
});
html += `
`;
$body.html(html);
$body.find('.product-item').on('click', e => {
const id = $(e.currentTarget).data('id');
nextStep(id);
});
// ФУТЕР: «Далі» = «Нічого не бажаю» (скип)
setFooterNextAsNone(() => nextStep('none'), noneLabel);
});
}
/* =========================
* Сбор данных
* ========================= */
function collectQuantity() {
answers.quantity = parseInt($('#step-quantity').val(), 10) || 1;
return answers.quantity;
}
function collectIngredients() {
const added = [], removed = [];
$('.add-ingredient:checked').each((_, el) => added.push(el.value));
$('.remove-ingredient:checked').each((_, el) => removed.push(el.value));
answers.ingredients = { added, removed };
return answers.ingredients;
}
function collectOtherCategory() {
return answers.otherCategory; // заполнится через processNext(prev.name)=answer
}
function collectProductSelection(id) {
return (id === 'none' || id == null) ? null : id;
}
function collectDrinkCategory(selected) {
answers.drinkCategory = selected; // 26 | 'none'
return answers.drinkCategory;
}
/* =========================
* Финализация и корзина
* ========================= */
function renderCartSummary() {
hideFooterAll();
const $body = $('#productModalBody');
$body.html(`
`);
$.get('/wp-json/wc/store/cart', data => {
const items = (data && data.items) || [];
let list = '';
items.forEach(it => {
const img = (it.images && it.images[0] && it.images[0].src) || '';
const name = it.name || 'Товар';
const qty = it.quantity || 1;
const line = formatStoreAmount(it.totals?.line_total, it.totals, { stripZeros: true });
const key = it.key; // ключ позиции в корзине (нужен для апдейта)
list += `
${img ? `
` : ''}
${line}
`;
});
const totalsHtml = data?.totals?.total_price
? `
Разом: ${formatStoreAmount(data.totals.total_price, data.totals, { stripZeros: true })}
`
: '';
$body.html(`
Ваше замовлення
${list || '
Кошик порожній.
'}
${totalsHtml}
`);
$('#modal-continue-shopping').on('click', () => $('#productModal').modal('hide'));
// Делегирование событий для qty
const applyChange = debounce(($row, newQty) => {
const key = $row.data('key');
const $inputs = $row.find('.qty-input, .qty-minus, .qty-plus').prop('disabled', true);
updateCartItemQty(key, newQty)
.always(() => $inputs.prop('disabled', false))
.done(() => renderCartSummary())
.fail(() => renderCartSummary());
}, 300);
$body.off('click.qtyMinus').on('click.qtyMinus', '.qty-minus', function(){
const $row = $(this).closest('.mini-cart-row');
const $in = $row.find('.qty-input');
const next = Math.max(0, (parseInt($in.val(),10)||0) - 1);
$in.val(next); applyChange($row, next);
});
$body.off('click.qtyPlus').on('click.qtyPlus', '.qty-plus', function(){
const $row = $(this).closest('.mini-cart-row');
const $in = $row.find('.qty-input');
const next = (parseInt($in.val(),10)||0) + 1;
$in.val(next); applyChange($row, next);
});
$body.off('input.qtyInput change.qtyInput').on('input.qtyInput change.qtyInput', '.qty-input', function(){
const $row = $(this).closest('.mini-cart-row');
const val = parseInt(this.value, 10);
if (!Number.isNaN(val) && val >= 0) applyChange($row, val);
});
}).fail(() => {
$body.html(`
Не вдалося завантажити кошик.
`);
$('#modal-continue-shopping').on('click', () => $('#productModal').modal('hide'));
});
}
function finalizeAddToCart() {
hideFooterAll();
const mainData = {
product_id: productId,
quantity: answers.quantity || 1
};
if (answers.ingredients) {
mainData.ingredients_added = answers.ingredients.added || [];
mainData.ingredients_removed = answers.ingredients.removed || [];
mainData.comment = `Додано інгредієнти: ${(answers.ingredients.added||[]).join(', ')}; Видалено інгредієнти: ${(answers.ingredients.removed||[]).join(', ')}`;
}
$.post('/?wc-ajax=add_to_cart', mainData)
.done(() => {
let seq = $.Deferred().resolve();
['sauceList','drinkList','dessertList'].forEach(key => {
if (answers[key]) {
seq = seq.then(() => $.post('/?wc-ajax=add_to_cart', {
product_id: answers[key],
quantity: 1
}));
}
});
seq.always(() => renderCartSummary());
})
.fail(() => renderCartSummary());
}
/* =========================
* Навигация по шагам
* ========================= */
function processNext(answer) {
if (answer !== undefined) {
const prev = stepConfig[currentStep-1];
answers[prev.name] = answer;
}
while (currentStep = stepConfig.length) return finalizeAddToCart();
const step = stepConfig[currentStep++];
step.render($('#productModalBody'), latestResponse, answers);
}
function nextStep(arg) { processNext(arg); }
function resetProductModal() { $('#productModalBody').empty(); }
/* =========================
* Точка входа
* ========================= */
$(document).on('click', '#product_listing .item', function(e) {
e.preventDefault();
resetProductModal();
productId = $(this).find('[data-product_id]').data('product_id');
// Из карточки товара тянем метаданные
const $it = $(this);
const $img = $it.find('img').first();
const imgSrc = $img.attr('src') || $img.attr('data-src') || $img.attr('data-lazy') ||
(($img.attr('srcset') || '').split(',')[0] || '').trim().split(' ')[0];
productMeta = {
name: ($it.find('.title, .product-title').first().text() || $it.data('name') || '').trim(),
image: imgSrc || $it.data('image') || '',
link: $it.find('a').first().attr('href') || '#'
};
if (productMeta.name) $('#productModalTitle').text(productMeta.name);
$('#productModalLink').attr('href', productMeta.link);
$.get(`/wp-json/custom/v1/product-category/${productId}`, resp => {
latestResponse = resp;
currentStep = 0;
Object.keys(answers).forEach(k => delete answers[k]);
processNext();
});
$('#productModal').modal('show');
});
})(jQuery);