{% extends "base.html" %} {% block title %}{{ verbose_name_plural }}{% endblock %} {% block extra_head %} {% endblock %} {% block content %}
{# ── Breadcrumb ── #}
Dashboard {{ verbose_name_plural }}
{# ── Hero Header ── #}

{{ verbose_name_plural }}

{{ total }} record{{ 's' if total != 1 else '' }} Page {{ page }}/{{ total_pages }} {% if active_filters %}{{ active_filters|length }} filter{{ 's' if active_filters|length != 1 else '' }} active{% endif %}
{# ── Stats Cards ── #}
Total Records
{{ total }}
Across all pages
Current Page
{{ page }} / {{ total_pages }}
{{ per_page }} per page
Columns
{{ list_display|length }}
{{ list_editable|length }} editable
Active Filters
{{ active_filters|length if active_filters else 0 }}
{{ list_filter|length if list_filter else 0 }} available
{# ── Search Bar ── #}
{# ── Toolbar ── #}
Visible Columns
{% for col in list_display %} {% endfor %}
Show
{{ per_page }}
{% for n in [10, 25, 50, 100] %}
{{ n }}
{% endfor %}
{{ total }} record{{ 's' if total != 1 else '' }}
{# ── Active filter pills ── #} {% if active_filters %}
Active: {% for key, val in active_filters.items() %} {{ key.replace('filter__', '').replace('__gte', ' from').replace('__lte', ' to').replace('_', ' ')|title }}: {{ val }} {% endfor %}
{% endif %} {# ── Bulk actions bar ── #}
0 selected
{% if csrf_token %}{% endif %}
— Select action —
— Select action —
{% if actions %} {% for name, action_desc in actions.items() %}
{{ action_desc.short_description }}
{% endfor %} {% endif %}
{# ── Data table (table view) ── #}
{# ── Grid View (card mode) ── #}
{% for row in rows %}
{% for col in list_display %} {% set val = row[col]|default('—') %}
{{ col.replace('_', ' ')|title }} {{ val|string|truncate(60, True) }}
{% endfor %}
{% else %}
No records found
{% if search %}Try adjusting your search query.{% else %}Add your first record to get started.{% endif %}
Add {{ verbose_name }}
{% endfor %}
{# ── Pagination ── #} {% if total_pages > 0 %}
Show
{{ per_page }}
{% for n in [10, 25, 50, 100] %}
{{ n }}
{% endfor %}
per page
{% if total_pages > 1 %} {% endif %}
Showing {{ ((page-1)*per_page)+1 }}–{{ [page*per_page, total]|min }} of {{ total }}
{% endif %}
{# .list-main #} {# ═══ FILTER OVERLAY PANEL ═════════════════════════════════════ #}
Filters
{% if list_filter %}
{% if search %}{% endif %} {% if ordering %}{% endif %} {% for f_field in list_filter %} {% set meta = filter_metadata.get(f_field, {}) %} {% set f_type = meta.get('type', 'text') %} {% set f_label = meta.get('label', f_field.replace('_', ' ')|title) %} {% set f_choices = meta.get('choices', []) %}
{% if f_type == 'checkbox' %} {% elif f_type in ('datetime-local', 'date') %} {% elif f_type == 'time' %} {% elif f_type == 'select' %} {% elif f_type == 'number' %} {% else %} {% endif %} {{ f_label }}
{% if f_type == 'checkbox' %}
{% set cur_val = active_filters.get(f_field, '') %}
{% elif f_type in ('datetime-local', 'date') %}
{% elif f_type == 'time' %}
to
{% elif f_type == 'select' or f_choices %}
{% if active_filters.get(f_field) %}{{ active_filters.get(f_field) }}{% else %}All{% endif %}
All
{% for choice in f_choices %} {% if choice is iterable and choice is not string %}
{{ choice[1] }}
{% else %}
{{ choice }}
{% endif %} {% endfor %}
{% elif f_type == 'number' %}
to
{% else %} {% endif %}
{% endfor %}
{% else %}

No filter fields configured for this model.

{% endif %}
{# .filter-panel #}
{# .filter-overlay #} {# ── Keyboard Shortcuts Hint ── #}
⌘K Search N New record J/K Navigate rows Enter Open selected X Expand row F Filters V Toggle view ⌘A Select all ? Toggle shortcuts
{# ── Scroll to Top ── #}
{# .list-layout #} {% endblock %} {% block extra_js %} var BASE_URL = '{{ url_prefix|default("/admin") }}/{{ model_name|lower }}'; var LIST_DISPLAY = {{ list_display|tojson }}; var FIELD_TYPES = {{ field_types|tojson }}; var LIST_EDITABLE = {{ list_editable|tojson }}; var CURRENT_PAGE = {{ page }}; var CURRENT_PER_PAGE = {{ per_page }}; var CURRENT_ORDERING = '{{ ordering|default("") }}'; /* ── Custom Select helpers ── */ function toggleCustomSelect(trigger) { var wrap = trigger.closest('.custom-select'); var wasOpen = wrap.classList.contains('open'); closeAllCustomSelects(); if (!wasOpen) wrap.classList.add('open'); } function selectCustomOption(opt, callback) { var wrap = opt.closest('.custom-select'); var label = wrap.querySelector('.custom-select-label'); var siblings = wrap.querySelectorAll('.custom-select-option'); siblings.forEach(function(s){ s.classList.remove('selected'); }); opt.classList.add('selected'); label.textContent = opt.textContent; label.style.color = ''; label.style.fontStyle = ''; wrap.classList.remove('open'); if (callback) callback(opt.getAttribute('data-value')); } function selectFilterOption(opt, hiddenName) { var wrap = opt.closest('.custom-select'); var label = wrap.querySelector('.custom-select-label'); var siblings = wrap.querySelectorAll('.custom-select-option'); siblings.forEach(function(s){ s.classList.remove('selected'); }); opt.classList.add('selected'); label.textContent = opt.textContent; wrap.classList.remove('open'); var hidden = wrap.parentElement.querySelector('input[name="' + hiddenName + '"]'); if (hidden) hidden.value = opt.getAttribute('data-value'); } function closeAllCustomSelects() { document.querySelectorAll('.custom-select.open').forEach(function(el){ el.classList.remove('open'); }); } document.addEventListener('click', function(e) { if (!e.target.closest('.custom-select')) closeAllCustomSelects(); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeAllCustomSelects(); }); function buildListUrl(overrides) { var params = new URLSearchParams(window.location.search); for (var k in overrides) { if (overrides[k] === '' || overrides[k] === null || overrides[k] === undefined) params.delete(k); else params.set(k, overrides[k]); } return BASE_URL + '/?' + params.toString(); } function openFilterPanel() { document.getElementById('filterOverlay').classList.add('open'); } function closeFilterPanel() { document.getElementById('filterOverlay').classList.remove('open'); } function toggleFilterSidebar() { var o = document.getElementById('filterOverlay'); if (o.classList.contains('open')) closeFilterPanel(); else openFilterPanel(); } function setBoolFilter(field, val) { var params = new URLSearchParams(window.location.search); if (val) params.set('filter__' + field, val); else params.delete('filter__' + field); params.set('page', '1'); window.location.href = BASE_URL + '/?' + params.toString(); } function setDatePreset(field, preset, ftype) { var now = new Date(), from = new Date(); if (preset === 'today') from = new Date(now.getFullYear(), now.getMonth(), now.getDate()); else if (preset === '7d') from.setDate(now.getDate() - 7); else if (preset === '30d') from.setDate(now.getDate() - 30); else if (preset === '90d') from.setDate(now.getDate() - 90); else if (preset === 'year') from = new Date(now.getFullYear(), 0, 1); var fromStr, toStr; if (ftype === 'date') { fromStr = from.toISOString().split('T')[0]; toStr = now.toISOString().split('T')[0]; } else { fromStr = from.toISOString().slice(0, 16); toStr = now.toISOString().slice(0, 16); } var g = document.querySelector('input[name="filter__' + field + '__gte"]'); var l = document.querySelector('input[name="filter__' + field + '__lte"]'); if (g) g.value = fromStr; if (l) l.value = toStr; } function removeFilter(key) { var params = new URLSearchParams(window.location.search); params.delete(key); if (!key.startsWith('filter__')) params.delete('filter__' + key); params.set('page', '1'); window.location.href = BASE_URL + '/?' + params.toString(); } function clearAllFilters() { var params = new URLSearchParams(window.location.search); var rm = []; params.forEach(function(v, k) { if (k.startsWith('filter__')) rm.push(k); }); rm.forEach(function(k) { params.delete(k); }); params.set('page', '1'); window.location.href = BASE_URL + '/?' + params.toString(); } function changePerPage(val) { window.location.href = buildListUrl({ per_page: val, page: '1' }); } function goToPage(p) { window.location.href = buildListUrl({ page: p }); } // ── View Mode Toggle (Table / Grid) ──────────────────────────── function setViewMode(mode) { var tw = document.getElementById('tableViewWrap'), gw = document.getElementById('gridViewWrap'); var tb = document.getElementById('viewTableBtn'), gb = document.getElementById('viewGridBtn'); if (mode === 'grid') { if (tw) tw.style.display = 'none'; if (gw) gw.style.display = 'grid'; if (tb) tb.classList.remove('active'); if (gb) gb.classList.add('active'); } else { if (tw) tw.style.display = ''; if (gw) gw.style.display = 'none'; if (tb) tb.classList.add('active'); if (gb) gb.classList.remove('active'); } try { localStorage.setItem('aq_view_mode_{{ model_name|lower }}', mode); } catch(e) {} } (function() { try { var m = localStorage.getItem('aq_view_mode_{{ model_name|lower }}'); if (m === 'grid') setViewMode('grid'); } catch(e) {} })(); // ── Column Visibility ────────────────────────────────────────── (function() { var btn = document.getElementById('colToggleBtn'), panel = document.getElementById('colTogglePanel'); if (!btn || !panel) return; btn.addEventListener('click', function(e) { e.stopPropagation(); panel.classList.toggle('open'); }); document.addEventListener('click', function(e) { if (!panel.contains(e.target) && e.target !== btn) panel.classList.remove('open'); }); })(); function toggleColumn(col, show) { document.querySelectorAll('th[data-col="' + col + '"]').forEach(function(h) { h.style.display = show ? '' : 'none'; }); document.querySelectorAll('td[data-col="' + col + '"]').forEach(function(c) { c.style.display = show ? '' : 'none'; }); try { var p = JSON.parse(localStorage.getItem('aq_col_vis_{{ model_name|lower }}') || '{}'); p[col] = show; localStorage.setItem('aq_col_vis_{{ model_name|lower }}', JSON.stringify(p)); } catch(e) {} } (function() { try { var p = JSON.parse(localStorage.getItem('aq_col_vis_{{ model_name|lower }}') || '{}'); for (var col in p) { if (p[col] === false) { toggleColumn(col, false); var cb = document.querySelector('.col-toggle-item input[data-col="' + col + '"]'); if (cb) cb.checked = false; } } } catch(e) {} })(); function restoreColumnVisibility() { try { var p = JSON.parse(localStorage.getItem('aq_col_vis_{{ model_name|lower }}') || '{}'); for (var col in p) { if (p[col] === false) document.querySelectorAll('td[data-col="' + col + '"]').forEach(function(c) { c.style.display = 'none'; }); } } catch(e) {} } // ── Server-side Sorting ──────────────────────────────────────── var _sortCol = CURRENT_ORDERING.replace(/^-/, '') || null; var _sortDir = CURRENT_ORDERING.startsWith('-') ? 'desc' : 'asc'; function sortColumn(col, e) { if (e && e.target.closest && e.target.closest('.col-toggle-panel')) return; if (_sortCol === col) _sortDir = _sortDir === 'asc' ? 'desc' : 'asc'; else { _sortCol = col; _sortDir = 'asc'; } window.location.href = buildListUrl({ ordering: (_sortDir === 'desc' ? '-' : '') + col, page: '1' }); } (function() { if (!_sortCol) return; var el = document.getElementById('sort-' + _sortCol); if (!el) return; el.classList.add('active'); el.innerHTML = _sortDir === 'asc' ? '' : ''; })(); // ── Live Search ──────────────────────────────────────────────── (function() { var input = document.getElementById('liveSearchInput'), spinner = document.getElementById('searchSpinner'); var resultCount = document.getElementById('resultCount'), tbody = document.getElementById('recordsBody'); var statTotal = document.getElementById('statTotal'); if (!input || !tbody) return; var debounceTimer = null, DEBOUNCE_MS = 350, lastQuery = input.value.trim(); function esc(s) { if (s == null) return ''; var d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; } var IMG = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i, PDF = /\.pdf$/i, DOC = /\.(doc|docx)$/i; var SHT = /\.(xls|xlsx|csv)$/i, VID = /\.(mp4|webm|mov|avi)$/i, AUD = /\.(mp3|wav|ogg|flac)$/i, URL_RE = /^https?:\/\//i; function renderCell(val, col, isFirst, row) { var pk = row.pk || row.id || '', d = (val == null) ? '\u2014' : String(val); var rk = '_raw_' + col, ct = FIELD_TYPES[col] || 'text'; var ie = LIST_EDITABLE.indexOf(col) !== -1 && !isFirst; var ea = ie ? ' data-field="' + col + '" data-pk="' + esc(pk) + '" data-type="' + ct + '" onclick="startInlineEdit(this)"' : ''; if (isFirst) return '' + esc(d) + ''; if (row[rk] === true) return ' ' + esc(d) + ''; if (row[rk] === false) return ' ' + esc(d) + ''; if ((ct==='datetime-local'||ct==='date'||ct==='time') && row[rk]) return '' + esc(d) + ''; if (URL_RE.test(d)) { if (IMG.test(d)) return '
'; if (PDF.test(d)) return ' PDF'; if (DOC.test(d)) return ' DOCX'; if (SHT.test(d)) return ' Sheet'; if (VID.test(d)) return ' Video'; if (AUD.test(d)) return ' Audio'; var t = d.length > 40 ? d.substring(0, 37) + '...' : d; return ' ' + esc(t) + ''; } return '' + esc(d) + ''; } function buildRow(row) { var pk = row.pk || row.id || '', h = ''; h += ''; h += ''; LIST_DISPLAY.forEach(function(col, i) { h += renderCell(row[col], col, i===0, row); }); h += '
'; h += ''; h += ''; h += ''; h += ''; h += '
'; return h; } function doSearch(q) { spinner.style.display = 'inline'; var sp = new URLSearchParams(window.location.search); sp.set('q', q); sp.set('per_page', CURRENT_PER_PAGE); if (CURRENT_ORDERING) sp.set('ordering', CURRENT_ORDERING); fetch(BASE_URL + '/search?' + sp.toString()) .then(function(r) { return r.json(); }) .then(function(data) { spinner.style.display = 'none'; history.replaceState(null, '', q ? BASE_URL + '/?q=' + encodeURIComponent(q) : BASE_URL + '/'); if (data.rows && data.rows.length) { var rh = ''; data.rows.forEach(function(r) { rh += buildRow(r); }); tbody.innerHTML = rh; initRelativeTimes(); restoreColumnVisibility(); } else { tbody.innerHTML = '
No records found
Try adjusting your search query or filters.
'; } var t = data.total || 0; if (resultCount) resultCount.innerHTML = '' + t + ' record' + (t !== 1 ? 's' : ''); if (statTotal) statTotal.textContent = t; var bar = document.getElementById('bulkActionsBar'); if (bar) bar.classList.remove('visible'); var sa = document.getElementById('selectAllCb'); if (sa) { sa.checked = false; sa.indeterminate = false; } }).catch(function() { spinner.style.display = 'none'; }); } input.addEventListener('input', function() { var q = input.value.trim(); if (q === lastQuery) return; clearTimeout(debounceTimer); if (q.length === 1) return; spinner.style.display = 'inline'; debounceTimer = setTimeout(function() { lastQuery = q; doSearch(q); }, DEBOUNCE_MS); }); document.addEventListener('keydown', function(e) { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); input.focus(); input.select(); } if (e.key === 'Escape' && document.activeElement === input) { input.value = ''; input.blur(); } }); })(); // ── Bulk Actions ──────────────────────────────────────────────── function updateBulkBar() { var ch = document.querySelectorAll('.row-check:checked'), bar = document.getElementById('bulkActionsBar'); var cnt = document.getElementById('selectedCount'), sa = document.getElementById('selectAllCb'); if (ch.length > 0) { bar.classList.add('visible'); cnt.textContent = ch.length; document.getElementById('bulkSelectedPks').value = Array.from(ch).map(function(c){return c.value;}).join(','); // Highlight selected rows in table view document.querySelectorAll('#recordsBody tr[data-pk]').forEach(function(r) { var cb = r.querySelector('.row-check'); if (cb && cb.checked) r.classList.add('row-selected'); else r.classList.remove('row-selected'); }); // Highlight selected cards in grid view document.querySelectorAll('.grid-card[data-pk]').forEach(function(c) { var cb = c.querySelector('.row-check'); if (cb && cb.checked) c.classList.add('grid-card-selected'); else c.classList.remove('grid-card-selected'); }); } else { bar.classList.remove('visible'); document.querySelectorAll('.row-selected').forEach(function(r) { r.classList.remove('row-selected'); }); document.querySelectorAll('.grid-card-selected').forEach(function(c) { c.classList.remove('grid-card-selected'); }); } var tot = document.querySelectorAll('.row-check').length; sa.indeterminate = ch.length > 0 && ch.length < tot; sa.checked = ch.length === tot && tot > 0; } function toggleAll(s) { document.querySelectorAll('.row-check').forEach(function(c){c.checked=s.checked;}); updateBulkBar(); } function confirmBulkAction() { var actionVal = document.getElementById('bulkActionValue').value; if (!actionVal) { showToast('Select an action first.', 'warning'); return false; } var c = document.querySelectorAll('.row-check:checked').length; if (!c) { showToast('No records selected.', 'warning'); return false; } var label = document.querySelector('#bulkActionSelectWrap .custom-select-label'); showConfirmModal('Execute "' + (label ? label.textContent : actionVal) + '"?', 'Applied to ' + c + ' record(s).', function(){ document.getElementById('bulkActionForm').submit(); }); return false; } // ── Export dropdown ───────────────────────────────────────────── (function() { var b = document.getElementById('exportDropBtn'), m = document.getElementById('exportDropMenu'); if (!b || !m) return; b.addEventListener('click', function(e) { e.stopPropagation(); m.classList.toggle('open'); }); document.addEventListener('click', function() { m.classList.remove('open'); }); })(); // ── Scroll-to-Top Button ──────────────────────────────────────── (function() { var btn = document.getElementById('scrollTopBtn'); if (!btn) return; window.addEventListener('scroll', function() { if (window.scrollY > 400) btn.classList.add('visible'); else btn.classList.remove('visible'); }); })(); // ── Keyboard shortcuts ────────────────────────────────────────── document.addEventListener('keydown', function(e) { if (['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) return; if (e.key === 'n' && !e.metaKey && !e.ctrlKey && !e.altKey) window.location.href = BASE_URL + '/add'; if (e.key === 'j' || e.key === 'k') { var rows = Array.from(document.querySelectorAll('#recordsBody tr[data-pk]')); if (!rows.length) return; var cur = document.querySelector('#recordsBody tr.row-focused'), idx = cur ? rows.indexOf(cur) : -1; if (e.key === 'j') idx = Math.min(rows.length - 1, idx + 1); else idx = Math.max(0, idx - 1); rows.forEach(function(r) { r.classList.remove('row-focused'); }); rows[idx].classList.add('row-focused'); rows[idx].scrollIntoView({ block: 'nearest' }); } if (e.key === 'Enter') { var f = document.querySelector('#recordsBody tr.row-focused'); if (f) { var pk = f.getAttribute('data-pk'); if (pk) window.location.href = BASE_URL + '/' + pk; } } if (e.key === 'f' && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); openFilterPanel(); } if (e.key === 'Escape') { closeFilterPanel(); } if (e.key === 'x' && !e.metaKey && !e.ctrlKey && !e.altKey) { var f = document.querySelector('#recordsBody tr.row-focused'); if (f) { var b = f.querySelector('.btn-expand'); if (b) b.click(); } } if (e.key === 'v' && !e.metaKey && !e.ctrlKey && !e.altKey) { var gw = document.getElementById('gridViewWrap'); setViewMode(gw && gw.style.display !== 'none' ? 'table' : 'grid'); } if (e.key === '?') { var hint = document.getElementById('kbdHintBar'); if (hint) hint.classList.toggle('visible'); } // Select all with Ctrl/Cmd+A if ((e.metaKey || e.ctrlKey) && e.key === 'a') { e.preventDefault(); var sa = document.getElementById('selectAllCb'); if (sa) { sa.checked = !sa.checked; toggleAll(sa); } } }); // ── Row Detail Expansion ──────────────────────────────────────── function toggleRowDetail(btn, pk) { var tr = btn.closest('tr'), ex = tr.nextElementSibling; if (ex && ex.classList.contains('row-detail-tr')) { ex.remove(); btn.classList.remove('expanded'); return; } btn.classList.add('expanded'); var cells = tr.querySelectorAll('td[data-col]'); var h = '
'; h += '
PK
' + pk + '
'; cells.forEach(function(c) { var col = c.getAttribute('data-col'), lbl = col.replace(/_/g, ' ').replace(/\b\w/g, function(l){return l.toUpperCase();}), v = c.textContent.trim() || '\u2014'; h += '
' + lbl + '
' + v + '
'; }); h += '
'; tr.insertAdjacentHTML('afterend', h); } // ── Inline Editing ────────────────────────────────────────────── function startInlineEdit(td) { if (td.querySelector('.inline-edit-input')) return; var field = td.getAttribute('data-field'), pk = td.getAttribute('data-pk'), dt = td.getAttribute('data-type') || 'text'; var cv = td.textContent.trim(); if (cv === '\u2014') cv = ''; if (dt === 'checkbox') { saveInlineEdit(pk, field, td.classList.contains('cell-bool-true') ? 'false' : 'true', td); return; } td.dataset.originalHtml = td.innerHTML; var inp = document.createElement('input'); inp.type = dt; inp.className = 'inline-edit-input'; inp.value = cv; td.innerHTML = ''; td.appendChild(inp); inp.focus(); inp.select(); inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') saveInlineEdit(pk, field, inp.value, td); if (e.key === 'Escape') td.innerHTML = td.dataset.originalHtml; }); inp.addEventListener('blur', function() { setTimeout(function() { if (td.querySelector('.inline-edit-input')) td.innerHTML = td.dataset.originalHtml; }, 150); }); inp.addEventListener('click', function(e) { e.stopPropagation(); }); } /* quickUpdate: inline-edit shortcut for instant field updates */ function quickUpdate(pk, field, value) { saveInlineEdit(pk, field, value, null); } function saveInlineEdit(pk, field, value, td) { var fd = new FormData(); fd.append(field, value); fetch(BASE_URL + '/' + pk + '/update', { method: 'POST', body: fd }) .then(function(r) { if (r.ok) window.location.reload(); else { if (td) td.innerHTML = td.dataset.originalHtml || ''; showToast('Save failed: ' + r.status, 'error'); } }) .catch(function() { if (td) td.innerHTML = td.dataset.originalHtml || ''; showToast('Network error.', 'error'); }); } // ── Relative Time Display ─────────────────────────────────────── function formatRelativeTime(iso) { try { var d = new Date(iso); if (isNaN(d.getTime())) return null; var diff = Date.now() - d, abs = Math.abs(diff), s = Math.floor(abs/1000), m = Math.floor(s/60), h = Math.floor(m/60), dy = Math.floor(h/24), mo = Math.floor(dy/30), y = Math.floor(dy/365); var sx = diff >= 0 ? 'ago' : 'from now'; if (s < 60) return 'just now'; if (m < 60) return m + 'm ' + sx; if (h < 24) return h + 'h ' + sx; if (dy < 30) return dy + 'd ' + sx; if (mo < 12) return mo + 'mo ' + sx; return y + 'y ' + sx; } catch(e) { return null; } } function initRelativeTimes() { document.querySelectorAll('.time-relative').forEach(function(el) { var iso = el.getAttribute('data-iso'); if (!iso) return; var r = formatRelativeTime(iso); if (r) { el.setAttribute('title', el.textContent); el.textContent = r; } }); } initRelativeTimes(); setInterval(initRelativeTimes, 60000); // ── Custom Confirmation Modal ────────────────────────────────── function showConfirmModal(title, message, onConfirm, isDanger) { var ex = document.getElementById('confirm-modal-overlay'); if (ex) ex.remove(); var dk = document.documentElement.getAttribute('data-theme') !== 'light'; var ov = document.createElement('div'); ov.id = 'confirm-modal-overlay'; ov.style.cssText = 'position:fixed;inset:0;background:' + (dk?'rgba(0,0,0,0.65)':'rgba(0,0,0,0.3)') + ';z-index:10003;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);animation:fadeIn 0.15s ease;'; var bx = document.createElement('div'); bx.style.cssText = 'background:' + (dk?'var(--bg-elevated)':'#fff') + ';border:1px solid var(--border-color);border-radius:14px;padding:28px 32px;max-width:420px;width:90%;box-shadow:0 24px 64px rgba(0,0,0,' + (dk?'0.4':'0.12') + ');'; var ic = isDanger ? 'var(--danger)' : 'var(--accent)', inn = isDanger ? 'alert-triangle' : 'help-circle'; bx.innerHTML = '

' + title + '

' + message + '

'; ov.appendChild(bx); document.body.appendChild(ov); ov.querySelector('#modal-cancel-btn').onclick = function() { ov.remove(); }; ov.querySelector('#modal-confirm-btn').onclick = function() { ov.remove(); if (onConfirm) onConfirm(); }; ov.addEventListener('click', function(e) { if (e.target === ov) ov.remove(); }); document.addEventListener('keydown', function handler(e) { if (e.key === 'Escape') { ov.remove(); document.removeEventListener('keydown', handler); } }); ov.querySelector('#modal-confirm-btn').focus(); } // ── Delete / Duplicate ────────────────────────────────────────── function doDelete(pk) { var f = document.createElement('form'); f.method = 'POST'; f.action = BASE_URL + '/' + pk + '/delete'; var _t = document.querySelector('meta[name="csrf-token"]'); if (_t) { var c = document.createElement('input'); c.type='hidden'; c.name='_csrf_token'; c.value=_t.content; f.appendChild(c); } document.body.appendChild(f); f.submit(); } function doDuplicate(pk) { var f = document.createElement('form'); f.method = 'POST'; f.action = BASE_URL + '/action'; var _t = document.querySelector('meta[name="csrf-token"]'); if (_t) { var c = document.createElement('input'); c.type='hidden'; c.name='_csrf_token'; c.value=_t.content; f.appendChild(c); } var a = document.createElement('input'); a.type = 'hidden'; a.name = 'action'; a.value = 'duplicate_selected'; f.appendChild(a); var s = document.createElement('input'); s.type = 'hidden'; s.name = 'selected'; s.value = pk; f.appendChild(s); document.body.appendChild(f); f.submit(); } // ── Import Modal ──────────────────────────────────────────────── function showImportModal() { var dk = document.documentElement.getAttribute('data-theme') !== 'light'; var ov = document.createElement('div'); ov.className = 'import-overlay'; ov.onclick = function(e) { if (e.target === ov) ov.remove(); }; ov.innerHTML = '

Import {{ verbose_name_plural }}

Upload a CSV or JSON file to import records.

Drop file here or click to browse
'; document.body.appendChild(ov); var dz = ov.querySelector('#importDropzone'); dz.addEventListener('dragover', function(e) { e.preventDefault(); dz.classList.add('dragover'); }); dz.addEventListener('dragleave', function() { dz.classList.remove('dragover'); }); dz.addEventListener('drop', function(e) { e.preventDefault(); dz.classList.remove('dragover'); if (e.dataTransfer.files.length) { var i = document.getElementById('importFileInput'); i.files = e.dataTransfer.files; handleImportFile(i); } }); } var _importData = null; function handleImportFile(input) { if (!input.files.length) return; var file = input.files[0], preview = document.getElementById('importPreview'), sb = document.getElementById('importSubmitBtn'); var reader = new FileReader(); reader.onload = function(e) { try { if (file.name.endsWith('.json')) { _importData = JSON.parse(e.target.result); if (!Array.isArray(_importData)) _importData = [_importData]; } else { var lines = e.target.result.split('\n').filter(function(l){return l.trim();}); if (lines.length < 2) { preview.innerHTML = 'Need header + data rows.'; preview.style.display = 'block'; return; } var hdr = lines[0].split(',').map(function(h){return h.trim().replace(/^"|"$/g,'');}); _importData = []; for (var i = 1; i < lines.length; i++) { var v = lines[i].split(',').map(function(x){return x.trim().replace(/^"|"$/g,'');}); var r = {}; hdr.forEach(function(h,j){r[h]=v[j]||'';}); _importData.push(r); } } preview.innerHTML = '
' + _importData.length + ' record(s) from ' + file.name + '
'; preview.style.display = 'block'; sb.style.display = 'inline-flex'; } catch(err) { preview.innerHTML = 'Parse error: ' + err.message + ''; preview.style.display = 'block'; _importData = null; } }; reader.readAsText(file); } function submitImport() { if (!_importData || !_importData.length) return; var sb = document.getElementById('importSubmitBtn'); sb.disabled = true; sb.textContent = 'Importing...'; var ok = 0, fail = 0, tot = _importData.length; function next(i) { if (i >= tot) { var ov = document.querySelector('.import-overlay'); if (ov) ov.remove(); showToast('Imported ' + ok + (fail ? ', ' + fail + ' failed' : ''), ok ? 'success' : 'error'); if (ok > 0) window.location.reload(); return; } var fd = new FormData(), r = _importData[i]; for (var k in r) if (r.hasOwnProperty(k)) fd.append(k, r[k]); fetch(BASE_URL + '/add', { method: 'POST', body: fd }).then(function(res) { if (res.ok || res.status === 302 || res.status === 303) ok++; else fail++; next(i+1); }).catch(function() { fail++; next(i+1); }); } next(0); } // ── Batch Update Modal ────────────────────────────────────────── function showBatchUpdateModal() { var checked = document.querySelectorAll('.row-check:checked'); if (!checked.length) { showToast('Select records first', 'warning'); return; } var pks = []; checked.forEach(function(cb) { pks.push(cb.value); }); var cols = []; {% if list_editable and list_editable|length > 0 %} cols = {{ list_editable|tojson }}; {% else %} // Use all non-pk columns {% for col in list_display %}{% if not loop.first %}cols.push('{{ col }}');{% endif %}{% endfor %} {% endif %} var ex = document.getElementById('batch-update-overlay'); if (ex) ex.remove(); var ov = document.createElement('div'); ov.id = 'batch-update-overlay'; ov.style.cssText = 'position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);'; ov.onclick = function(e) { if (e.target === ov) ov.remove(); }; var opts = ''; cols.forEach(function(c) { opts += '
' + c.replace(/_/g, ' ').replace(/\b\w/g, function(l){return l.toUpperCase();}) + '
'; }); var firstLabel = cols.length ? cols[0].replace(/_/g, ' ').replace(/\b\w/g, function(l){return l.toUpperCase();}) : 'Select field'; ov.innerHTML = '
' + '

Batch Update

' + '

Update ' + pks.length + ' selected record(s).

' + '' + '' + '
' + firstLabel + '
' + opts + '
' + '' + '' + '
' + '
'; document.body.appendChild(ov); ov.querySelector('#batchValue').focus(); ov._pks = pks; } function executeBatchUpdate() { var ov = document.getElementById('batch-update-overlay'); if (!ov || !ov._pks) return; var field = document.getElementById('batchFieldHidden').value; var value = document.getElementById('batchValue').value; if (!field) { showToast('Select a field', 'warning'); return; } var fd = new FormData(); fd.append('field', field); fd.append('value', value); fd.append('selected', JSON.stringify(ov._pks)); fetch(BASE_URL + '/batch-update', { method: 'POST', body: fd }) .then(function(res) { return res.json(); }) .then(function(data) { ov.remove(); if (data.success) { showToast('Updated ' + data.updated + ' record(s)', 'success'); setTimeout(function() { window.location.reload(); }, 800); } else { showToast(data.error || 'Update failed', 'error'); } }) .catch(function(err) { showToast('Error: ' + err.message, 'error'); }); } // ── Toast Notifications (enhanced with icons) ─────────────────── function showToast(msg, type) { type = type || 'info'; var el = document.createElement('div'); var icons = { success: 'check-circle', error: 'alert-circle', warning: 'alert-triangle', info: 'info' }; var colors = { success: { bg: 'rgba(34,197,94,0.15)', fg: '#22c55e', border: 'rgba(34,197,94,0.3)' }, error: { bg: 'rgba(239,68,68,0.15)', fg: '#ef4444', border: 'rgba(239,68,68,0.3)' }, warning: { bg: 'rgba(234,179,8,0.15)', fg: '#eab308', border: 'rgba(234,179,8,0.3)' }, info: { bg: 'rgba(59,130,246,0.15)', fg: '#3b82f6', border: 'rgba(59,130,246,0.3)' } }; var c = colors[type] || colors.info; el.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:10010;padding:14px 22px;border-radius:12px;font-size:0.82rem;font-weight:500;box-shadow:0 8px 32px rgba(0,0,0,0.3);animation:fadeIn 0.2s ease;max-width:400px;font-family:var(--font-sans);display:flex;align-items:center;gap:10px;background:' + c.bg + ';color:' + c.fg + ';border:1px solid ' + c.border + ';backdrop-filter:blur(12px);'; el.innerHTML = '' + msg + ''; document.body.appendChild(el); setTimeout(function() { el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; el.style.transition = 'opacity 0.3s, transform 0.3s'; setTimeout(function() { el.remove(); }, 300); }, 3500); } // ── Dropdown Styles ───────────────────────────────────────────── (function() { var s = document.createElement('style'); s.textContent = '.dropdown-menu{display:none;position:absolute;right:0;top:100%;margin-top:6px;z-index:100;min-width:160px;background:var(--bg-elevated);border:1px solid var(--border-color);border-radius:10px;padding:4px;box-shadow:0 12px 40px rgba(0,0,0,0.4);animation:fadeIn 0.12s ease;}.dropdown-menu.open{display:block!important;}.dropdown-item{display:flex;align-items:center;gap:10px;padding:10px 14px;font-size:0.8rem;color:var(--text-secondary);border-radius:8px;transition:background 0.12s,color 0.12s;text-decoration:none;}.dropdown-item:hover{background:var(--bg-surface-hover);color:var(--text-primary)!important;}.grid-card-selected{border-color:var(--accent)!important;box-shadow:0 0 0 2px rgba(34,197,94,0.2),0 8px 24px rgba(0,0,0,0.15)!important;}'; document.head.appendChild(s); })(); {% endblock %}