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>
This commit is contained in:
File diff suppressed because one or more lines are too long
200
public/assets/js/modules/inline-status-change.js
Normal file
200
public/assets/js/modules/inline-status-change.js
Normal file
@@ -0,0 +1,200 @@
|
||||
(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, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user