Files
orderPRO/public/assets/js/modules/inline-status-change.js
Jacek Pyziak f2f1c44324 feat(v1.6): inline status change on orders list
Phase 44 complete:
- Clickable status badge opens dropdown with grouped statuses
- AJAX POST changes status without page reload (optimistic update)
- Fixed-position dropdown escapes table overflow:hidden
- updateStatus() returns JSON for AJAX, redirect for standard POST

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 09:40:06 +01:00

201 lines
6.4 KiB
JavaScript

(function () {
'use strict';
var activeDropdown = null;
var activeWrap = null;
function closeDropdown() {
if (activeDropdown) {
activeDropdown.remove();
activeDropdown = null;
activeWrap = null;
}
}
function buildBadgeHtml(statusCode, statusLabel, statusColor) {
var label = statusLabel || '-';
if (statusColor) {
return '<span class="order-tag" style="background-color:' + escapeAttr(statusColor) + ';color:#fff">' + escapeHtml(label) + '</span>';
}
var code = statusCode.toLowerCase();
var cls = 'is-neutral';
if (code === 'shipped' || code === 'delivered') cls = 'is-success';
else if (code === 'cancelled' || code === 'returned') cls = 'is-danger';
else if (code === 'new' || code === 'confirmed') cls = 'is-info';
else if (code === 'processing' || code === 'packed' || code === 'paid') cls = 'is-warn';
return '<span class="order-tag ' + cls + '">' + escapeHtml(label) + '</span>';
}
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function createDropdown(wrap, allStatuses, statusColorMap, csrfToken) {
closeDropdown();
var currentStatus = (wrap.getAttribute('data-current-status') || '').toLowerCase();
var orderId = wrap.getAttribute('data-order-id');
var dropdown = document.createElement('div');
dropdown.className = 'orders-status-dropdown';
var lastGroup = null;
for (var i = 0; i < allStatuses.length; i++) {
var s = allStatuses[i];
if (s.group && s.group !== lastGroup) {
var header = document.createElement('div');
header.className = 'orders-status-dropdown__group-header';
header.textContent = s.group;
dropdown.appendChild(header);
lastGroup = s.group;
}
var item = document.createElement('div');
item.className = 'orders-status-dropdown__item';
if (s.code.toLowerCase() === currentStatus) {
item.classList.add('is-current');
}
item.setAttribute('data-status-code', s.code);
var color = statusColorMap[s.code.toLowerCase()] || '#94a3b8';
var dot = document.createElement('span');
dot.className = 'orders-status-dropdown__color-dot';
dot.style.backgroundColor = color;
item.appendChild(dot);
var label = document.createElement('span');
label.textContent = s.name;
item.appendChild(label);
item.addEventListener('click', (function (statusCode) {
return function (e) {
e.stopPropagation();
changeStatus(wrap, orderId, statusCode, allStatuses, statusColorMap, csrfToken);
};
})(s.code));
dropdown.appendChild(item);
}
dropdown.addEventListener('click', function (e) {
e.stopPropagation();
});
document.body.appendChild(dropdown);
activeDropdown = dropdown;
activeWrap = wrap;
var wrapRect = wrap.getBoundingClientRect();
var dropdownHeight = dropdown.offsetHeight;
var spaceBelow = window.innerHeight - wrapRect.bottom - 8;
var spaceAbove = wrapRect.top - 8;
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
dropdown.style.top = (wrapRect.bottom + 4) + 'px';
} else {
dropdown.style.top = (wrapRect.top - dropdownHeight - 4) + 'px';
}
dropdown.style.left = wrapRect.left + 'px';
}
function changeStatus(wrap, orderId, newStatusCode, allStatuses, statusColorMap, csrfToken) {
var prevHtml = wrap.innerHTML;
var prevStatus = wrap.getAttribute('data-current-status');
closeDropdown();
var statusInfo = null;
for (var i = 0; i < allStatuses.length; i++) {
if (allStatuses[i].code === newStatusCode) {
statusInfo = allStatuses[i];
break;
}
}
if (statusInfo) {
var color = statusColorMap[newStatusCode.toLowerCase()] || '';
wrap.innerHTML = buildBadgeHtml(newStatusCode, statusInfo.name, color);
wrap.setAttribute('data-current-status', newStatusCode.toLowerCase());
}
var body = new FormData();
body.append('new_status', newStatusCode);
body.append('_token', csrfToken);
fetch('/orders/' + orderId + '/status', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: body
})
.then(function (resp) {
return resp.json().then(function (data) {
return { ok: resp.ok, data: data };
});
})
.then(function (result) {
if (!result.ok || !result.data.success) {
wrap.innerHTML = prevHtml;
wrap.setAttribute('data-current-status', prevStatus || '');
var msg = (result.data && result.data.error) ? result.data.error : 'Nie udalo sie zmienic statusu';
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
window.OrderProAlerts.alert({ title: 'Blad', message: msg });
}
return;
}
var d = result.data;
wrap.innerHTML = buildBadgeHtml(d.status_code, d.status_label, d.status_color);
wrap.setAttribute('data-current-status', d.status_code);
})
.catch(function () {
wrap.innerHTML = prevHtml;
wrap.setAttribute('data-current-status', prevStatus || '');
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
window.OrderProAlerts.alert({ title: 'Blad', message: 'Blad polaczenia z serwerem' });
}
});
}
document.addEventListener('click', function (e) {
if (!e.target || !e.target.closest) return;
var wrap = e.target.closest('.orders-status-wrap');
if (!wrap || !wrap.hasAttribute('data-order-id')) {
closeDropdown();
return;
}
if (activeDropdown && activeWrap === wrap) {
closeDropdown();
return;
}
var configEl = document.getElementById('js-inline-status-config');
if (!configEl) return;
var allStatuses, statusColorMap, csrfToken;
try {
var config = JSON.parse(configEl.textContent);
allStatuses = config.allStatuses || [];
statusColorMap = config.statusColorMap || {};
csrfToken = config.csrfToken || '';
} catch (ex) {
return;
}
e.stopPropagation();
createDropdown(wrap, allStatuses, statusColorMap, csrfToken);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeDropdown();
});
})();