Edit
diff --git a/admin/menu.js b/admin/menu.js
index 2709fd4..e04220b 100644
--- a/admin/menu.js
+++ b/admin/menu.js
@@ -16,7 +16,6 @@ function openItemModal(itemData = null) {
const priceField = document.getElementById('item-field-price');
const badgeField = document.getElementById('item-field-badge');
const descField = document.getElementById('item-field-desc');
- const sortField = document.getElementById('item-field-sort');
const availField = document.getElementById('item-field-available');
if (itemData) {
@@ -27,7 +26,6 @@ function openItemModal(itemData = null) {
priceField.value = itemData.price || '';
badgeField.value = itemData.badge || '';
descField.value = itemData.description || '';
- sortField.value = itemData.sort_order || 0;
availField.checked = parseInt(itemData.is_available, 10) === 1;
} else {
title.textContent = '✨ Add Menu Item';
@@ -36,7 +34,6 @@ function openItemModal(itemData = null) {
priceField.value = '';
badgeField.value = '';
descField.value = '';
- sortField.value = 0;
availField.checked = true;
}
@@ -53,20 +50,17 @@ function openCategoryModal(catData = null) {
const idField = document.getElementById('cat-field-id');
const nameField = document.getElementById('cat-field-name');
const descField = document.getElementById('cat-field-desc');
- const sortField = document.getElementById('cat-field-sort');
if (catData) {
title.textContent = '✏️ Edit Category';
idField.value = catData.id || 0;
nameField.value = catData.name || '';
descField.value = catData.description || '';
- sortField.value = catData.sort_order || 0;
} else {
title.textContent = '✨ Add Category';
idField.value = 0;
nameField.value = '';
descField.value = '';
- sortField.value = 0;
}
catModal.classList.add('is-open');
@@ -163,12 +157,84 @@ document.getElementById('categories-container').addEventListener('click', async
openCategoryModal({
id: target.dataset.id,
name: target.dataset.name,
- description: target.dataset.desc,
- sort_order: target.dataset.sort
+ description: target.dataset.desc
});
return;
}
+ // Move Item Up/Down
+ if (target.classList.contains('btn-move-item')) {
+ const direction = target.dataset.direction;
+ const row = target.closest('tr');
+ const tbody = row.parentElement;
+ const sibling = direction === 'up' ? row.previousElementSibling : row.nextElementSibling;
+ if (!sibling) return; // already at this edge; button should be disabled anyway
+
+ target.disabled = true;
+ const fd = new FormData();
+ fd.append('action', 'move_item');
+ fd.append('item_id', row.dataset.itemId);
+ fd.append('direction', direction);
+ fd.append('csrf_token', csrfToken);
+
+ try {
+ const data = await postForm(fd);
+ if (!data.success) {
+ alert(data.error || 'Failed to reorder item.');
+ target.disabled = false;
+ return;
+ }
+ if (direction === 'up') {
+ tbody.insertBefore(row, sibling);
+ } else {
+ tbody.insertBefore(sibling, row);
+ }
+ // Re-derives disabled state from DOM position rather than re-enabling
+ // `target` unconditionally, since the swap may have just moved it
+ // onto an edge (e.g. into the first/last slot) where it should stay disabled.
+ refreshMoveButtonStates(tbody);
+ } catch {
+ alert('Network error. Please try again.');
+ target.disabled = false;
+ }
+ return;
+ }
+
+ // Move Category Up/Down
+ if (target.classList.contains('btn-move-cat')) {
+ const direction = target.dataset.direction;
+ const panel = target.closest('.category-panel');
+ const container = panel.parentElement;
+ const sibling = direction === 'up' ? panel.previousElementSibling : panel.nextElementSibling;
+ if (!sibling) return;
+
+ target.disabled = true;
+ const fd = new FormData();
+ fd.append('action', 'move_category');
+ fd.append('category_id', target.dataset.id);
+ fd.append('direction', direction);
+ fd.append('csrf_token', csrfToken);
+
+ try {
+ const data = await postForm(fd);
+ if (!data.success) {
+ alert(data.error || 'Failed to reorder category.');
+ target.disabled = false;
+ return;
+ }
+ if (direction === 'up') {
+ container.insertBefore(panel, sibling);
+ } else {
+ container.insertBefore(sibling, panel);
+ }
+ refreshCategoryMoveButtonStates();
+ } catch {
+ alert('Network error. Please try again.');
+ target.disabled = false;
+ }
+ return;
+ }
+
// Toggle Availability (Mark Sold Out / Mark Available)
if (target.classList.contains('btn-toggle-item')) {
const row = target.closest('tr');
@@ -203,6 +269,7 @@ document.getElementById('categories-container').addEventListener('click', async
if (target.classList.contains('btn-delete-item')) {
if (!confirm('Are you sure you want to delete this menu item?')) return;
const row = target.closest('tr');
+ const tbody = row?.parentElement;
const fd = new FormData();
fd.append('action', 'delete_item');
@@ -215,7 +282,10 @@ document.getElementById('categories-container').addEventListener('click', async
row.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
row.style.opacity = '0';
row.style.transform = 'translateX(-10px)';
- setTimeout(() => row.remove(), 200);
+ setTimeout(() => {
+ row.remove();
+ refreshMoveButtonStates(tbody);
+ }, 200);
}
} catch {
alert('Failed to delete item.');
@@ -239,7 +309,10 @@ document.getElementById('categories-container').addEventListener('click', async
panel.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
panel.style.opacity = '0';
panel.style.transform = 'translateY(-10px)';
- setTimeout(() => panel.remove(), 200);
+ setTimeout(() => {
+ panel.remove();
+ refreshCategoryMoveButtonStates();
+ }, 200);
}
} catch {
alert('Failed to delete category.');
@@ -247,9 +320,64 @@ document.getElementById('categories-container').addEventListener('click', async
}
});
+// Sets the up/down move buttons' disabled state from each row's actual DOM
+// position, rather than tracking indices by hand — first row can't move up,
+// last can't move down, everything in between can do either. Call this
+// after any insert/remove/reorder in a category's tbody.
+function refreshMoveButtonStates(tbody) {
+ if (!tbody) return;
+ const rows = [...tbody.querySelectorAll('tr[data-item-id]')];
+ rows.forEach((row, index) => {
+ const upBtn = row.querySelector('.btn-move-item[data-direction="up"]');
+ const downBtn = row.querySelector('.btn-move-item[data-direction="down"]');
+ if (upBtn) upBtn.disabled = index === 0;
+ if (downBtn) downBtn.disabled = index === rows.length - 1;
+ });
+}
+
+// Same idea as refreshMoveButtonStates(), for category panels.
+function refreshCategoryMoveButtonStates() {
+ const panels = [...document.querySelectorAll('#categories-container .category-panel')];
+ panels.forEach((panel, index) => {
+ const upBtn = panel.querySelector('.btn-move-cat[data-direction="up"]');
+ const downBtn = panel.querySelector('.btn-move-cat[data-direction="down"]');
+ if (upBtn) upBtn.disabled = index === 0;
+ if (downBtn) downBtn.disabled = index === panels.length - 1;
+ });
+}
+
+function itemRowHtml(item, isAvail) {
+ return `
+
${isAvail ? '● Available' : '○ Sold Out / Hidden'}
+
${escapeHtml(item.name)}
${escapeHtml(item.description || '')}
+
${escapeHtml(item.price)}
+
${item.badge ? `${escapeHtml(item.badge)}` : '—'}
+
+
+
+
+
+
+
+ `;
+}
+
function updateOrInsertItemRow(item, isNew) {
const isAvail = parseInt(item.is_available, 10) === 1;
- const existingRow = document.getElementById(`item-row-${item.id}`);
+ let existingRow = document.getElementById(`item-row-${item.id}`);
+
+ // An edit can reassign an item to a different category — when that
+ // happens the row needs to move tables, not just update in place, so
+ // detach it here and let it fall through to the insert path below.
+ if (existingRow && existingRow.closest('table')?.id !== `cat-table-${item.category_id}`) {
+ const oldTbody = existingRow.parentElement;
+ existingRow.remove();
+ existingRow = null;
+ refreshMoveButtonStates(oldTbody);
+ if (oldTbody && !oldTbody.querySelector('tr[data-item-id]')) {
+ oldTbody.innerHTML = '
No items in this category yet.
';
+ }
+ }
if (existingRow) {
// Update existing row
@@ -261,7 +389,6 @@ function updateOrInsertItemRow(item, isNew) {
existingRow.querySelector('.item-name-cell').textContent = item.name;
existingRow.querySelector('.item-desc-cell').textContent = item.description || '';
existingRow.querySelector('.item-price-cell').textContent = item.price;
- existingRow.querySelector('.item-sort-cell').textContent = item.sort_order || 0;
const badgeCell = existingRow.querySelector('.item-badge-cell');
badgeCell.innerHTML = item.badge
@@ -272,7 +399,8 @@ function updateOrInsertItemRow(item, isNew) {
return;
}
- // Item was created or moved — insert into matching category table
+ // Item was created or moved to this category — it always joins at the
+ // end (see next_item_sort_order() server-side), so just append it.
const catTable = document.getElementById(`cat-table-${item.category_id}`);
if (!catTable) return;
@@ -283,21 +411,10 @@ function updateOrInsertItemRow(item, isNew) {
tr.id = `item-row-${item.id}`;
tr.dataset.itemId = item.id;
tr.dataset.item = JSON.stringify(item);
-
- tr.innerHTML = `
-
${isAvail ? '● Available' : '○ Sold Out / Hidden'}
-
${escapeHtml(item.name)}
${escapeHtml(item.description || '')}
-
${escapeHtml(item.price)}
-
${item.badge ? `${escapeHtml(item.badge)}` : '—'}
-
${item.sort_order || 0}
-
-
-
-
-
- `;
+ tr.innerHTML = itemRowHtml(item, isAvail);
tbody.appendChild(tr);
+ refreshMoveButtonStates(tbody);
}
// Static shell for a category panel. Only text-content positions use
@@ -314,6 +431,8 @@ function categoryPanelHtml(category) {
${escapeHtml(category.description || '')}
+
+
@@ -327,13 +446,12 @@ function categoryPanelHtml(category) {