|
|
|
|
|
const searchBox = document.getElementById("searchBox"); |
|
|
const clearSearch = document.getElementById("clearSearch"); |
|
|
const pageSizeEl = document.getElementById("pageSize"); |
|
|
const themeBtn = document.getElementById("themeToggle"); |
|
|
const tableBody = document.getElementById("tableBody"); |
|
|
const pagination = document.getElementById("pagination"); |
|
|
const counter = document.getElementById("counter"); |
|
|
const loading = document.getElementById("loading"); |
|
|
|
|
|
let data = []; |
|
|
let filtered = []; |
|
|
let currentPage = 1; |
|
|
|
|
|
const DEFAULT_PAGE_SIZE = 10; |
|
|
let rowsPerPage = DEFAULT_PAGE_SIZE; |
|
|
|
|
|
|
|
|
if (pageSizeEl) { |
|
|
pageSizeEl.value = String(DEFAULT_PAGE_SIZE); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeArabic(str = "") { |
|
|
return str |
|
|
.replace(/[\u064B-\u0652]/g, "") |
|
|
.replace(/[أإآا]/g, "ا") |
|
|
.replace(/ى/g, "ي") |
|
|
.replace(/ؤ/g, "و") |
|
|
.replace(/ئ/g, "ي") |
|
|
.replace(/ة/g, "ه") |
|
|
.toLowerCase(); |
|
|
} |
|
|
|
|
|
|
|
|
function matches(text, query) { |
|
|
if (!query) return true; |
|
|
return normalizeArabic(String(text ?? "")).includes( |
|
|
normalizeArabic(query) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
function highlight(text, query) { |
|
|
if (!query) return String(text ?? ""); |
|
|
const safe = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
|
|
return String(text ?? "").replace( |
|
|
new RegExp(safe, "gi"), |
|
|
(m) => `<mark class="hl">${m}</mark>` |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderPagination() { |
|
|
pagination.innerHTML = ""; |
|
|
|
|
|
const totalRows = filtered.length; |
|
|
const totalPages = Math.ceil(totalRows / rowsPerPage); |
|
|
|
|
|
if (totalPages <= 1) return; |
|
|
|
|
|
const makeBtn = (label, page, options = {}) => { |
|
|
const btn = document.createElement("button"); |
|
|
btn.textContent = label; |
|
|
btn.className = "page-btn" + (options.ghost ? " ghost" : ""); |
|
|
if (options.disabled) btn.disabled = true; |
|
|
btn.addEventListener("click", () => { |
|
|
currentPage = page; |
|
|
updateView(); |
|
|
window.scrollTo({ top: 0, behavior: "smooth" }); |
|
|
}); |
|
|
return btn; |
|
|
}; |
|
|
|
|
|
|
|
|
pagination.appendChild( |
|
|
makeBtn("‹", Math.max(1, currentPage - 1), { |
|
|
ghost: true, |
|
|
disabled: currentPage === 1, |
|
|
}) |
|
|
); |
|
|
|
|
|
|
|
|
const windowSize = 5; |
|
|
let start = Math.max(1, currentPage - Math.floor(windowSize / 2)); |
|
|
let end = Math.min(totalPages, start + windowSize - 1); |
|
|
|
|
|
if (end - start + 1 < windowSize) { |
|
|
start = Math.max(1, end - windowSize + 1); |
|
|
} |
|
|
|
|
|
if (start > 1) { |
|
|
pagination.appendChild( |
|
|
makeBtn("1", 1, { ghost: currentPage !== 1 }) |
|
|
); |
|
|
} |
|
|
|
|
|
if (start > 2) { |
|
|
const dots = document.createElement("span"); |
|
|
dots.className = "dots"; |
|
|
dots.textContent = "..."; |
|
|
pagination.appendChild(dots); |
|
|
} |
|
|
|
|
|
for (let p = start; p <= end; p++) { |
|
|
const btn = makeBtn(String(p), p); |
|
|
if (p === currentPage) btn.classList.add("active"); |
|
|
pagination.appendChild(btn); |
|
|
} |
|
|
|
|
|
if (end < totalPages - 1) { |
|
|
const dots = document.createElement("span"); |
|
|
dots.className = "dots"; |
|
|
dots.textContent = "..."; |
|
|
pagination.appendChild(dots); |
|
|
} |
|
|
|
|
|
if (end < totalPages) { |
|
|
pagination.appendChild( |
|
|
makeBtn(String(totalPages), totalPages, { |
|
|
ghost: currentPage !== totalPages, |
|
|
}) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
pagination.appendChild( |
|
|
makeBtn("›", Math.min(totalPages, currentPage + 1), { |
|
|
ghost: true, |
|
|
disabled: currentPage === totalPages, |
|
|
}) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderTable() { |
|
|
const query = searchBox ? searchBox.value.trim() : ""; |
|
|
tableBody.innerHTML = ""; |
|
|
|
|
|
const startIndex = (currentPage - 1) * rowsPerPage; |
|
|
const pageItems = filtered.slice(startIndex, startIndex + rowsPerPage); |
|
|
|
|
|
if (pageItems.length === 0) { |
|
|
const tr = document.createElement("tr"); |
|
|
const td = document.createElement("td"); |
|
|
td.colSpan = 3; |
|
|
td.className = "no-results"; |
|
|
td.textContent = "لا توجد نتائج مطابقة لبحثك."; |
|
|
tr.appendChild(td); |
|
|
tableBody.appendChild(tr); |
|
|
return; |
|
|
} |
|
|
|
|
|
pageItems.forEach((item) => { |
|
|
const tr = document.createElement("tr"); |
|
|
|
|
|
const nameTd = document.createElement("td"); |
|
|
nameTd.innerHTML = highlight(item.name, query); |
|
|
|
|
|
const qtyTd = document.createElement("td"); |
|
|
qtyTd.innerHTML = highlight(item.quantity, query); |
|
|
|
|
|
const unitTd = document.createElement("td"); |
|
|
unitTd.innerHTML = highlight(item.unit, query); |
|
|
|
|
|
tr.appendChild(nameTd); |
|
|
tr.appendChild(qtyTd); |
|
|
tr.appendChild(unitTd); |
|
|
|
|
|
tableBody.appendChild(tr); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateView() { |
|
|
const query = searchBox ? searchBox.value.trim() : ""; |
|
|
|
|
|
filtered = data.filter((item) => { |
|
|
return ( |
|
|
matches(item.code, query) || |
|
|
matches(item.name, query) || |
|
|
matches(item.quantity, query) || |
|
|
matches(item.unit, query) |
|
|
); |
|
|
}); |
|
|
|
|
|
if (!counter) return; |
|
|
|
|
|
if (!query) { |
|
|
counter.textContent = `إجمالي البنود: ${data.length.toLocaleString()}`; |
|
|
} else if (filtered.length) { |
|
|
counter.textContent = `✅ عدد النتائج: ${filtered.length.toLocaleString()}`; |
|
|
} else { |
|
|
counter.textContent = "لا توجد نتائج مطابقة"; |
|
|
} |
|
|
|
|
|
if (loading) { |
|
|
loading.style.display = "none"; |
|
|
} |
|
|
|
|
|
renderTable(); |
|
|
renderPagination(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function debounce(fn, delay = 220) { |
|
|
let timer; |
|
|
return (...args) => { |
|
|
clearTimeout(timer); |
|
|
timer = setTimeout(() => fn(...args), delay); |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (searchBox) { |
|
|
searchBox.addEventListener( |
|
|
"input", |
|
|
debounce(() => { |
|
|
currentPage = 1; |
|
|
updateView(); |
|
|
}) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (clearSearch) { |
|
|
clearSearch.addEventListener("click", () => { |
|
|
searchBox.value = ""; |
|
|
searchBox.focus(); |
|
|
currentPage = 1; |
|
|
updateView(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (pageSizeEl) { |
|
|
pageSizeEl.addEventListener("change", () => { |
|
|
const val = parseInt(pageSizeEl.value, 10); |
|
|
rowsPerPage = !isNaN(val) && val > 0 ? val : DEFAULT_PAGE_SIZE; |
|
|
currentPage = 1; |
|
|
updateView(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener("keydown", (e) => { |
|
|
if (e.key === "/") { |
|
|
if (document.activeElement === searchBox) return; |
|
|
e.preventDefault(); |
|
|
if (searchBox) searchBox.focus(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function initTheme() { |
|
|
if (!themeBtn) return; |
|
|
|
|
|
const saved = localStorage.getItem("theme") || "light"; |
|
|
const root = document.documentElement; |
|
|
|
|
|
if (saved === "dark") { |
|
|
root.classList.add("dark"); |
|
|
themeBtn.textContent = "☀️ فاتح"; |
|
|
} else { |
|
|
root.classList.remove("dark"); |
|
|
themeBtn.textContent = "ليلي"; |
|
|
} |
|
|
|
|
|
themeBtn.addEventListener("click", () => { |
|
|
const isDark = root.classList.toggle("dark"); |
|
|
localStorage.setItem("theme", isDark ? "dark" : "light"); |
|
|
themeBtn.textContent = isDark ? "☀️ فاتح" : "ليلي"; |
|
|
}); |
|
|
})(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fetch("data.json", { cache: "no-cache" }) |
|
|
.then((res) => res.json()) |
|
|
.then((json) => { |
|
|
|
|
|
data = (Array.isArray(json) ? json : []).map((row) => ({ |
|
|
code: |
|
|
row.code ?? |
|
|
row.Code ?? |
|
|
row["رمز البند"] ?? |
|
|
"", |
|
|
name: |
|
|
row.name ?? |
|
|
row["اسم البند"] ?? |
|
|
"", |
|
|
quantity: |
|
|
row.quantity ?? |
|
|
row["الكمية المطلوبة"] ?? |
|
|
"", |
|
|
unit: |
|
|
row.unit ?? |
|
|
row["وحدة البند"] ?? |
|
|
"", |
|
|
})); |
|
|
|
|
|
currentPage = 1; |
|
|
updateView(); |
|
|
}) |
|
|
.catch((err) => { |
|
|
console.error("تعذر تحميل data.json:", err); |
|
|
if (loading) { |
|
|
loading.textContent = "تعذر تحميل البيانات ❌"; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function attachScrollHint() { |
|
|
const wrap = document.querySelector(".table-wrap"); |
|
|
if (!wrap) return; |
|
|
|
|
|
let timer; |
|
|
wrap.addEventListener("scroll", () => { |
|
|
wrap.classList.add("scrolling"); |
|
|
clearTimeout(timer); |
|
|
timer = setTimeout(() => { |
|
|
wrap.classList.remove("scrolling"); |
|
|
}, 200); |
|
|
}); |
|
|
})(); |
|
|
|
|
|
|
|
|
|