"use strict";
const PAGE_SIZE = 24;
const ADMIN_REFRESH_INTERVAL = 60000;
const STATE_KEY = "ics2ResearcherWorkspace";
const DOCUMENTATION_DONE_KEY = "ics2DocumentationCompleted";
const FORM_URL = "https://drive.google.com/file/d/1BEJ3qrqB3RMvhw4Z0i4dxZiwEreT9cJc/view?usp=sharing";
const MADON_GROUP_URL = "https://chat.whatsapp.com/CeSJId4TjanKJk7xAMWOAg?mode=gi_t";
const REPRESENTATIVES = [
{ match: ["الصناعية الأولى", "الصناعية الاولى"], city: "المدينة الصناعية الأولى بالدمام", name: "يوسف العباد", phone: "0501444214" },
{ match: ["الصناعية الثانية"], city: "المدينة الصناعية الثانية بالدمام", name: "سالم بوسوده", phone: "0560503707" },
{ match: ["الصناعية الثالثة"], city: "المدينة الصناعية الثالثة بالدمام", name: "إبراهيم الغامدي", phone: "0555309388" },
{ match: ["الأحساء", "الاحساء", "العيون", "واحة مدن"], city: "المدينة الصناعية الأولى بالأحساء", name: "أحمد الخليفة", phone: "0506448053" },
{ match: ["حفر الباطن"], city: "المدينة الصناعية بحفر الباطن", name: "براك المطيري", phone: "0540055303", role: "مدير المدينة الصناعية بحفر الباطن" },
];
const ICONS = {
copy: "M8 8h11v11H8z M5 16H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1",
map: "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",
whatsapp: "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 elements = Object.fromEntries(
[
"loginView", "loginForm", "password", "togglePassword", "loginButton", "loginError",
"researcherView", "researcherSearch", "researcherGrid", "entitySearchResults",
"researcherModeButton", "entityModeButton", "dashboardView", "researcherName",
"switchResearcherButton", "logoutButton", "summaryStats", "researcherMapLink", "formLink",
"shareFormButton", "representativesDirectory", "toggleContactsButton", "cityButtons", "searchInput", "statusSelect", "locationSelect",
"clearFiltersButton", "resultsMeta", "resultsTitle", "samplesGrid",
"noResults", "loadMoreWrap", "loadMoreButton", "loadMoreMeta", "toast",
"selectionBar", "selectionCount", "clearSelectionButton", "sendSelectionButton",
"shareDialog", "closeShareDialog", "shareForm", "recipientPhone", "recipientPhoneError",
"messagePreview", "copyShareMessage", "sendShareButton",
"madonDialog", "closeMadonDialog", "madonForm", "madonRepresentativeOptions",
"madonMessagePreview", "copyMadonMessage", "openMadonGroup", "sendMadonMessage",
"documentationDialog", "documentationDialogTitle", "closeDocumentationDialog", "documentationEntity",
"documentationUnavailable", "documentationForm", "fieldStatus", "otherStatusField", "otherStatus", "fieldStatement",
"statementCount", "documentationPhotos", "photoPreviews", "documentationError",
"uploadProgress", "uploadProgressBar", "cancelDocumentationButton", "saveDocumentationButton",
"adminView", "adminLogoutButton", "adminUpdatedAt", "adminSyncStatus", "refreshAdminButton", "adminStats", "adminInsight",
"adminProgressOverview", "adminTrendTotal", "adminTrendChart", "adminCityProgress",
"adminControls", "adminSearch", "adminResearcherFilter", "adminCityFilter", "adminDateFilter",
"adminStatusFilter", "clearAdminFilters", "activeResearchersCount",
"researcherProductivity", "adminRecordsCount", "adminRecords", "adminEmpty",
].map((id) => [id, document.getElementById(id)]),
);
let payload = null;
let activeResearcher = null;
let researcherRows = [];
let visibleRows = [];
let renderedCount = 0;
let toastTimer;
let shareRow = null;
let madonRow = null;
let selectedRepresentative = null;
const selectedRows = new Set();
let isBulkMadonMessage = false;
let sessionAccessCode = "";
let documentationRow = null;
let documentationFiles = [];
let documentationExistingPhotos = [];
let documentationRecords = [];
let lastSavedRowId = "";
let adminRefreshTimer = null;
let adminRefreshInProgress = false;
let adminDashboardInitialized = false;
let newAdminRecordIds = new Set();
let adminRecords = [];
let directoryMode = "researcher";
const mobileQuery = window.matchMedia("(max-width: 560px)");
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, encryptedSource = ENCRYPTED_DATA) {
const salt = base64ToBytes(encryptedSource.salt);
const iv = base64ToBytes(encryptedSource.iv);
const combined = base64ToBytes(encryptedSource.payload);
const cipherText = combined.slice(0, -16);
const authTag = combined.slice(-16);
const encrypted = new Uint8Array(cipherText.length + authTag.length);
encrypted.set(cipherText);
encrypted.set(authTag, cipherText.length);
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"],
);
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: encryptedSource.iterations, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"],
);
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encrypted);
return JSON.parse(new TextDecoder().decode(plain));
}
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 createIcon(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 element = document.createElementNS("http://www.w3.org/2000/svg", "path");
element.setAttribute("d", path);
svg.append(element);
return svg;
}
function showToast(message) {
clearTimeout(toastTimer);
elements.toast.textContent = message;
elements.toast.classList.add("show");
toastTimer = setTimeout(() => elements.toast.classList.remove("show"), 4000);
}
function documentationReady() {
return Boolean(
typeof DOCUMENTATION_CONFIG === "object" &&
/^https:\/\/script\.google\.com\/macros\/s\/.+\/exec$/.test(DOCUMENTATION_CONFIG.endpoint),
);
}
async function fetchDocumentation(action, accessCode) {
if (!documentationReady()) throw new Error("خدمة التوثيق غير متاحة.");
const url = new URL(DOCUMENTATION_CONFIG.endpoint);
url.searchParams.set("action", action);
url.searchParams.set("accessCode", accessCode);
const response = await fetch(url);
const result = await response.json();
if (!result.ok) throw new Error(result.error || "تعذر مزامنة بيانات التوثيق.");
return result;
}
function latestRecordsBySample(records) {
const map = new Map();
records.forEach((record) => {
if (!record.sampleKey || map.has(record.sampleKey)) return;
map.set(record.sampleKey, record);
});
return map;
}
function latestRecordsByCommercialRecord(records) {
const map = new Map();
records.forEach((record) => {
if (!record.commercialRecord || map.has(record.commercialRecord)) return;
map.set(record.commercialRecord, record);
});
return map;
}
function documentationRecord(row) {
return (
latestRecordsBySample(documentationRecords).get(row.sampleKey) ||
latestRecordsByCommercialRecord(documentationRecords).get(row.commercialRecord)
);
}
function hydrateRecordKeys(records) {
const currentKeys = new Set(payload?.rows.map((row) => row.sampleKey) || []);
return records.map((record) => {
if (record.sampleKey && currentKeys.has(record.sampleKey)) return record;
const match = payload?.rows.find((row) => {
if (record.commercialRecord && row.commercialRecord === record.commercialRecord) {
return true;
}
return (
normalize(row.establishmentName) === normalize(record.establishmentName) &&
(!record.contractNumber || row.contractNumber === record.contractNumber)
);
});
return {
...record,
sampleKey: match?.sampleKey || record.sampleKey || "",
researcher: match?.researcher || record.researcher,
};
});
}
function preserveDocumentedRows(records) {
if (!payload?.rows?.length || !Array.isArray(records) || !records.length) return;
const existingKeys = new Set(payload.rows.map((row) => row.sampleKey));
const existingCommercialRecords = new Set(payload.rows.map((row) => row.commercialRecord).filter(Boolean));
const extraRows = [];
records.forEach((record) => {
if (!record.sampleKey || existingKeys.has(record.sampleKey) || !record.establishmentName) return;
if (record.commercialRecord && existingCommercialRecords.has(record.commercialRecord)) return;
existingKeys.add(record.sampleKey);
if (record.commercialRecord) existingCommercialRecords.add(record.commercialRecord);
extraRows.push({
researcher: record.researcher || "غير محدد",
establishmentName: record.establishmentName,
contractNumber: record.contractNumber || "لا يوجد رقم عقد",
city: record.city || "غير محدد",
sourceCity: record.city || "غير محدد",
representativeCity: record.city || "غير محدد",
status: record.fieldStatus || "توثيق سابق",
madonStatement: "",
madonNoteText: "",
coordinates: "",
locationType: "none",
commercialRecord: record.commercialRecord || "",
unifiedNumber: "",
activityCode: "",
activity: "",
sampleKey: record.sampleKey,
preservedDocumentation: true,
});
});
if (!extraRows.length) return;
payload.rows = [...payload.rows, ...extraRows];
const counts = new Map();
payload.rows.forEach((row) => counts.set(row.researcher, (counts.get(row.researcher) || 0) + 1));
const researchers = new Map(payload.researchers.map((researcher) => [researcher.name, researcher]));
counts.forEach((count, name) => {
const existing = researchers.get(name);
researchers.set(name, {
name,
count,
mapUrl: existing?.mapUrl || "",
});
});
payload.researchers = [...researchers.values()].sort((a, b) =>
a.name.localeCompare(b.name, "ar", { sensitivity: "base" }),
);
}
function riyadhToday() {
return new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Riyadh",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date());
}
function dateOffset(dateValue, offset) {
const [year, month, day] = dateValue.split("-").map(Number);
const date = new Date(Date.UTC(year, month - 1, day + offset));
return [
date.getUTCFullYear(),
String(date.getUTCMonth() + 1).padStart(2, "0"),
String(date.getUTCDate()).padStart(2, "0"),
].join("-");
}
function adminSnapshot() {
const records = [...latestRecordsBySample(adminRecords).values()];
const recordMap = new Map(records.map((record) => [record.sampleKey, record]));
const commercialRecordMap = latestRecordsByCommercialRecord(records);
const today = riyadhToday();
const yesterday = dateOffset(today, -1);
const documented = payload.rows.filter((row) =>
recordMap.has(row.sampleKey) ||
Boolean(row.commercialRecord && commercialRecordMap.has(row.commercialRecord)),
);
const todayRecords = records.filter((record) => record.documentedDate === today);
const yesterdayRecords = records.filter((record) => record.documentedDate === yesterday);
const activeToday = new Set(todayRecords.map((record) => record.researcher).filter(Boolean));
return {
records,
recordMap,
commercialRecordMap,
today,
yesterday,
documented,
todayRecords,
yesterdayRecords,
activeToday,
percentage: payload.rows.length ? (documented.length / payload.rows.length) * 100 : 0,
};
}
async function syncResearcherDocumentation() {
try {
const result = await fetchDocumentation("status", sessionAccessCode);
documentationRecords = hydrateRecordKeys(result.records || []);
preserveDocumentedRows(documentationRecords);
} catch (error) {
console.warn(error.message);
documentationRecords = [];
}
}
function adminStat(value, label, note = "") {
const item = document.createElement("article");
item.className = "admin-stat";
const strong = document.createElement("strong");
strong.textContent = value;
const span = document.createElement("span");
span.textContent = label;
item.append(strong, span);
if (note) {
const small = document.createElement("small");
small.textContent = note;
item.append(small);
}
return item;
}
function setAdminFilterOptions() {
const selectedResearcher = elements.adminResearcherFilter.value;
const selectedCity = elements.adminCityFilter.value;
const selectedStatus = elements.adminStatusFilter.value;
setSelectOptions(
elements.adminResearcherFilter,
uniqueSorted(payload.researchers.map((item) => item.name)),
"جميع الباحثين",
);
setSelectOptions(
elements.adminCityFilter,
uniqueSorted(payload.rows.map((row) => row.city)),
"جميع المدن",
);
setSelectOptions(
elements.adminStatusFilter,
uniqueSorted(adminRecords.map((record) => record.fieldStatus)),
"جميع الحالات",
);
if ([...elements.adminResearcherFilter.options].some((option) => option.value === selectedResearcher)) {
elements.adminResearcherFilter.value = selectedResearcher;
}
if ([...elements.adminCityFilter.options].some((option) => option.value === selectedCity)) {
elements.adminCityFilter.value = selectedCity;
}
if ([...elements.adminStatusFilter.options].some((option) => option.value === selectedStatus)) {
elements.adminStatusFilter.value = selectedStatus;
}
}
function adminRecordIdentity(record) {
return record.documentationId || record.sampleKey || `${record.commercialRecord}|${record.establishmentName}`;
}
function setAdminSyncState(state, text) {
elements.adminSyncStatus.className = `sync-status ${state}`;
elements.adminSyncStatus.querySelector("span").textContent = text;
}
function renderAdminStats() {
const snapshot = adminSnapshot();
const inactiveToday = Math.max(payload.researchers.length - snapshot.activeToday.size, 0);
elements.adminStats.replaceChildren(
adminStat(payload.rows.length, "إجمالي العينات"),
adminStat(snapshot.documented.length, "تم التوثيق", `${snapshot.percentage.toFixed(1)}% من الإجمالي`),
adminStat(payload.rows.length - snapshot.documented.length, "المتبقي"),
adminStat(snapshot.todayRecords.length, "إنجاز اليوم", snapshot.today),
adminStat(snapshot.yesterdayRecords.length, "إنجاز أمس"),
adminStat(snapshot.activeToday.size, "باحثون أنجزوا اليوم", `${inactiveToday} دون إنجاز اليوم`),
);
renderAdminInsight(snapshot, inactiveToday);
}
function renderAdminInsight(snapshot, inactiveToday) {
const todayByResearcher = new Map();
snapshot.todayRecords.forEach((record) => {
if (!record.researcher) return;
todayByResearcher.set(record.researcher, (todayByResearcher.get(record.researcher) || 0) + 1);
});
const leader = [...todayByResearcher.entries()].sort((a, b) => b[1] - a[1])[0];
const difference = snapshot.todayRecords.length - snapshot.yesterdayRecords.length;
const trendText = difference > 0
? `إنجاز اليوم أعلى من أمس بـ ${difference}`
: difference < 0
? `إنجاز اليوم أقل من أمس بـ ${Math.abs(difference)}`
: "إنجاز اليوم مماثل لأمس";
const leaderText = leader
? `الأعلى اليوم: ${leader[0]} بعدد ${leader[1]}`
: "لم يسجل أي إنجاز اليوم حتى الآن";
elements.adminInsight.className = `admin-insight ${difference < 0 ? "needs-attention" : "positive"}`;
elements.adminInsight.innerHTML = `
${difference < 0 ? "!" : "✓"}
${trendText}
${leaderText} · ${inactiveToday} باحث دون إنجاز اليوم.
`;
}
function renderAdminOverview() {
const snapshot = adminSnapshot();
const remaining = Math.max(payload.rows.length - snapshot.documented.length, 0);
const degrees = Math.min(snapshot.percentage, 100) * 3.6;
elements.adminProgressOverview.innerHTML = `
${snapshot.percentage.toFixed(1)}%مكتمل
موثق${snapshot.documented.length}
متبقٍ${remaining}
تُحتسب النسبة من التوثيق الفعلي المسجل في Google Drive.
`;
const days = Array.from({ length: 7 }, (_, index) => dateOffset(snapshot.today, index - 6));
const dayCounts = days.map((day) =>
snapshot.records.filter((record) => record.documentedDate === day).length
);
const maxCount = Math.max(...dayCounts, 1);
const weekTotal = dayCounts.reduce((sum, count) => sum + count, 0);
const dayFormatter = new Intl.DateTimeFormat("ar-SA", {
weekday: "short",
timeZone: "UTC",
});
elements.adminTrendTotal.textContent = `${weekTotal} توثيق`;
elements.adminTrendChart.replaceChildren(
...days.map((day, index) => {
const item = document.createElement("div");
item.className = "trend-day";
const date = new Date(`${day}T12:00:00Z`);
const height = dayCounts[index] ? Math.max((dayCounts[index] / maxCount) * 100, 12) : 3;
item.innerHTML = `
${dayCounts[index]}
${dayFormatter.format(date)}`;
if (day === snapshot.today) item.classList.add("today");
return item;
}),
);
const totalsByCity = new Map();
payload.rows.forEach((row) => totalsByCity.set(row.city, (totalsByCity.get(row.city) || 0) + 1));
const doneByCity = new Map();
snapshot.documented.forEach((row) => doneByCity.set(row.city, (doneByCity.get(row.city) || 0) + 1));
const cities = [...totalsByCity].map(([city, total]) => {
const done = doneByCity.get(city) || 0;
return { city, total, done, percent: total ? (done / total) * 100 : 0 };
}).sort((a, b) => b.done - a.done || b.percent - a.percent || a.city.localeCompare(b.city, "ar"));
elements.adminCityProgress.replaceChildren(
...cities.slice(0, 6).map((item) => {
const button = document.createElement("button");
button.type = "button";
button.className = "city-progress-row";
button.innerHTML = `
${item.city || "غير محدد"}${item.done} / ${item.total}
${item.percent.toFixed(1)}% مكتمل`;
button.addEventListener("click", () => {
elements.adminCityFilter.value = item.city;
renderAdminRecords();
elements.adminControls?.scrollIntoView?.({ behavior: "smooth", block: "start" });
});
return button;
}),
);
}
function filteredAdminRecords() {
const query = normalize(elements.adminSearch.value);
const researcher = elements.adminResearcherFilter.value;
const city = elements.adminCityFilter.value;
const date = elements.adminDateFilter.value;
const status = elements.adminStatusFilter.value;
return adminRecords.filter((record) => {
if (researcher && record.researcher !== researcher) return false;
if (city && record.city !== city) return false;
if (date && record.documentedDate !== date) return false;
if (status && record.fieldStatus !== status) return false;
if (!query) return true;
return normalize(
`${record.establishmentName} ${record.commercialRecord} ${record.researcher} ${record.city}`,
).includes(query);
}).sort((a, b) => {
const aTime = Date.parse(a.updatedAt || a.documentedAt || "") || 0;
const bTime = Date.parse(b.updatedAt || b.documentedAt || "") || 0;
return bTime - aTime;
});
}
function renderProductivity() {
const snapshot = adminSnapshot();
const totals = new Map(payload.researchers.map((item) => [item.name, item.count]));
const documented = new Map();
const today = new Map();
snapshot.records.forEach((record) => {
documented.set(record.researcher, (documented.get(record.researcher) || 0) + 1);
if (record.documentedDate === snapshot.today) {
today.set(record.researcher, (today.get(record.researcher) || 0) + 1);
}
});
const fragment = document.createDocumentFragment();
const researchers = payload.researchers
.map((item) => ({
name: item.name,
total: totals.get(item.name) || 0,
done: documented.get(item.name) || 0,
today: today.get(item.name) || 0,
}))
.sort((a, b) => b.done - a.done || b.today - a.today || a.name.localeCompare(b.name, "ar"));
elements.activeResearchersCount.textContent = `${snapshot.activeToday.size} نشط اليوم`;
researchers.forEach((item, index) => {
const row = document.createElement("button");
row.type = "button";
row.className = "productivity-row";
const percent = item.total ? (item.done / item.total) * 100 : 0;
row.innerHTML = `
${index + 1}
${item.name}
${item.today ? `${item.today} اليوم` : "دون إنجاز اليوم"} · متبقي ${item.total - item.done}
${item.done}من ${item.total}
${percent.toFixed(1)}%`;
row.addEventListener("click", () => {
elements.adminResearcherFilter.value = item.name;
renderAdminRecords();
});
fragment.append(row);
});
elements.researcherProductivity.replaceChildren(fragment);
}
function renderAdminRecords() {
const records = filteredAdminRecords();
const hasFilters = Boolean(
elements.adminSearch.value ||
elements.adminResearcherFilter.value ||
elements.adminCityFilter.value ||
elements.adminDateFilter.value ||
elements.adminStatusFilter.value
);
elements.adminRecordsCount.textContent = hasFilters
? `${records.length} من ${adminRecords.length}`
: `${records.length} عملية`;
elements.adminEmpty.hidden = records.length > 0;
const fragment = document.createDocumentFragment();
records.slice(0, 100).forEach((record) => {
const item = document.createElement("article");
item.className = "admin-record";
if (newAdminRecordIds.has(adminRecordIdentity(record))) item.classList.add("new-record");
const date = record.documentedAt
? new Intl.DateTimeFormat("ar-SA", {
timeZone: "Asia/Riyadh",
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(record.documentedAt))
: "-";
const heading = document.createElement("div");
heading.className = "admin-record-heading";
const title = document.createElement("div");
const name = document.createElement("strong");
name.textContent = record.establishmentName || "منشأة غير محددة";
const meta = document.createElement("small");
meta.textContent = `${record.researcher || "-"} · ${record.city || "-"}`;
title.append(name, meta);
const time = document.createElement("time");
const updatedDate = record.updatedAt
? new Intl.DateTimeFormat("ar-SA", {
timeZone: "Asia/Riyadh",
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(record.updatedAt))
: "";
time.textContent = updatedDate ? `آخر تعديل: ${updatedDate}` : date;
heading.append(title, time);
const badges = document.createElement("div");
badges.className = "admin-record-badges";
const documentedBadge = document.createElement("span");
documentedBadge.className = "admin-badge-documented";
documentedBadge.textContent = record.documentedDate === riyadhToday() ? "موثق اليوم" : "موثق";
const statusBadge = document.createElement("span");
statusBadge.textContent = record.fieldStatus || "غير محدد";
const photoBadge = document.createElement("span");
photoBadge.textContent = record.photoCount ? `${record.photoCount} صور` : "بدون صور";
badges.append(documentedBadge, statusBadge, photoBadge);
const documentedTime = Date.parse(record.documentedAt || "") || 0;
const updatedTime = Date.parse(record.updatedAt || "") || 0;
if (updatedTime && documentedTime && updatedTime - documentedTime > 60000) {
const editedBadge = document.createElement("span");
editedBadge.className = "admin-badge-edited";
editedBadge.textContent = "تم التعديل";
badges.append(editedBadge);
}
const details = document.createElement("div");
details.className = "admin-record-details";
details.append(
detailItem("السجل التجاري", record.commercialRecord || "-"),
detailItem("الحالة الميدانية", record.fieldStatus || "-"),
detailItem("عدد الصور", record.photoCount || 0),
);
if (record.statement) details.append(detailItem("الإفادة", record.statement, "wide"));
const actions = document.createElement("div");
actions.className = "admin-record-actions";
if (record.folderUrl) {
const folder = document.createElement("a");
folder.href = record.folderUrl;
folder.target = "_blank";
folder.rel = "noopener";
folder.textContent = "فتح المجلد";
actions.append(folder);
}
(record.photoUrls || []).slice(0, 3).forEach((url, index) => {
const photo = document.createElement("a");
photo.href = url;
photo.target = "_blank";
photo.rel = "noopener";
photo.textContent = `الصورة ${index + 1}`;
actions.append(photo);
});
item.append(heading, badges, details, actions);
fragment.append(item);
});
elements.adminRecords.replaceChildren(fragment);
}
async function loadAdminDashboard({ automatic = false } = {}) {
if (adminRefreshInProgress || sessionAccessCode !== "1448") return;
adminRefreshInProgress = true;
elements.refreshAdminButton.disabled = true;
setAdminSyncState("syncing", automatic ? "جارٍ التحديث التلقائي..." : "جارٍ تحديث البيانات...");
try {
const result = await fetchDocumentation("admin", sessionAccessCode);
const incomingRecords = hydrateRecordKeys(result.records || []);
preserveDocumentedRows(incomingRecords);
if (adminDashboardInitialized) {
const previousIds = new Set(adminRecords.map(adminRecordIdentity));
newAdminRecordIds = new Set(
incomingRecords
.map(adminRecordIdentity)
.filter((identity) => identity && !previousIds.has(identity)),
);
} else {
newAdminRecordIds.clear();
}
adminRecords = incomingRecords;
documentationRecords = adminRecords;
setAdminFilterOptions();
elements.adminUpdatedAt.textContent = `آخر مزامنة: ${new Intl.DateTimeFormat("ar-SA", {
timeZone: "Asia/Riyadh",
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(result.generatedAt))}`;
renderAdminStats();
renderAdminOverview();
renderProductivity();
renderAdminRecords();
adminDashboardInitialized = true;
setAdminSyncState("connected", newAdminRecordIds.size
? `${newAdminRecordIds.size} توثيق جديد`
: "محدّث تلقائيًا");
if (newAdminRecordIds.size) {
showToast(`وصل ${newAdminRecordIds.size} توثيق جديد وتم تحديث لوحة المشرف`);
setTimeout(() => {
newAdminRecordIds.clear();
elements.adminRecords.querySelectorAll(".new-record").forEach((item) => item.classList.remove("new-record"));
}, 6000);
}
} catch (error) {
console.warn(error.message);
setAdminSyncState("offline", "تعذر التحديث، ستتم إعادة المحاولة");
} finally {
elements.refreshAdminButton.disabled = false;
adminRefreshInProgress = false;
}
}
function stopAdminAutoRefresh() {
clearInterval(adminRefreshTimer);
adminRefreshTimer = null;
}
function startAdminAutoRefresh() {
stopAdminAutoRefresh();
if (sessionAccessCode !== "1448" || document.hidden) return;
adminRefreshTimer = setInterval(
() => loadAdminDashboard({ automatic: true }),
ADMIN_REFRESH_INTERVAL,
);
}
async function copyText(text, successMessage) {
try {
await navigator.clipboard.writeText(text);
} catch {
const area = document.createElement("textarea");
area.value = text;
area.style.cssText = "position:fixed;opacity:0";
document.body.append(area);
area.select();
document.execCommand("copy");
area.remove();
}
showToast(successMessage);
}
function localPhone(value) {
const digits = String(value ?? "").replace(/\D/g, "");
if (digits.startsWith("966")) return `0${digits.slice(3)}`;
return digits;
}
function internationalPhone(value) {
const digits = localPhone(value);
return digits.startsWith("0") ? `966${digits.slice(1)}` : digits;
}
function toEnglishDigits(value) {
return String(value ?? "")
.replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit)))
.replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit)));
}
function recipientPhone(value) {
let digits = toEnglishDigits(value).replace(/\D/g, "");
if (digits.startsWith("00966")) digits = digits.slice(2);
if (digits.startsWith("966")) digits = digits.slice(3);
if (digits.startsWith("0")) digits = digits.slice(1);
return /^5\d{8}$/.test(digits) ? `966${digits}` : "";
}
function locationRank(row) {
return { madon: 4, base: 3, statement: 2, none: 1 }[row.locationType] || 0;
}
function saveState() {
if (!activeResearcher) return;
localStorage.setItem(
STATE_KEY,
JSON.stringify({
researcher: activeResearcher.name,
city: elements.cityButtons.querySelector(".city-choice.active")?.dataset.city || "",
search: elements.searchInput.value,
status: elements.statusSelect.value,
location: elements.locationSelect.value,
}),
);
}
function savedState() {
try {
return JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
} catch {
return {};
}
}
function showOnly(view) {
[elements.loginView, elements.researcherView, elements.dashboardView, elements.adminView].forEach((item) => {
item.hidden = item !== view;
});
}
function renderResearchers() {
const query = normalize(elements.researcherSearch.value);
const fragment = document.createDocumentFragment();
payload.researchers
.filter((researcher) => !query || normalize(researcher.name).includes(query))
.forEach((researcher) => {
const button = document.createElement("button");
button.type = "button";
button.className = "researcher-choice";
const content = document.createElement("span");
const name = document.createElement("strong");
name.textContent = researcher.name;
const meta = document.createElement("small");
meta.textContent = `${researcher.count} عينة`;
content.append(name, meta);
const arrow = createIcon("m15 18-6-6 6-6");
button.append(content, arrow);
button.addEventListener("click", () => openResearcher(researcher));
fragment.append(button);
});
elements.researcherGrid.replaceChildren(fragment);
}
function setDirectoryMode(mode) {
directoryMode = mode;
const entityMode = mode === "entity";
elements.researcherModeButton.classList.toggle("active", !entityMode);
elements.entityModeButton.classList.toggle("active", entityMode);
elements.researcherGrid.hidden = entityMode;
elements.entitySearchResults.hidden = !entityMode;
elements.researcherSearch.placeholder = entityMode
? "ابحث باسم المنشأة أو السجل التجاري"
: "ابحث باسم الباحث";
elements.researcherSearch.value = "";
if (entityMode) renderEntitySearch();
else renderResearchers();
}
function renderEntitySearch() {
const query = normalize(elements.researcherSearch.value);
if (query.length < 2) {
elements.entitySearchResults.innerHTML =
'اكتب حرفين على الأقل للبحث في جميع المنشآت.
';
return;
}
const matches = payload.rows
.filter((row) =>
normalize(`${row.establishmentName} ${row.commercialRecord} ${row.unifiedNumber} ${row.researcher}`)
.includes(query))
.slice(0, 40);
if (!matches.length) {
elements.entitySearchResults.innerHTML =
'لا توجد منشأة مطابقة لبحثك.
';
return;
}
const fragment = document.createDocumentFragment();
matches.forEach((row) => {
const button = document.createElement("button");
button.type = "button";
button.className = "entity-result";
const text = document.createElement("span");
const name = document.createElement("strong");
name.textContent = row.establishmentName;
const meta = document.createElement("small");
meta.textContent = `${row.researcher} · ${row.city}`;
text.append(name, meta);
const record = document.createElement("span");
record.className = "entity-result-meta";
record.textContent = row.commercialRecord || "دون سجل";
button.append(text, record);
button.addEventListener("click", () => {
const researcher = payload.researchers.find((item) => item.name === row.researcher);
openResearcher(researcher, row.sampleKey);
});
fragment.append(button);
});
elements.entitySearchResults.replaceChildren(fragment);
}
function createStat(value, label, className) {
const item = document.createElement("div");
item.className = `stat-item ${className}`;
const strong = document.createElement("strong");
strong.textContent = value;
const span = document.createElement("span");
span.textContent = label;
item.append(strong, span);
return item;
}
function renderSummary() {
const records = latestRecordsBySample(documentationRecords);
const commercialRecords = latestRecordsByCommercialRecord(documentationRecords);
const documented = researcherRows.filter((row) =>
records.has(row.sampleKey) ||
Boolean(row.commercialRecord && commercialRecords.has(row.commercialRecord)),
).length;
const total = researcherRows.length;
const remaining = Math.max(total - documented, 0);
const percentage = total ? Math.round((documented / total) * 100) : 0;
const today = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Riyadh",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date());
const documentedToday = researcherRows.filter(
(row) => (records.get(row.sampleKey) || commercialRecords.get(row.commercialRecord))?.documentedDate === today,
).length;
const progress = document.createElement("div");
progress.className = "researcher-progress";
progress.innerHTML = `
${remaining === 0 && total ? "أحسنت، اكتملت جميع العينات" : motivationalMessage(percentage)}
${percentage}%
`;
elements.summaryStats.replaceChildren(
createStat(total, "إجمالي العينات", "total"),
createStat(documented, "تم التوثيق", "documented"),
createStat(remaining, "المتبقي", "remaining"),
createStat(documentedToday, "إنجاز اليوم", "today"),
progress,
);
}
function motivationalMessage(percentage) {
if (percentage >= 90) return "بقي القليل، شكرًا لجهودك";
if (percentage >= 50) return "تقدم ممتاز، واصل الإنجاز";
if (percentage > 0) return "بداية موفقة، كل توثيق يصنع فرقًا";
return "جاهز للبدء، نتمنى لك يومًا موفقًا";
}
function activeCity() {
return elements.cityButtons.querySelector(".city-choice.active")?.dataset.city || "";
}
function setActiveCity(city) {
elements.cityButtons.querySelectorAll(".city-choice").forEach((button) => {
const active = button.dataset.city === city;
button.classList.toggle("active", active);
button.setAttribute("aria-checked", String(active));
});
updateRepresentativeHighlight(city);
applyFilters();
}
function renderCities(preferredCity = "") {
const counts = new Map();
researcherRows.forEach((row) => counts.set(row.city, (counts.get(row.city) || 0) + 1));
const cities = uniqueSorted([...counts.keys()]);
const allButton = document.createElement("button");
allButton.type = "button";
allButton.className = "city-choice";
allButton.dataset.city = "";
allButton.innerHTML = `جميع المدن${researcherRows.length}`;
allButton.addEventListener("click", () => setActiveCity(""));
const buttons = [allButton];
cities.forEach((city) => {
const button = document.createElement("button");
button.type = "button";
button.className = "city-choice";
button.dataset.city = city;
const label = document.createElement("span");
label.textContent = city;
const count = document.createElement("strong");
count.textContent = counts.get(city);
button.append(label, count);
button.addEventListener("click", () => setActiveCity(city));
buttons.push(button);
});
elements.cityButtons.replaceChildren(...buttons);
const initialCity = cities.includes(preferredCity) ? preferredCity : "";
setActiveCity(initialCity);
}
function findRepresentative(city) {
const normalizedCity = normalize(city);
if (!normalizedCity) return null;
return REPRESENTATIVES.find((item) => item.match.some((term) => normalizedCity.includes(normalize(term))));
}
function actionButton(label, icon, onClick) {
const button = document.createElement("button");
button.type = "button";
button.className = "mini-action";
button.append(createIcon(icon), document.createTextNode(label));
button.addEventListener("click", onClick);
return button;
}
function actionLink(label, href, icon) {
const link = document.createElement("a");
link.className = "mini-action";
link.href = href;
link.target = "_blank";
link.rel = "noopener";
link.append(createIcon(icon), document.createTextNode(label));
return link;
}
function representativeMessage(representative, row = null) {
const lines = [
"السلام عليكم ورحمة الله وبركاته،",
"",
`الأستاذ/ ${representative.name} المحترم،`,
"",
"نأمل التكرم بإفادتنا عن المنشأة الموضحة أدناه، وتزويدنا بموقعها عند توفره، وذلك لاستكمال بيانات الزيارة الميدانية.",
];
if (row) {
lines.push(
"",
"*بيانات المنشأة:*",
`• المنشأة: ${row.establishmentName || "-"}`,
`• السجل التجاري: ${row.commercialRecord || "-"}`,
`• رقم العقد: ${row.contractNumber || "لا يوجد رقم عقد"}`,
`• المدينة الصناعية: ${row.city || "-"}`,
`• حالة العينة: ${row.status || "-"}`,
);
if (row.madonStatement) lines.push(`• إفادة مدن المسجلة: ${row.madonStatement}`);
}
lines.push("", "شاكرين لكم تعاونكم وتجاوبكم.");
return lines.join("\n");
}
function bulkRepresentativeMessage(representative, rows) {
const lines = [
"السلام عليكم ورحمة الله وبركاته،",
"",
`الأستاذ/ ${representative.name} المحترم،`,
"",
`نأمل التكرم بإفادتنا عن المنشآت التالية وعددها (${rows.length})، وتزويدنا بمواقعها عند توفرها، وذلك لاستكمال بيانات الزيارات الميدانية.`,
];
rows.forEach((row, index) => {
lines.push(
"",
`*${index + 1}. ${row.establishmentName || "منشأة دون اسم"}*`,
`• السجل التجاري: ${row.commercialRecord || "-"}`,
`• رقم العقد: ${row.contractNumber || "لا يوجد رقم عقد"}`,
`• المدينة الصناعية: ${row.city || "-"}`,
`• حالة العينة: ${row.status || "-"}`,
);
if (row.madonStatement) lines.push(`• إفادة مدن المسجلة: ${row.madonStatement}`);
if (row.madonNoteText) lines.push(`• ملاحظات مدن: ${row.madonNoteText}`);
});
lines.push("", "شاكرين لكم تعاونكم وتجاوبكم.");
return lines.join("\n");
}
function renderRepresentativesDirectory() {
const fragment = document.createDocumentFragment();
REPRESENTATIVES.forEach((representative) => {
const card = document.createElement("article");
card.className = "representative-card";
card.dataset.city = representative.city;
const header = document.createElement("div");
header.className = "representative-card-header";
const icon = document.createElement("span");
icon.className = "representative-icon";
icon.append(createIcon("M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8M19 8v6M22 11h-6"));
const text = document.createElement("div");
const city = document.createElement("small");
city.textContent = representative.role || representative.city;
const name = document.createElement("strong");
name.textContent = representative.name;
const phone = document.createElement("span");
phone.textContent = representative.phone;
text.append(city, name, phone);
header.append(icon, text);
const actions = document.createElement("div");
actions.className = "representative-card-actions";
actions.append(
actionButton("نسخ الرقم", ICONS.copy, () => copyText(representative.phone, "تم نسخ رقم الممثل")),
actionLink(
"واتساب",
`https://wa.me/${internationalPhone(representative.phone)}?text=${encodeURIComponent(representativeMessage(representative))}`,
ICONS.whatsapp,
),
);
card.append(header, actions);
fragment.append(card);
});
elements.representativesDirectory.replaceChildren(fragment);
}
function setContactsExpanded(expanded) {
elements.representativesDirectory.classList.toggle("collapsed", !expanded);
elements.toggleContactsButton.setAttribute("aria-expanded", String(expanded));
elements.toggleContactsButton.querySelector("span").textContent = expanded ? "إخفاء الأرقام" : "عرض أرقام الممثلين";
}
function updateRepresentativeHighlight(city) {
const cityRow = researcherRows.find((row) => row.city === city);
const representative = findRepresentative(cityRow?.representativeCity || city);
elements.representativesDirectory.querySelectorAll(".representative-card").forEach((card) => {
card.classList.toggle("recommended", Boolean(representative && card.dataset.city === representative.city));
});
}
function setSelectOptions(select, values, placeholder) {
select.replaceChildren(new Option(placeholder, ""));
values.forEach((value) => select.add(new Option(value, value)));
}
function openResearcher(researcher, focusSampleKey = "") {
clearSelection();
activeResearcher = researcher;
researcherRows = payload.rows
.filter((row) => row.researcher === researcher.name)
.sort((a, b) => locationRank(b) - locationRank(a))
.map((row) => ({ ...row, rowId: row.sampleKey }));
elements.researcherName.textContent = researcher.name;
elements.researcherMapLink.href = researcher.mapUrl || "#";
elements.researcherMapLink.hidden = !researcher.mapUrl;
elements.formLink.href = FORM_URL;
elements.searchInput.value = "";
elements.locationSelect.value = "";
setSelectOptions(elements.statusSelect, uniqueSorted(researcherRows.map((row) => row.status)), "جميع الحالات");
renderSummary();
renderRepresentativesDirectory();
setContactsExpanded(!mobileQuery.matches);
const state = savedState();
if (state.researcher === researcher.name) {
elements.searchInput.value = state.search || "";
elements.statusSelect.value = state.status || "";
elements.locationSelect.value = state.location || "";
}
renderCities(state.researcher === researcher.name ? state.city : "");
showOnly(elements.dashboardView);
window.scrollTo({ top: 0 });
saveState();
if (focusSampleKey) {
elements.searchInput.value =
researcherRows.find((row) => row.sampleKey === focusSampleKey)?.establishmentName || "";
applyFilters();
}
}
function detailItem(label, value, extraClass = "") {
const item = document.createElement("div");
item.className = `detail-item ${extraClass}`.trim();
const name = document.createElement("span");
name.className = "detail-label";
name.textContent = label;
const content = document.createElement("span");
content.className = "detail-value";
content.textContent = String(value);
item.append(name, content);
return item;
}
function createLocationBlock(row) {
if (!row.coordinates) return null;
const block = document.createElement("div");
block.className = `location-block location-${row.locationType}`;
if (row.coordinates) {
const heading = document.createElement("div");
heading.className = "location-heading";
const label = document.createElement("span");
label.textContent = row.locationType === "madon" ? "إحداثية مدن" : "الإحداثية الأساسية";
const source = document.createElement("small");
source.textContent = row.locationType === "madon" ? "الموقع المحدّث" : "حسب بيانات X وY";
heading.append(label, source);
block.append(heading);
block.append(
actionLink(
"فتح الموقع في خرائط Google",
`https://www.google.com/maps?q=${encodeURIComponent(row.coordinates)}`,
ICONS.map,
),
);
}
return block;
}
function createMadonInfo(row) {
if (!row.madonStatement && !row.madonNoteText) return null;
const section = document.createElement("div");
section.className = "madon-info";
if (row.madonStatement) {
const statement = document.createElement("div");
statement.className = "madon-info-item madon-report";
const title = document.createElement("span");
title.textContent = "إفادة مدن";
const text = document.createElement("p");
text.textContent = row.madonStatement;
statement.append(title, text);
section.append(statement);
}
if (row.madonNoteText) {
const note = document.createElement("div");
note.className = "madon-info-item madon-note";
const title = document.createElement("span");
title.textContent = "ملاحظات مدن";
const text = document.createElement("p");
text.textContent = row.madonNoteText;
note.append(title, text);
section.append(note);
}
return section;
}
function formMessage(row = null) {
const lines = [
"السلام عليكم ورحمة الله وبركاته،",
"",
row ? "السادة/ إدارة المنشأة المحترمين،" : "السادة الكرام،",
"نأمل التكرم باستكمال استمارة المسح عبر الرابط المرفق، وذلك لاستكمال بيانات الزيارة الميدانية.",
];
if (row) {
lines.push(
"",
"*بيانات المنشأة:*",
`• المنشأة: ${row.establishmentName || "-"}`,
`• السجل التجاري: ${row.commercialRecord || "-"}`,
`• رقم العقد: ${row.contractNumber || "لا يوجد رقم عقد"}`,
`• المدينة الصناعية: ${row.city || "-"}`,
);
}
lines.push(
"",
"*رابط الاستمارة:*",
FORM_URL,
"",
"نأمل إشعارنا بعد استكمال الاستمارة.",
"",
"شاكرين لكم حسن تعاونكم وتجاوبكم.",
);
return lines.join("\n");
}
function openShareDialog(row = null) {
shareRow = row;
elements.recipientPhone.value = localStorage.getItem("ics2LastRecipientPhone") || "";
elements.recipientPhoneError.textContent = "";
elements.messagePreview.value = formMessage(row);
updateShareButton();
elements.shareDialog.hidden = false;
document.body.classList.add("dialog-open");
requestAnimationFrame(() => elements.recipientPhone.focus());
}
function updateShareButton() {
elements.sendShareButton.disabled = !recipientPhone(elements.recipientPhone.value);
}
function closeShareDialog() {
elements.shareDialog.hidden = true;
document.body.classList.remove("dialog-open");
shareRow = null;
}
function renderMadonOptions(recommended) {
const fragment = document.createDocumentFragment();
REPRESENTATIVES.forEach((representative, index) => {
const label = document.createElement("label");
label.className = "representative-option";
const input = document.createElement("input");
input.type = "radio";
input.name = "madonRepresentative";
input.value = String(index);
input.checked = representative === recommended;
const content = document.createElement("span");
const name = document.createElement("strong");
name.textContent = representative.name;
const meta = document.createElement("small");
meta.textContent = `${representative.role || representative.city} • ${representative.phone}`;
content.append(name, meta);
const check = createIcon("m5 12 4 4L19 6");
label.append(input, content, check);
input.addEventListener("change", () => {
selectedRepresentative = representative;
elements.madonMessagePreview.value = isBulkMadonMessage
? bulkRepresentativeMessage(representative, selectedRowItems())
: representativeMessage(representative, madonRow);
});
fragment.append(label);
});
elements.madonRepresentativeOptions.replaceChildren(fragment);
}
function openMadonDialog(row) {
isBulkMadonMessage = false;
madonRow = row;
selectedRepresentative =
findRepresentative(row.representativeCity || row.city) || REPRESENTATIVES[0];
renderMadonOptions(selectedRepresentative);
elements.madonMessagePreview.value = representativeMessage(selectedRepresentative, row);
elements.madonDialog.hidden = false;
document.body.classList.add("dialog-open");
}
function selectedRowItems() {
return researcherRows.filter((row) => selectedRows.has(row.rowId));
}
function recommendedRepresentativeForRows(rows) {
const representatives = rows
.map((row) => findRepresentative(row.representativeCity || row.city))
.filter(Boolean);
if (!representatives.length) return REPRESENTATIVES[0];
const counts = new Map();
representatives.forEach((item) => counts.set(item, (counts.get(item) || 0) + 1));
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
}
function openBulkMadonDialog() {
const rows = selectedRowItems();
if (!rows.length) return;
isBulkMadonMessage = true;
madonRow = null;
selectedRepresentative = recommendedRepresentativeForRows(rows);
renderMadonOptions(selectedRepresentative);
elements.madonMessagePreview.value = bulkRepresentativeMessage(selectedRepresentative, rows);
elements.madonDialog.hidden = false;
document.body.classList.add("dialog-open");
}
function closeMadonDialog() {
elements.madonDialog.hidden = true;
document.body.classList.remove("dialog-open");
madonRow = null;
selectedRepresentative = null;
isBulkMadonMessage = false;
}
function renderPhotoPreviews() {
const fragment = document.createDocumentFragment();
documentationExistingPhotos.forEach((photo, index) => {
const item = document.createElement("div");
item.className = "photo-preview existing-photo";
const link = document.createElement("a");
link.href = photo.url;
link.target = "_blank";
link.rel = "noopener";
link.append(
createIcon(ICONS.map),
document.createTextNode(`صورة محفوظة ${index + 1}`),
);
const remove = document.createElement("button");
remove.type = "button";
remove.setAttribute("aria-label", `حذف الصورة المحفوظة ${index + 1}`);
remove.title = "حذف الصورة من التوثيق";
remove.append(createIcon("M18 6 6 18M6 6l12 12"));
remove.addEventListener("click", () => {
documentationExistingPhotos.splice(index, 1);
renderPhotoPreviews();
});
item.append(link, remove);
fragment.append(item);
});
documentationFiles.forEach((file, index) => {
const item = document.createElement("div");
item.className = "photo-preview";
const image = document.createElement("img");
image.src = URL.createObjectURL(file);
image.alt = `الصورة ${index + 1}`;
image.addEventListener("load", () => URL.revokeObjectURL(image.src), { once: true });
const remove = document.createElement("button");
remove.type = "button";
remove.setAttribute("aria-label", `حذف الصورة ${index + 1}`);
remove.title = "حذف الصورة";
remove.append(createIcon("M18 6 6 18M6 6l12 12"));
remove.addEventListener("click", () => {
documentationFiles.splice(index, 1);
renderPhotoPreviews();
});
item.append(image, remove);
fragment.append(item);
});
elements.photoPreviews.replaceChildren(fragment);
}
function openDocumentationDialog(row) {
documentationRow = row;
documentationFiles = [];
const record = documentationRecord(row);
documentationExistingPhotos = (record?.photoUrls || []).map((url) => ({ url }));
elements.documentationEntity.replaceChildren(
detailItem("المنشأة", row.establishmentName || "-", "wide"),
detailItem("السجل التجاري", row.commercialRecord || "-", "ltr-value"),
detailItem("رقم العقد", row.contractNumber || "لا يوجد رقم عقد", "ltr-value"),
);
const storedStatus = record?.fieldStatus || "";
const statusOptions = [...elements.fieldStatus.options].map((option) => option.value);
const knownStatus = statusOptions.includes(storedStatus);
const customStatus = storedStatus.startsWith("أخرى:")
? storedStatus.replace(/^أخرى:\s*/, "")
: storedStatus && !knownStatus
? storedStatus
: "";
const otherMode = storedStatus === "أخرى" || Boolean(customStatus);
elements.fieldStatus.value = otherMode ? "أخرى" : knownStatus ? storedStatus : "";
elements.otherStatus.value = "";
if (customStatus) elements.otherStatus.value = customStatus;
elements.otherStatusField.hidden = !otherMode;
elements.otherStatus.required = otherMode;
elements.fieldStatement.value = record?.statement || "";
elements.statementCount.textContent = String(elements.fieldStatement.value.length);
elements.documentationPhotos.value = "";
elements.documentationError.textContent = "";
elements.uploadProgress.hidden = true;
elements.uploadProgressBar.style.width = "0";
elements.documentationUnavailable.hidden = documentationReady();
elements.saveDocumentationButton.disabled = !documentationReady();
elements.documentationDialogTitle.textContent = record ? "تعديل التوثيق الميداني" : "إضافة إفادة ميدانية";
renderPhotoPreviews();
elements.documentationDialog.hidden = false;
document.body.classList.add("dialog-open");
}
function closeDocumentationDialog() {
elements.documentationDialog.hidden = true;
document.body.classList.remove("dialog-open");
documentationRow = null;
documentationFiles = [];
documentationExistingPhotos = [];
}
async function compressImage(file) {
let source;
let widthSource;
let heightSource;
if ("createImageBitmap" in window) {
source = await createImageBitmap(file);
widthSource = source.width;
heightSource = source.height;
} else {
source = await new Promise((resolve, reject) => {
const image = new Image();
const url = URL.createObjectURL(file);
image.onload = () => {
URL.revokeObjectURL(url);
resolve(image);
};
image.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("تعذر فتح إحدى الصور."));
};
image.src = url;
});
widthSource = source.naturalWidth;
heightSource = source.naturalHeight;
}
const maxDimension = DOCUMENTATION_CONFIG.maxImageDimension || 1600;
const scale = Math.min(1, maxDimension / Math.max(widthSource, heightSource));
const width = Math.max(1, Math.round(widthSource * scale));
const height = Math.max(1, Math.round(heightSource * scale));
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d", { alpha: false }).drawImage(source, 0, 0, width, height);
if (typeof source.close === "function") source.close();
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/jpeg", DOCUMENTATION_CONFIG.jpegQuality || 0.78),
);
if (!blob) throw new Error("تعذر تجهيز إحدى الصور.");
return blob;
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result).split(",")[1] || "");
reader.onerror = () => reject(new Error("تعذر قراءة الصورة."));
reader.readAsDataURL(blob);
});
}
async function buildDocumentationPhotos() {
const output = [];
for (let index = 0; index < documentationFiles.length; index += 1) {
elements.uploadProgressBar.style.width = `${15 + Math.round((index / documentationFiles.length) * 45)}%`;
const compressed = await compressImage(documentationFiles[index]);
output.push({
mimeType: "image/jpeg",
base64: await blobToBase64(compressed),
});
}
return output;
}
function updateSelectionBar() {
const count = selectedRows.size;
elements.selectionBar.hidden = count === 0;
elements.selectionCount.textContent = `${count} ${count === 1 ? "منشأة محددة" : "منشآت محددة"}`;
document.body.classList.toggle("has-selection", count > 0);
elements.samplesGrid.querySelectorAll(".sample-card").forEach((card) => {
const selected = selectedRows.has(card.dataset.rowId);
card.classList.toggle("selected", selected);
const input = card.querySelector(".sample-selector input");
if (input) input.checked = selected;
});
}
function clearSelection() {
selectedRows.clear();
updateSelectionBar();
}
function completedDocumentationMap() {
try {
return JSON.parse(localStorage.getItem(DOCUMENTATION_DONE_KEY) || "{}");
} catch {
return {};
}
}
function isDocumentationCompleted(rowId) {
const row = researcherRows.find((item) => item.rowId === rowId || item.sampleKey === rowId);
return Boolean(
latestRecordsBySample(documentationRecords).has(rowId) ||
(row?.commercialRecord && latestRecordsByCommercialRecord(documentationRecords).has(row.commercialRecord)) ||
completedDocumentationMap()[rowId],
);
}
function documentedRowsFirst(rows) {
const records = latestRecordsBySample(documentationRecords);
const commercialRecords = latestRecordsByCommercialRecord(documentationRecords);
const locallyCompleted = completedDocumentationMap();
return [...rows].sort((a, b) => {
const aRecord = records.get(a.sampleKey) || commercialRecords.get(a.commercialRecord);
const bRecord = records.get(b.sampleKey) || commercialRecords.get(b.commercialRecord);
const aCompleted = Boolean(aRecord || locallyCompleted[a.rowId]);
const bCompleted = Boolean(bRecord || locallyCompleted[b.rowId]);
if (aCompleted !== bCompleted) return bCompleted - aCompleted;
if (!aCompleted) return 0;
const aTime = Date.parse(aRecord?.documentedAt || "") || 0;
const bTime = Date.parse(bRecord?.documentedAt || "") || 0;
return bTime - aTime;
});
}
function markDocumentationCompleted(rowId, record = {}) {
if (!rowId) return;
const completed = completedDocumentationMap();
completed[rowId] = true;
localStorage.setItem(DOCUMENTATION_DONE_KEY, JSON.stringify(completed));
documentationRecords = [
{
sampleKey: rowId,
documentedAt: record.documentedAt || new Date().toISOString(),
documentedDate: record.documentedDate || new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Riyadh",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date()),
...record,
},
...documentationRecords.filter((item) => item.sampleKey !== rowId),
];
const card = elements.samplesGrid.querySelector(`[data-row-id="${rowId}"]`);
if (card) card.classList.add("completed");
renderSummary();
}
function createCard(row, index) {
const card = document.createElement("article");
card.className = "sample-card";
card.dataset.rowId = row.rowId;
if (row.rowId === lastSavedRowId) card.classList.add("recently-documented");
if (isDocumentationCompleted(row.rowId)) {
card.classList.add("completed");
const record = documentationRecord(row);
if (record?.documentedAt) {
card.dataset.documentedAt = new Intl.DateTimeFormat("ar-SA", {
timeZone: "Asia/Riyadh",
dateStyle: "medium",
}).format(new Date(record.documentedAt));
}
}
const header = document.createElement("div");
header.className = "card-header";
const titleWrap = document.createElement("div");
const selector = document.createElement("label");
selector.className = "sample-selector";
const selectorInput = document.createElement("input");
selectorInput.type = "checkbox";
selectorInput.checked = selectedRows.has(row.rowId);
const selectorMark = document.createElement("span");
selectorMark.append(createIcon("m5 12 4 4L19 6"));
const selectorText = document.createElement("span");
selectorText.textContent = "تحديد";
selector.append(selectorInput, selectorMark, selectorText);
selectorInput.addEventListener("change", () => {
if (selectorInput.checked) selectedRows.add(row.rowId);
else selectedRows.delete(row.rowId);
updateSelectionBar();
});
const indexLabel = document.createElement("span");
indexLabel.className = "sample-index";
indexLabel.textContent = `عينة ${index + 1}`;
const title = document.createElement("h3");
title.textContent = row.establishmentName || "منشأة دون اسم";
titleWrap.append(selector, indexLabel, title);
const badges = document.createElement("div");
badges.className = "card-badges";
if (isDocumentationCompleted(row.rowId)) {
const documentedBadge = document.createElement("span");
documentedBadge.className = "documented-badge";
documentedBadge.textContent = "تم التوثيق بنجاح";
badges.append(documentedBadge);
}
const badge = document.createElement("span");
badge.className = `status-badge status-${normalize(row.status).includes("مغلق") ? "closed" : "default"}`;
badge.textContent = row.status || "غير محدد";
const locationBadge = document.createElement("span");
locationBadge.className = `location-badge location-badge-${row.locationType}`;
locationBadge.textContent = {
madon: "موقع مدن",
base: "موقع أساسي",
statement: "إفادة مدن",
none: "دون موقع",
}[row.locationType] || "دون موقع";
badges.append(badge, locationBadge);
header.append(titleWrap, badges);
const details = document.createElement("div");
details.className = "card-details";
[
["السجل التجاري", row.commercialRecord, "ltr-value"],
["رقم العقد", row.contractNumber || "لا يوجد رقم عقد", "ltr-value"],
["الرقم الموحد", row.unifiedNumber, "ltr-value"],
["المدينة الصناعية", row.city, "wide"],
["النشاط", row.activity, "wide"],
].forEach(([label, value, className]) => {
if (String(value ?? "").trim()) details.append(detailItem(label, value, className));
});
const location = createLocationBlock(row);
if (location) details.append(location);
const madonInfo = createMadonInfo(row);
if (madonInfo) details.append(madonInfo);
const footer = document.createElement("div");
footer.className = "card-actions";
const documented = isDocumentationCompleted(row.rowId);
footer.append(
actionButton("رسالة لمسؤول مدن", ICONS.whatsapp, () => openMadonDialog(row)),
actionButton("مشاركة الاستمارة", ICONS.whatsapp, () => openShareDialog(row)),
actionButton(
documented ? "تعديل التوثيق" : "توثيق ميداني",
documented ? "M4 20h4l11-11-4-4L4 16v4Z M13.5 6.5l4 4" : "M5 3h14v18H5z M8 7h8M8 11h8M8 15h5",
() => openDocumentationDialog(row),
),
);
card.append(header, details, footer);
return card;
}
function renderRows(reset = false) {
if (reset) {
renderedCount = 0;
elements.samplesGrid.replaceChildren();
}
const fragment = document.createDocumentFragment();
visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE).forEach((row, offset) => {
fragment.append(createCard(row, renderedCount + offset));
});
elements.samplesGrid.append(fragment);
updateSelectionBar();
renderedCount += Math.min(PAGE_SIZE, visibleRows.length - renderedCount);
const remaining = visibleRows.length - renderedCount;
elements.loadMoreWrap.hidden = remaining <= 0;
elements.loadMoreMeta.textContent = remaining > 0 ? `متبقي ${remaining} عينة` : "";
}
function applyFilters() {
if (!activeResearcher) return;
const city = activeCity();
const query = normalize(elements.searchInput.value);
const status = elements.statusSelect.value;
const location = elements.locationSelect.value;
visibleRows = documentedRowsFirst(researcherRows.filter((row) => {
if (city && row.city !== city) return false;
if (status && row.status !== status) return false;
if (location === "statement" && !row.madonStatement && !row.madonNoteText) return false;
if (location && location !== "statement" && row.locationType !== location) return false;
if (query) {
const haystack = normalize(
[row.establishmentName, row.commercialRecord, row.unifiedNumber, row.activity].join(" "),
);
if (!haystack.includes(query)) return false;
}
return true;
}));
elements.resultsTitle.textContent = city || "جميع العينات";
elements.resultsMeta.textContent = `${visibleRows.length} من أصل ${researcherRows.length} عينة`;
elements.noResults.hidden = visibleRows.length > 0;
renderRows(true);
saveState();
}
function clearFilters() {
elements.searchInput.value = "";
elements.statusSelect.value = "";
elements.locationSelect.value = "";
setActiveCity("");
}
function shareForm() {
openShareDialog();
}
elements.loginForm.addEventListener("submit", async (event) => {
event.preventDefault();
elements.loginError.textContent = "";
elements.loginButton.disabled = true;
elements.loginButton.querySelector("span").textContent = "جارٍ الدخول...";
try {
const password = elements.password.value;
const supervisor = password === "1448";
payload = await decryptData(
password,
supervisor && typeof SUPERVISOR_ENCRYPTED_DATA === "object"
? SUPERVISOR_ENCRYPTED_DATA
: ENCRYPTED_DATA,
);
sessionAccessCode = password;
elements.password.value = "";
if (supervisor) {
adminDashboardInitialized = false;
newAdminRecordIds.clear();
setAdminFilterOptions();
showOnly(elements.adminView);
await loadAdminDashboard();
startAdminAutoRefresh();
return;
}
await syncResearcherDocumentation();
elements.researcherSearch.value = "";
setDirectoryMode("researcher");
renderResearchers();
showOnly(elements.researcherView);
} catch {
elements.loginError.textContent = "رمز الدخول غير صحيح.";
} finally {
elements.loginButton.disabled = false;
elements.loginButton.querySelector("span").textContent = "دخول";
}
});
elements.togglePassword.addEventListener("click", () => {
const visible = elements.password.type === "text";
elements.password.type = visible ? "password" : "text";
elements.togglePassword.setAttribute("aria-label", visible ? "إظهار رمز الدخول" : "إخفاء رمز الدخول");
});
elements.researcherSearch.addEventListener("input", () => {
if (directoryMode === "entity") renderEntitySearch();
else renderResearchers();
});
elements.researcherModeButton.addEventListener("click", () => setDirectoryMode("researcher"));
elements.entityModeButton.addEventListener("click", () => setDirectoryMode("entity"));
elements.switchResearcherButton.addEventListener("click", () => {
activeResearcher = null;
setDirectoryMode("researcher");
renderResearchers();
showOnly(elements.researcherView);
window.scrollTo({ top: 0 });
});
elements.logoutButton.addEventListener("click", () => {
payload = null;
activeResearcher = null;
researcherRows = [];
visibleRows = [];
sessionAccessCode = "";
showOnly(elements.loginView);
elements.password.focus();
});
elements.adminLogoutButton.addEventListener("click", () => {
stopAdminAutoRefresh();
payload = null;
adminRecords = [];
documentationRecords = [];
adminDashboardInitialized = false;
newAdminRecordIds.clear();
sessionAccessCode = "";
showOnly(elements.loginView);
elements.password.focus();
});
elements.refreshAdminButton.addEventListener("click", () => loadAdminDashboard());
document.addEventListener("visibilitychange", () => {
if (sessionAccessCode !== "1448") return;
if (document.hidden) {
stopAdminAutoRefresh();
setAdminSyncState("paused", "التحديث متوقف أثناء وجود الصفحة بالخلفية");
return;
}
loadAdminDashboard({ automatic: true });
startAdminAutoRefresh();
});
[elements.adminSearch, elements.adminResearcherFilter, elements.adminCityFilter, elements.adminDateFilter, elements.adminStatusFilter]
.forEach((control) => control.addEventListener(control.tagName === "INPUT" ? "input" : "change", renderAdminRecords));
elements.clearAdminFilters.addEventListener("click", () => {
elements.adminSearch.value = "";
elements.adminResearcherFilter.value = "";
elements.adminCityFilter.value = "";
elements.adminDateFilter.value = "";
elements.adminStatusFilter.value = "";
renderAdminRecords();
});
elements.searchInput.addEventListener("input", applyFilters);
elements.statusSelect.addEventListener("change", applyFilters);
elements.locationSelect.addEventListener("change", applyFilters);
elements.clearFiltersButton.addEventListener("click", clearFilters);
elements.loadMoreButton.addEventListener("click", () => renderRows(false));
elements.shareFormButton.addEventListener("click", shareForm);
elements.toggleContactsButton.addEventListener("click", () => {
setContactsExpanded(elements.toggleContactsButton.getAttribute("aria-expanded") !== "true");
});
elements.closeShareDialog.addEventListener("click", closeShareDialog);
elements.shareDialog.addEventListener("click", (event) => {
if (event.target === elements.shareDialog) closeShareDialog();
});
elements.recipientPhone.addEventListener("input", () => {
elements.recipientPhoneError.textContent = "";
updateShareButton();
});
elements.copyShareMessage.addEventListener("click", () =>
copyText(elements.messagePreview.value, "تم نسخ رسالة الاستمارة"),
);
elements.shareForm.addEventListener("submit", (event) => {
event.preventDefault();
const phone = recipientPhone(elements.recipientPhone.value);
if (!phone) {
elements.recipientPhoneError.textContent = "يرجى إدخال رقم جوال سعودي صحيح، مثال: 0501234567.";
elements.recipientPhone.focus();
return;
}
localStorage.setItem("ics2LastRecipientPhone", localPhone(phone));
const message = formMessage(shareRow);
window.open(`https://wa.me/${phone}?text=${encodeURIComponent(message)}`, "_blank", "noopener");
closeShareDialog();
});
elements.closeMadonDialog.addEventListener("click", closeMadonDialog);
elements.madonDialog.addEventListener("click", (event) => {
if (event.target === elements.madonDialog) closeMadonDialog();
});
elements.copyMadonMessage.addEventListener("click", () =>
copyText(elements.madonMessagePreview.value, "تم نسخ رسالة مسؤول مدن"),
);
elements.openMadonGroup.addEventListener("click", async () => {
await copyText(elements.madonMessagePreview.value, "تم نسخ الرسالة، ألصقها الآن في مجموعة مدن");
window.open(MADON_GROUP_URL, "_blank", "noopener");
});
elements.madonForm.addEventListener("submit", (event) => {
event.preventDefault();
if (!selectedRepresentative) return;
const message = isBulkMadonMessage
? bulkRepresentativeMessage(selectedRepresentative, selectedRowItems())
: representativeMessage(selectedRepresentative, madonRow);
window.open(
`https://wa.me/${internationalPhone(selectedRepresentative.phone)}?text=${encodeURIComponent(message)}`,
"_blank",
"noopener",
);
closeMadonDialog();
});
elements.closeDocumentationDialog.addEventListener("click", closeDocumentationDialog);
elements.cancelDocumentationButton.addEventListener("click", closeDocumentationDialog);
elements.documentationDialog.addEventListener("click", (event) => {
if (event.target === elements.documentationDialog) closeDocumentationDialog();
});
elements.fieldStatus.addEventListener("change", () => {
const isOther = elements.fieldStatus.value === "أخرى";
elements.otherStatusField.hidden = !isOther;
elements.otherStatus.required = isOther;
if (!isOther) elements.otherStatus.value = "";
elements.documentationError.textContent = "";
if (isOther) elements.otherStatus.focus();
});
elements.otherStatus.addEventListener("input", () => {
elements.documentationError.textContent = "";
});
elements.fieldStatement.addEventListener("input", () => {
elements.statementCount.textContent = String(elements.fieldStatement.value.length);
elements.documentationError.textContent = "";
});
elements.documentationPhotos.addEventListener("change", () => {
const incoming = [...elements.documentationPhotos.files].filter((file) => file.type.startsWith("image/"));
const maxPhotos = DOCUMENTATION_CONFIG.maxPhotos || 3;
const availableSlots = Math.max(maxPhotos - documentationExistingPhotos.length, 0);
const combined = [...documentationFiles, ...incoming];
documentationFiles = combined.slice(0, availableSlots);
elements.documentationPhotos.value = "";
elements.documentationError.textContent =
combined.length > availableSlots ? `الحد الأعلى ${maxPhotos} صور مع الصور المحفوظة.` : "";
renderPhotoPreviews();
});
elements.documentationForm.addEventListener("submit", async (event) => {
event.preventDefault();
elements.documentationError.textContent = "";
if (!documentationReady()) {
elements.documentationError.textContent = "خدمة التوثيق غير مفعلة بعد.";
return;
}
if (!elements.fieldStatus.value) {
elements.documentationError.textContent = "اختر الحالة الميدانية.";
elements.fieldStatus.focus();
return;
}
const customStatus = elements.otherStatus.value.trim();
if (elements.fieldStatus.value === "أخرى" && customStatus.length < 3) {
elements.documentationError.textContent = "اكتب توضيح الحالة الأخرى.";
elements.otherStatus.focus();
return;
}
if (!elements.fieldStatement.value.trim()) {
elements.documentationError.textContent = "اكتب الإفادة الميدانية.";
elements.fieldStatement.focus();
return;
}
elements.saveDocumentationButton.disabled = true;
elements.saveDocumentationButton.textContent = "جارٍ الحفظ...";
elements.uploadProgress.hidden = false;
elements.uploadProgressBar.style.width = "10%";
try {
const photos = await buildDocumentationPhotos();
elements.uploadProgressBar.style.width = "65%";
const existingRecord = documentationRecord(documentationRow);
const documentationId = existingRecord?.documentationId || crypto.randomUUID();
const isEditing = Boolean(existingRecord);
const response = await fetch(DOCUMENTATION_CONFIG.endpoint, {
method: "POST",
headers: { "Content-Type": "text/plain;charset=utf-8" },
body: JSON.stringify({
action: isEditing ? "updateEvidence" : "createEvidence",
accessCode: sessionAccessCode,
documentationId,
sampleKey: documentationRow.sampleKey,
researcher: activeResearcher.name,
establishmentName: documentationRow.establishmentName,
commercialRecord: documentationRow.commercialRecord,
contractNumber: documentationRow.contractNumber,
city: documentationRow.city,
fieldStatus: elements.fieldStatus.value === "أخرى" ? `أخرى: ${customStatus}` : elements.fieldStatus.value,
statement: elements.fieldStatement.value.trim(),
keptPhotoUrls: documentationExistingPhotos.map((photo) => photo.url),
photos,
}),
});
elements.uploadProgressBar.style.width = "90%";
const result = await response.json();
if (!result.ok) throw new Error(result.error || "تعذر حفظ التوثيق.");
elements.uploadProgressBar.style.width = "100%";
markDocumentationCompleted(documentationRow.rowId, result);
lastSavedRowId = documentationRow.rowId;
closeDocumentationDialog();
applyFilters();
const records = latestRecordsBySample(documentationRecords);
const commercialRecords = latestRecordsByCommercialRecord(documentationRecords);
const completedCount = researcherRows.filter((row) =>
records.has(row.sampleKey) ||
Boolean(row.commercialRecord && commercialRecords.has(row.commercialRecord)),
).length;
const remainingCount = Math.max(researcherRows.length - completedCount, 0);
showToast(
remainingCount === 0
? "أحسنت، تم استكمال جميع العينات المسندة إليك"
: `شكرًا لك، تم ${isEditing ? "تحديث" : "توثيق"} العينة وإضافتها إلى إنجاز اليوم. المتبقي ${remainingCount}`,
);
setTimeout(() => {
lastSavedRowId = "";
elements.samplesGrid.querySelector(".recently-documented")?.classList.remove("recently-documented");
}, 4200);
} catch (error) {
elements.documentationError.textContent =
error.message || "تعذر رفع التوثيق. تحقق من الاتصال وحاول مرة أخرى.";
elements.uploadProgress.hidden = true;
} finally {
elements.saveDocumentationButton.disabled = !documentationReady();
elements.saveDocumentationButton.replaceChildren(
createIcon("M5 3h12l2 2v16H5V3Z M8 3v6h8V3M8 21v-8h8v8"),
document.createTextNode("حفظ التوثيق"),
);
}
});
elements.clearSelectionButton.addEventListener("click", clearSelection);
elements.sendSelectionButton.addEventListener("click", openBulkMadonDialog);
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
if (!elements.shareDialog.hidden) closeShareDialog();
if (!elements.madonDialog.hidden) closeMadonDialog();
if (!elements.documentationDialog.hidden) closeDocumentationDialog();
});