"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(); });