Commit 7a6dba5d authored by DevPilot's avatar DevPilot

feat(ui): بحث وترتيب تلقائي لأي جدول طويل

٣١٦ شاشة فيها جدول من غير أي خانة بحث، والمستخدم بيلف بعينه على الصفحة.
بدل ما نضيف خانة في كل شاشة (شغل ما بيخلصش وبيتنسي في الجديد)، مكوّن
واحد بيشتغل لوحده على أي جدول أكتر من ٨ صفوف:

- خانة بحث بتفلتر وانت بتكتب، بنفس نواة التطبيع العربي بتاعة النظام.
- ترتيب بالضغط على عنوان العمود، بيفهم الأرقام والفلوس والتواريخ فـ
  «1,200.00 ج.م» بتترتّب كرقم مش كنص.
- عدّاد «كام من كام»، ورسالة واضحة لما مفيش نتيجة.
- بيتخطى الشاشات اللي فيها بحث أصلًا، وبيتعطّل بـ data-no-filter.
parent 60962799
......@@ -105,6 +105,7 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<script src="<?= url('assets/js/app.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/app.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/arabic-search.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/arabic-search.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/searchable-select.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/searchable-select.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/table-tools.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/table-tools.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/sidebar-search.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/sidebar-search.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/forms-engine.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/forms-engine.js') ?: time() ?>"></script>
<script>
......
......@@ -2673,3 +2673,80 @@ code {
.dash-skeleton-text { animation: none; }
.dash-widget, .dash-progress-fill { transition: none; }
}
/* ── بحث وترتيب الجداول (table-tools.js) ─────────────────────────── */
.tt-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid #E5E7EB;
background: #FCFCFD;
flex-wrap: wrap;
}
.tt-search-wrap {
position: relative;
flex: 1;
min-width: 220px;
max-width: 420px;
}
.tt-search {
width: 100%;
padding: 7px 32px 7px 12px;
border: 1px solid #E5E7EB;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
outline: none;
background: #fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat;
background-position: right 9px center;
transition: border-color var(--duration-fast, .15s) ease;
}
.tt-search:focus {
border-color: var(--brand-primary, #0D7377);
box-shadow: 0 0 0 3px rgba(13, 115, 119, .08);
}
.tt-count {
font-size: 12px;
color: #6B7280;
white-space: nowrap;
}
.tt-empty td {
padding: 26px 16px !important;
text-align: center;
color: #9CA3AF;
font-size: 13px;
}
/* الأعمدة القابلة للترتيب */
th.tt-sortable {
cursor: pointer;
user-select: none;
position: relative;
padding-inline-end: 18px !important;
}
th.tt-sortable:hover {
color: var(--brand-primary, #0D7377);
}
th.tt-sortable::after {
content: '⇅';
position: absolute;
inset-inline-end: 5px;
opacity: .25;
font-size: 10px;
}
th.tt-sortable.tt-asc::after { content: '↑'; opacity: .9; }
th.tt-sortable.tt-desc::after { content: '↓'; opacity: .9; }
@media print {
.tt-bar { display: none; }
}
/**
* بحث وترتيب تلقائي لأي جدول طويل.
*
* المشكلة: في مئات الشاشات فيها جداول من غير أي خانة بحث، والمستخدم
* بيلف بعينه على الصفحة كلها. وإضافة خانة بحث في كل شاشة على حدة شغل
* ما بيخلصش وبيتنسي في الشاشات الجديدة.
*
* الحل: مكوّن واحد بيشتغل لوحده على أي جدول عدد صفوفه أكبر من الحد:
* • خانة بحث فوق الجدول بتفلتر الصفوف وانت بتكتب.
* • البحث بيستخدم نفس نواة التطبيع العربي بتاعة باقي النظام،
* فـ«احمد» بتلاقي «أحمد»، و«٥٠٠» بتلاقي «500».
* • الأعمدة بتترتّب بالضغط على العنوان — بيفهم الأرقام والفلوس
* والتواريخ، فـ«1,200.00 ج.م» بتترتّب كرقم مش كنص.
* • عدّاد بيقول بتشوف كام من كام.
*
* للتعطيل على جدول معيّن: data-no-filter على الـ <table> أو على أي أب ليه.
*/
(function () {
'use strict';
var MIN_ROWS = 8; // أقل من كده الجدول قصير وما يحتاجش بحث
function t(el) { return (el.textContent || '').replace(/\s+/g, ' ').trim(); }
/** بيحوّل نص الخلية لقيمة تترتّب صح: رقم، تاريخ، أو نص مطبّع. */
function sortValue(text) {
var s = text.trim();
if (s === '' || s === '—' || s === '-') return { n: null, s: '' };
// تاريخ YYYY-MM-DD أو DD/MM/YYYY
var iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (iso) return { n: Number(iso[1] + iso[2] + iso[3]), s: s };
var dmy = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);
if (dmy) {
return { n: Number(dmy[3] + ('0' + dmy[2]).slice(-2) + ('0' + dmy[1]).slice(-2)), s: s };
}
// رقم أو مبلغ: نشيل الفواصل والعملة والأرقام العربية
var cleaned = window.ArabicSearch
? window.ArabicSearch.normalize(s)
: s.toLowerCase();
var num = cleaned.replace(/[,\s]/g, '').replace(/[^\d.\-]/g, '');
if (num !== '' && num !== '-' && num !== '.' && /\d/.test(num) && /^-?[\d.]+$/.test(num)) {
var parsed = parseFloat(num);
if (!isNaN(parsed)) return { n: parsed, s: cleaned };
}
return { n: null, s: cleaned };
}
function buildBar(table, tbody, rows) {
var bar = document.createElement('div');
bar.className = 'tt-bar';
var wrap = document.createElement('div');
wrap.className = 'tt-search-wrap';
var input = document.createElement('input');
input.type = 'text';
input.className = 'tt-search';
input.placeholder = 'ابحث في الجدول…';
input.setAttribute('autocomplete', 'off');
input.setAttribute('aria-label', 'ابحث في الجدول');
var count = document.createElement('span');
count.className = 'tt-count';
wrap.appendChild(input);
bar.appendChild(wrap);
bar.appendChild(count);
function updateCount(shown) {
count.textContent = shown === rows.length
? rows.length + ' صف'
: shown + ' من ' + rows.length;
}
var emptyRow = null;
function ensureEmptyRow(colspan) {
if (emptyRow) return emptyRow;
emptyRow = document.createElement('tr');
emptyRow.className = 'tt-empty';
var td = document.createElement('td');
td.colSpan = colspan;
td.textContent = 'مفيش نتيجة مطابقة';
emptyRow.appendChild(td);
tbody.appendChild(emptyRow);
return emptyRow;
}
var timer = null;
input.addEventListener('input', function () {
clearTimeout(timer);
var q = input.value;
timer = setTimeout(function () { apply(q); }, 90);
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { input.value = ''; apply(''); }
});
function apply(rawQuery) {
var AS = window.ArabicSearch;
var query = AS ? AS.normalize(rawQuery) : (rawQuery || '').toLowerCase().trim();
var shown = 0;
rows.forEach(function (row) {
if (!row.__text) {
row.__text = AS ? AS.normalize(t(row)) : t(row).toLowerCase();
}
var match = !query || (AS ? AS.score(query, row.__text) > 0 : row.__text.indexOf(query) !== -1);
row.style.display = match ? '' : 'none';
if (match) shown++;
});
updateCount(shown);
var cols = (table.querySelector('tr') || { children: [] }).children.length || 1;
var er = ensureEmptyRow(cols);
er.style.display = shown === 0 ? '' : 'none';
}
updateCount(rows.length);
return bar;
}
function makeSortable(table, tbody, rows) {
var head = table.querySelector('thead tr');
if (!head) return;
Array.prototype.slice.call(head.children).forEach(function (th, idx) {
// أعمدة الاختيار والأزرار ما تترتّبش
if (th.querySelector('input[type=checkbox]')) return;
if (t(th) === '') return;
th.classList.add('tt-sortable');
th.setAttribute('title', 'اضغط للترتيب');
th.addEventListener('click', function () {
var asc = th.__dir !== 'asc';
Array.prototype.slice.call(head.children).forEach(function (o) {
o.__dir = null;
o.classList.remove('tt-asc', 'tt-desc');
});
th.__dir = asc ? 'asc' : 'desc';
th.classList.add(asc ? 'tt-asc' : 'tt-desc');
var sorted = rows.slice().sort(function (a, b) {
var av = sortValue(t(a.children[idx] || { textContent: '' }));
var bv = sortValue(t(b.children[idx] || { textContent: '' }));
// الفاضي دايمًا تحت
if (av.s === '' && bv.s !== '') return 1;
if (bv.s === '' && av.s !== '') return -1;
var r;
if (av.n !== null && bv.n !== null) r = av.n - bv.n;
else r = av.s.localeCompare(bv.s, 'ar');
return asc ? r : -r;
});
sorted.forEach(function (row) { tbody.appendChild(row); });
var empty = tbody.querySelector('.tt-empty');
if (empty) tbody.appendChild(empty);
});
});
}
function enhance(table) {
if (table.__ttDone) return;
if (table.hasAttribute('data-no-filter') || table.closest('[data-no-filter]')) return;
var tbody = table.querySelector('tbody');
if (!tbody) return;
var rows = Array.prototype.slice.call(tbody.rows).filter(function (r) {
return !r.classList.contains('tt-empty');
});
if (rows.length < MIN_ROWS) return;
// لو الشاشة فيها خانة بحث بالفعل ما نزوّدش واحدة تانية
var card = table.closest('.card') || table.parentElement;
var existing = card && card.querySelector('input[name="q"], input[name="search"], input[type="search"]');
table.__ttDone = true;
if (!existing) {
var host = table.closest('.table-responsive') || table;
host.parentNode.insertBefore(buildBar(table, tbody, rows), host);
}
makeSortable(table, tbody, rows);
}
function init(root) {
(root || document).querySelectorAll('table').forEach(enhance);
}
window.TableTools = { init: init, enhance: enhance };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { init(document); });
} else {
init(document);
}
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment