diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php
index fc634323..eaa1c021 100644
--- a/resources/views/business/pages/partials/directory-table.blade.php
+++ b/resources/views/business/pages/partials/directory-table.blade.php
@@ -175,6 +175,11 @@
line-height: 1.35;
cursor: pointer;
}
+ .directory-field-picker__option[draggable="true"] { cursor: grab; }
+ .directory-field-picker__option.is-dragging { opacity: 0.55; }
+ .directory-field-picker__option.is-drop-target { outline: 1px solid var(--vip-content-accent, #b9954f); background: var(--vip-content-accent-soft, #f7f0de); }
+ .directory-field-picker__hint { grid-column: 1 / -1; color: var(--vip-content-muted, #595959); font-size: 0.6875rem; }
+ .directory-table-tools .directory-export-group { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.directory-field-picker__option input[type="checkbox"] {
flex: 0 0 auto;
width: 1rem;
@@ -485,16 +490,22 @@
-
-
+
+
+
+
+
+
+
@@ -625,17 +636,37 @@
section.dataset.directoryPage || 'page',
section.dataset.directoryTab || 'tab',
].join(':');
+ const defaultOrder = checks.map((check) => check.value);
+ const orderedChecks = () => [...(picker?.querySelectorAll('[data-directory-field-option]') || [])]
+ .map((option) => option.querySelector('[data-directory-field-check]'))
+ .filter(Boolean);
const applyColumns = (save = true) => {
- const selected = new Set(checks.filter((check) => check.checked).map((check) => check.value));
+ const ordered = orderedChecks();
+ const selected = new Set(ordered.filter((check) => check.checked).map((check) => check.value));
+ const rank = new Map(ordered.map((check, index) => [check.value, index]));
table.dataset.columnsReady = 'true';
table.querySelectorAll('[data-column-key]').forEach((cell) => {
cell.classList.toggle('is-column-hidden', !selected.has(cell.dataset.columnKey));
});
- if (save) localStorage.setItem(storageKey, JSON.stringify([...selected]));
+ [...table.tHead.rows].forEach((row) => {
+ [...row.children].sort((a, b) => (rank.get(a.dataset.columnKey) ?? -1) - (rank.get(b.dataset.columnKey) ?? -1))
+ .forEach((cell) => row.appendChild(cell));
+ });
+ table.tBodies[0]?.querySelectorAll('tr').forEach((row) => {
+ [...row.children].slice(1).sort((a, b) => (rank.get(a.dataset.columnKey) ?? -1) - (rank.get(b.dataset.columnKey) ?? -1))
+ .forEach((cell) => row.appendChild(cell));
+ });
+ if (save) localStorage.setItem(storageKey, JSON.stringify({ order: ordered.map((check) => check.value), selected: [...selected] }));
};
try {
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
- if (Array.isArray(saved)) checks.forEach((check) => { check.checked = saved.includes(check.value); });
+ const savedOrder = Array.isArray(saved) ? saved : saved?.order;
+ const savedSelected = Array.isArray(saved) ? saved : saved?.selected;
+ if (Array.isArray(savedSelected)) checks.forEach((check) => { check.checked = savedSelected.includes(check.value); });
+ if (Array.isArray(savedOrder)) savedOrder.forEach((key) => {
+ const option = picker.querySelector(`[data-directory-field-option] input[value="${CSS.escape(key)}"]`)?.closest('[data-directory-field-option]');
+ if (option) picker.querySelector('.directory-field-picker__panel').appendChild(option);
+ });
} catch (_) {}
applyColumns(false);
toggle?.addEventListener('click', () => {
@@ -644,28 +675,47 @@
toggle.setAttribute('aria-expanded', String(isHidden));
});
checks.forEach((check) => check.addEventListener('change', () => applyColumns()));
+ let dragging = null;
+ picker?.querySelectorAll('[data-directory-field-option]').forEach((option) => {
+ option.addEventListener('dragstart', () => { dragging = option; option.classList.add('is-dragging'); });
+ option.addEventListener('dragend', () => { dragging = null; option.classList.remove('is-dragging'); picker.querySelectorAll('.is-drop-target').forEach((item) => item.classList.remove('is-drop-target')); applyColumns(); });
+ option.addEventListener('dragover', (event) => { event.preventDefault(); if (dragging && dragging !== option) option.classList.add('is-drop-target'); });
+ option.addEventListener('dragleave', () => option.classList.remove('is-drop-target'));
+ option.addEventListener('drop', (event) => { event.preventDefault(); if (!dragging || dragging === option) return; option.parentNode.insertBefore(dragging, option); option.classList.remove('is-drop-target'); applyColumns(); });
+ });
+ const resetColumns = () => {
+ const options = [...picker.querySelectorAll('[data-directory-field-option]')];
+ const byKey = new Map(options.map((option) => [option.querySelector('[data-directory-field-check]').value, option]));
+ defaultOrder.forEach((key) => picker.querySelector('.directory-field-picker__panel').appendChild(byKey.get(key)));
+ checks.forEach((check) => { check.checked = !['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06'].includes(check.value); });
+ applyColumns();
+ };
+ const resetButton = tools.querySelector('[data-directory-reset]');
+ if (resetButton) resetButton.onclick = resetColumns;
search.addEventListener('input', () => {
const query = search.value.trim().toLocaleLowerCase();
table.querySelectorAll('tbody tr').forEach((row) => {
row.hidden = Boolean(query) && !row.textContent.toLocaleLowerCase().includes(query);
});
});
- tools.querySelector('[data-directory-export]')?.addEventListener('click', () => {
+ tools.querySelectorAll('[data-directory-export]').forEach((button) => button.addEventListener('click', () => {
+ const selected = button.dataset.directoryExport === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
const rows = [...table.querySelectorAll('tr')].filter((row) => !row.hidden);
- const csv = rows.map((row) => [...row.children].map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
+ const cellsForExport = (row) => [...row.children].filter((cell) => !cell.dataset.columnKey || !selected || selected.has(cell.dataset.columnKey));
+ const csv = rows.map((row) => cellsForExport(row).map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
- link.download = `${section.dataset.directoryPage || 'directory'}-export.csv`;
+ link.download = `${section.dataset.directoryPage || 'directory'}-current-export.csv`;
link.click();
URL.revokeObjectURL(link.href);
- });
- tools.querySelector('[data-directory-export-all]')?.addEventListener('click', async (event) => {
+ }));
+ tools.querySelectorAll('[data-directory-export-all]').forEach((button) => button.addEventListener('click', async (event) => {
const button = event.currentTarget;
const lastPage = Number(section.dataset.directoryLastPage || 1);
- const selected = new Set(checks.filter((check) => check.checked).map((check) => check.value));
+ const selected = button.dataset.directoryExportAll === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
const cellsForExport = (row) => [...row.children]
- .filter((cell) => !cell.dataset.columnKey || selected.has(cell.dataset.columnKey));
+ .filter((cell) => !cell.dataset.columnKey || !selected || selected.has(cell.dataset.columnKey));
const header = cellsForExport(table.tHead.rows[0]).map((cell) => csvEscape(cell.textContent.trim())).join(',');
const lines = [];
button.disabled = true;
@@ -696,7 +746,7 @@
button.disabled = false;
button.textContent = originalLabel;
}
- });
+ }));
tools.querySelector('[data-directory-template]')?.addEventListener('click', () => {
const header = [...table.querySelectorAll('thead th')].map((cell) => csvEscape(cell.textContent.trim())).join(',');
const blob = new Blob([`\ufeff${header}\n`], { type: 'text/csv;charset=utf-8' });
diff --git a/tests/Feature/Business/ManagementCrudTest.php b/tests/Feature/Business/ManagementCrudTest.php
index 99a9107d..5cfaded4 100644
--- a/tests/Feature/Business/ManagementCrudTest.php
+++ b/tests/Feature/Business/ManagementCrudTest.php
@@ -533,6 +533,18 @@ class ManagementCrudTest extends FeatureTestCase
$response->assertSeeText($header);
}
}
+
+ $this->get(route('business.pages.show', [
+ 'section' => 'store-management',
+ 'page' => 'store-report',
+ ]))
+ ->assertOk()
+ ->assertSee('data-directory-field-picker', false)
+ ->assertSee('data-directory-export="visible"', false)
+ ->assertSee('data-directory-export-all="all-fields"', false)
+ ->assertSee('data-directory-reset', false)
+ ->assertSee('position: sticky', false)
+ ->assertSee('overflow: auto', false);
}
private function mockUpstreamPage(object $row): void