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