| "use strict"; |
|
|
| const loginView = document.getElementById("loginView"); |
| const dashboardView = document.getElementById("dashboardView"); |
| const loginForm = document.getElementById("loginForm"); |
| const passwordInput = document.getElementById("password"); |
| const loginButton = document.getElementById("loginButton"); |
| const loginError = document.getElementById("loginError"); |
| const togglePassword = document.getElementById("togglePassword"); |
| const logoutButton = document.getElementById("logoutButton"); |
| const regionSelect = document.getElementById("regionSelect"); |
| const regionButtons = document.getElementById("regionButtons"); |
| const citySelect = document.getElementById("citySelect"); |
| const searchInput = document.getElementById("searchInput"); |
| const clearFiltersButton = document.getElementById("clearFiltersButton"); |
| const emptyState = document.getElementById("emptyState"); |
| const resultsSection = document.getElementById("resultsSection"); |
| const resultsMeta = document.getElementById("resultsMeta"); |
| const resultsTitle = document.getElementById("resultsTitle"); |
| const samplesGrid = document.getElementById("samplesGrid"); |
| const noResults = document.getElementById("noResults"); |
| const loadMoreWrap = document.getElementById("loadMoreWrap"); |
| const loadMoreButton = document.getElementById("loadMoreButton"); |
| const loadMoreMeta = document.getElementById("loadMoreMeta"); |
| const toast = document.getElementById("toast"); |
|
|
| const PAGE_SIZE = 24; |
| const STATE_KEY = "icsInquiryFilters"; |
| const CONTACT_MESSAGE = (row) => |
| [ |
| "السلام عليكم ورحمة الله وبركاته", |
| "", |
| "أستاذي الكريم،", |
| "نأمل تزويدنا بموقع المنشأة التالية، وذلك لاستكمال بيانات الزيارة الميدانية:", |
| "", |
| `منشأة: ${row.establishmentName || "-"}`, |
| `السجل التجاري: ${row.commercialRecord || "-"}`, |
| `المدينة: ${row.city || "-"}`, |
| "", |
| "شاكرين لكم تعاونكم.", |
| ].join("\n"); |
|
|
| let rows = []; |
| let visibleRows = []; |
| let renderedCount = 0; |
| let toastTimer; |
| let isRestoringState = false; |
|
|
| function base64ToBytes(value) { |
| const binary = atob(value); |
| return Uint8Array.from(binary, (char) => char.charCodeAt(0)); |
| } |
|
|
| async function deriveKey(password, salt) { |
| const material = await crypto.subtle.importKey( |
| "raw", |
| new TextEncoder().encode(password), |
| "PBKDF2", |
| false, |
| ["deriveKey"], |
| ); |
| return crypto.subtle.deriveKey( |
| { name: "PBKDF2", salt, iterations: ENCRYPTED_DATA.iterations, hash: "SHA-256" }, |
| material, |
| { name: "AES-GCM", length: 256 }, |
| false, |
| ["decrypt"], |
| ); |
| } |
|
|
| async function decryptData(password) { |
| const salt = base64ToBytes(ENCRYPTED_DATA.salt); |
| const iv = base64ToBytes(ENCRYPTED_DATA.iv); |
| const cipherText = base64ToBytes(ENCRYPTED_DATA.payload); |
| const key = await deriveKey(password, salt); |
| const plainBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipherText); |
| return JSON.parse(new TextDecoder().decode(plainBuffer)); |
| } |
|
|
| function normalize(value) { |
| return String(value ?? "") |
| .normalize("NFKD") |
| .replace(/[\u064B-\u065F\u0670]/g, "") |
| .replace(/[أإآ]/g, "ا") |
| .replace(/ى/g, "ي") |
| .replace(/ة/g, "ه") |
| .replace(/\s+/g, " ") |
| .trim() |
| .toLowerCase(); |
| } |
|
|
| function uniqueSorted(values) { |
| return [...new Set(values.filter(Boolean))].sort((a, b) => |
| String(a).localeCompare(String(b), "ar", { sensitivity: "base" }), |
| ); |
| } |
|
|
| function setOptions(select, values, placeholder) { |
| select.replaceChildren(new Option(placeholder, "")); |
| values.forEach((value) => select.add(new Option(value, value))); |
| } |
|
|
| function saveFilterState() { |
| if (isRestoringState) return; |
| localStorage.setItem( |
| STATE_KEY, |
| JSON.stringify({ |
| region: regionSelect.value, |
| city: citySelect.value, |
| search: searchInput.value, |
| }), |
| ); |
| } |
|
|
| function getSavedFilterState() { |
| try { |
| return JSON.parse(localStorage.getItem(STATE_KEY) || "{}"); |
| } catch { |
| return {}; |
| } |
| } |
|
|
| function updateRegionButtonState() { |
| regionButtons.querySelectorAll(".region-choice").forEach((button) => { |
| const isActive = button.dataset.region === regionSelect.value; |
| button.classList.toggle("active", isActive); |
| button.setAttribute("aria-checked", String(isActive)); |
| }); |
| } |
|
|
| function setRegion(region, scrollToControls = false) { |
| regionSelect.value = region; |
| updateRegionButtonState(); |
| const cities = uniqueSorted(rows.filter((row) => row.region === region).map((row) => row.city)); |
| setOptions(citySelect, cities, "اختر المدينة الصناعية"); |
| citySelect.disabled = !region; |
| citySelect.value = ""; |
| applyFilters(); |
| if (scrollToControls) { |
| window.scrollTo({ top: document.querySelector(".controls-panel").offsetTop - 16, behavior: "smooth" }); |
| } |
| } |
|
|
| function showToast(message) { |
| clearTimeout(toastTimer); |
| toast.textContent = message; |
| toast.classList.add("show"); |
| toastTimer = setTimeout(() => toast.classList.remove("show"), 2200); |
| } |
|
|
| async function copyText(text, successMessage) { |
| try { |
| await navigator.clipboard.writeText(text); |
| } catch { |
| const area = document.createElement("textarea"); |
| area.value = text; |
| area.style.position = "fixed"; |
| area.style.opacity = "0"; |
| document.body.append(area); |
| area.select(); |
| document.execCommand("copy"); |
| area.remove(); |
| } |
| showToast(successMessage); |
| } |
|
|
| function phoneDigits(value) { |
| const match = String(value ?? "").match(/(?:\+?966|0)?5\d{8}/); |
| if (!match) return ""; |
| let digits = match[0].replace(/\D/g, ""); |
| if (digits.startsWith("966")) return digits; |
| if (digits.startsWith("0")) return `966${digits.slice(1)}`; |
| return `966${digits}`; |
| } |
|
|
| function localPhoneNumber(value) { |
| const digits = phoneDigits(value); |
| if (!digits) return ""; |
| return digits.startsWith("966") ? `0${digits.slice(3)}` : digits; |
| } |
|
|
| function isCoordinate(value) { |
| return /^[-+]?\d{1,2}(?:\.\d+)?\s*,\s*[-+]?\d{1,3}(?:\.\d+)?$/.test(String(value ?? "").trim()); |
| } |
|
|
| function hasLocationInfo(row) { |
| return Boolean(String(row.coordinates ?? "").trim()); |
| } |
|
|
| function sortRowsByLocationInfo(items) { |
| return [...items].sort((a, b) => Number(hasLocationInfo(b)) - Number(hasLocationInfo(a))); |
| } |
|
|
| function createSvgIcon(path) { |
| const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); |
| svg.setAttribute("viewBox", "0 0 24 24"); |
| svg.setAttribute("aria-hidden", "true"); |
| const p = document.createElementNS("http://www.w3.org/2000/svg", "path"); |
| p.setAttribute("d", path); |
| svg.append(p); |
| return svg; |
| } |
|
|
| function createActionLink(label, href, iconPath) { |
| const link = document.createElement("a"); |
| link.className = "action-link"; |
| link.href = href; |
| link.target = "_blank"; |
| link.rel = "noopener"; |
| link.append(createSvgIcon(iconPath), document.createTextNode(label)); |
| return link; |
| } |
|
|
| function createDetail(label, value, extraClass = "") { |
| const item = document.createElement("div"); |
| item.className = `detail-item ${extraClass}`.trim(); |
| const title = document.createElement("span"); |
| title.className = "detail-label"; |
| title.textContent = label; |
| const text = document.createElement("span"); |
| text.className = "detail-value"; |
| text.textContent = String(value); |
| item.append(title, text); |
| return item; |
| } |
|
|
| function createCoordinatesBlock(row) { |
| const value = String(row.coordinates ?? "").trim(); |
| if (!value) return null; |
|
|
| const item = document.createElement("div"); |
| const hasCoordinates = isCoordinate(value); |
| item.className = `detail-item coordinates-detail ${hasCoordinates ? "map-detail" : "statement-detail"}`; |
| const label = document.createElement("span"); |
| label.className = "detail-label"; |
| label.textContent = hasCoordinates ? "الإحداثيات" : "إفادة مدن"; |
| item.append(label); |
|
|
| if (hasCoordinates) { |
| item.append( |
| createActionLink( |
| "اضغط هنا للذهاب بخرائط Google", |
| `https://www.google.com/maps?q=${encodeURIComponent(value)}`, |
| "M12 21s7-4.6 7-11a7 7 0 1 0-14 0c0 6.4 7 11 7 11Z M12 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z", |
| ), |
| ); |
| return item; |
| } |
|
|
| const phone = phoneDigits(value); |
| const note = document.createElement("p"); |
| note.className = "contact-note"; |
| note.textContent = value; |
| item.append(note); |
|
|
| if (phone) { |
| const message = CONTACT_MESSAGE(row); |
| const actions = document.createElement("div"); |
| actions.className = "contact-actions"; |
| const copyPhoneButton = document.createElement("button"); |
| copyPhoneButton.type = "button"; |
| copyPhoneButton.className = "action-link"; |
| copyPhoneButton.append( |
| createSvgIcon("M8 8h11v11H8z M5 16H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"), |
| document.createTextNode("نسخ الرقم"), |
| ); |
| copyPhoneButton.addEventListener("click", () => |
| copyText(localPhoneNumber(value), "تم نسخ الرقم"), |
| ); |
| actions.append( |
| copyPhoneButton, |
| createActionLink("واتساب", `https://wa.me/${phone}?text=${encodeURIComponent(message)}`, "M20.5 11.5a8.5 8.5 0 0 1-12.6 7.4L3 20l1.2-4.7A8.5 8.5 0 1 1 20.5 11.5Z M8.6 8.7c.2 3.3 2.7 5.8 6 6.2"), |
| ); |
| const copyButton = document.createElement("button"); |
| copyButton.type = "button"; |
| copyButton.className = "action-link"; |
| copyButton.append(createSvgIcon("M8 8h11v11H8z M5 16H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"), document.createTextNode("نسخ رسالة مقترحة")); |
| copyButton.addEventListener("click", () => copyText(message, "تم نسخ الرسالة المقترحة")); |
| actions.append(copyButton); |
| item.append(actions); |
| } |
|
|
| return item; |
| } |
|
|
| function createCard(row, index) { |
| const card = document.createElement("article"); |
| card.className = "sample-card"; |
|
|
| const header = document.createElement("div"); |
| header.className = "sample-card-header"; |
| const heading = document.createElement("div"); |
| const indexEl = document.createElement("span"); |
| indexEl.className = "sample-index"; |
| indexEl.textContent = `منشأة ${index + 1}`; |
| const title = document.createElement("h3"); |
| title.textContent = row.establishmentName || "منشأة دون اسم"; |
| heading.append(indexEl, title); |
| const badge = document.createElement("span"); |
| badge.className = "status-badge"; |
| badge.textContent = row.complianceStatus || "غير محدد"; |
| header.append(heading, badge); |
|
|
| const details = document.createElement("div"); |
| details.className = "sample-details"; |
| [ |
| ["السجل التجاري", row.commercialRecord, "ltr-value wide-detail"], |
| ["توضيح المدينة", row.cityClarification, "wide-detail"], |
| ].forEach(([label, value, className]) => { |
| if (String(value ?? "").trim()) details.append(createDetail(label, value, className)); |
| }); |
| const coordinatesBlock = createCoordinatesBlock(row); |
| if (coordinatesBlock) details.append(coordinatesBlock); |
|
|
| card.append(header, details); |
| return card; |
| } |
|
|
| function renderVisibleRows(reset = false) { |
| if (reset) { |
| renderedCount = 0; |
| samplesGrid.replaceChildren(); |
| } |
| const fragment = document.createDocumentFragment(); |
| visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE).forEach((row, offset) => { |
| fragment.append(createCard(row, renderedCount + offset)); |
| }); |
| samplesGrid.append(fragment); |
| renderedCount += Math.min(PAGE_SIZE, visibleRows.length - renderedCount); |
| const remaining = visibleRows.length - renderedCount; |
| loadMoreWrap.hidden = remaining <= 0; |
| loadMoreButton.textContent = `عرض المزيد (${Math.min(PAGE_SIZE, remaining)})`; |
| loadMoreMeta.textContent = `تم عرض ${renderedCount} من أصل ${visibleRows.length} منشأة`; |
| } |
|
|
| function applyFilters() { |
| const region = regionSelect.value; |
| const city = citySelect.value; |
| const query = normalize(searchInput.value); |
|
|
| const scopedRows = rows.filter((row) => { |
| const matchesRegion = !region || row.region === region; |
| const matchesCity = !city || row.city === city; |
| return matchesRegion && matchesCity; |
| }); |
|
|
| visibleRows = sortRowsByLocationInfo(scopedRows.filter((row) => { |
| if (!query) return Boolean(city || region); |
| return ( |
| normalize(row.establishmentName).includes(query) || |
| normalize(row.commercialRecord).includes(query) || |
| normalize(row.cityClarification).includes(query) |
| ); |
| })); |
|
|
| const shouldShow = visibleRows.length > 0 || city || region || query; |
| emptyState.hidden = shouldShow; |
| resultsSection.hidden = !shouldShow; |
| noResults.hidden = visibleRows.length !== 0; |
| resultsTitle.textContent = city || region || "نتائج البحث"; |
| resultsMeta.textContent = `${visibleRows.length} من أصل ${scopedRows.length} منشأة`; |
| renderVisibleRows(true); |
| saveFilterState(); |
| } |
|
|
| function initializeDashboard(data) { |
| rows = data.rows || []; |
| const regions = data.regions || uniqueSorted(rows.map((row) => row.region)); |
| setOptions(regionSelect, regions, "اختر المنطقة"); |
| const regionCounts = new Map(); |
| rows.forEach((row) => regionCounts.set(row.region, (regionCounts.get(row.region) || 0) + 1)); |
| regionButtons.replaceChildren( |
| ...regions.map((region) => { |
| const button = document.createElement("button"); |
| button.type = "button"; |
| button.className = "region-choice"; |
| button.dataset.region = region; |
| button.setAttribute("role", "radio"); |
| button.setAttribute("aria-checked", "false"); |
| const name = document.createElement("span"); |
| name.textContent = region; |
| const count = document.createElement("strong"); |
| count.textContent = regionCounts.get(region) || 0; |
| button.append(name, count); |
| button.addEventListener("click", () => setRegion(region)); |
| return button; |
| }), |
| ); |
| setOptions(citySelect, [], "اختر المدينة الصناعية"); |
| citySelect.disabled = true; |
| restoreFilterState(); |
| } |
|
|
| function restoreFilterState() { |
| const saved = getSavedFilterState(); |
| isRestoringState = true; |
| searchInput.value = saved.search || ""; |
| if (saved.region && [...regionSelect.options].some((option) => option.value === saved.region)) { |
| setRegion(saved.region); |
| if (saved.city && [...citySelect.options].some((option) => option.value === saved.city)) { |
| citySelect.value = saved.city; |
| } |
| } else { |
| setRegion(""); |
| } |
| isRestoringState = false; |
| applyFilters(); |
| } |
|
|
| function performLogout() { |
| rows = []; |
| visibleRows = []; |
| searchInput.value = ""; |
| setRegion(""); |
| citySelect.value = ""; |
| dashboardView.hidden = true; |
| loginView.hidden = false; |
| passwordInput.focus(); |
| } |
|
|
| loginForm.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| const password = passwordInput.value.trim(); |
| if (!password) return; |
| loginButton.disabled = true; |
| loginButton.querySelector("span").textContent = "جاري التحقق..."; |
| loginError.textContent = ""; |
|
|
| try { |
| const data = await decryptData(password); |
| initializeDashboard(data); |
| passwordInput.value = ""; |
| loginView.hidden = true; |
| dashboardView.hidden = false; |
| } catch { |
| loginError.textContent = "رمز الدخول غير صحيح."; |
| passwordInput.select(); |
| } finally { |
| loginButton.disabled = false; |
| loginButton.querySelector("span").textContent = "دخول"; |
| } |
| }); |
|
|
| togglePassword.addEventListener("click", () => { |
| const showing = passwordInput.type === "text"; |
| passwordInput.type = showing ? "password" : "text"; |
| togglePassword.setAttribute("aria-label", showing ? "إظهار رمز الدخول" : "إخفاء رمز الدخول"); |
| }); |
|
|
| logoutButton.addEventListener("click", performLogout); |
|
|
| regionSelect.addEventListener("change", () => setRegion(regionSelect.value)); |
|
|
| citySelect.addEventListener("change", applyFilters); |
| searchInput.addEventListener("input", applyFilters); |
| loadMoreButton.addEventListener("click", () => renderVisibleRows()); |
| clearFiltersButton.addEventListener("click", () => { |
| searchInput.value = ""; |
| localStorage.removeItem(STATE_KEY); |
| setRegion(""); |
| showToast("تم مسح الاختيارات"); |
| }); |
|
|
| if (!window.crypto?.subtle || typeof ENCRYPTED_DATA === "undefined") { |
| loginButton.disabled = true; |
| loginError.textContent = "تعذر تشغيل التشفير في هذا المتصفح. استخدم متصفحًا حديثًا."; |
| } |
|
|