ICS / app.js
stat2025's picture
Upload 6 files
35f6a40 verified
Raw
History Blame Contribute Delete
16.3 kB
"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 = "تعذر تشغيل التشفير في هذا المتصفح. استخدم متصفحًا حديثًا.";
}