"use strict"; const CONFIG = { researcherPassword: "2030", managerPassword: "20302030", managerValue: "__manager__", apiUrl: "https://script.google.com/macros/s/AKfycbwH2wJA6ASKfm9P5FL4X_AFcVpzIoGaCub2tZ2csdxYDhinwZhToKnxhkCQlKPJi4mXvg/exec", }; const FIELD_ALIASES = { commercial: ["السجل التجاري", "التجاري", "رقم السجل التجاري"], facility: ["اسم المنشأة", "اسم المنشاة", "المنشأة"], accountManager: ["مدير الحساب"], name: ["الاسم", "اسم المسؤول", "اسم الشخص"], jobTitle: ["المسمى الوظفي", "المسمى الوظيفي", "الوظيفة"], email: ["البريد الإلكتروني", "البريد الالكتروني", "الايميل", "الإيميل"], phone: ["رقم التواصل", "الجوال", "رقم الجوال", "الهاتف"], x: ["X", "x", "الإحداثيات X", "احداثيات X"], y: ["Y", "y", "الإحداثيات Y", "احداثيات Y"], region: ["المنطقة"], district: ["الحي"], researcher: ["اسم الباحث", "الباحث"], }; const EDITABLE_KEYS = ["name", "jobTitle", "email", "phone"]; const REQUIRED_HEADERS = ["commercial", "facility", "name", "jobTitle", "email", "phone", "x", "y", "region", "district", "researcher"]; const DISPLAY_COLUMNS = [ ["commercial", "السجل التجاري"], ["facility", "اسم المنشأة"], ["accountManager", "مدير الحساب"], ["name", "الاسم"], ["jobTitle", "المسمى الوظيفي"], ["email", "البريد الإلكتروني"], ["phone", "رقم التواصل"], ["region", "المنطقة"], ["district", "الحي"], ["researcher", "اسم الباحث"], ["map", "الموقع"], ["status", "الحالة"], ["actions", "الإجراءات"], ]; const LABELS = { commercial: "السجل التجاري", facility: "اسم المنشأة", accountManager: "مدير الحساب", name: "الاسم", jobTitle: "المسمى الوظيفي", email: "البريد الإلكتروني", phone: "رقم التواصل", region: "المنطقة", district: "الحي", researcher: "اسم الباحث", }; const els = { loginView: document.getElementById("loginView"), appView: document.getElementById("appView"), loginForm: document.getElementById("loginForm"), researcherSelect: document.getElementById("researcherSelect"), password: document.getElementById("password"), loginError: document.getElementById("loginError"), togglePassword: document.getElementById("togglePassword"), roleLabel: document.getElementById("roleLabel"), welcomeLabel: document.getElementById("welcomeLabel"), heroTitle: document.getElementById("heroTitle"), heroDescription: document.getElementById("heroDescription"), totalCount: document.getElementById("totalCount"), enteredCount: document.getElementById("enteredCount"), missingCount: document.getElementById("missingCount"), progressCount: document.getElementById("progressCount"), progressLabel: document.getElementById("progressLabel"), progressBar: document.getElementById("progressBar"), adminStats: document.getElementById("adminStats"), researcherStatsGrid: document.getElementById("researcherStatsGrid"), bestResearcher: document.getElementById("bestResearcher"), leastResearcher: document.getElementById("leastResearcher"), exportSummaryButton: document.getElementById("exportSummaryButton"), exportFullButton: document.getElementById("exportFullButton"), searchInput: document.getElementById("searchInput"), researcherFilterGroup: document.getElementById("researcherFilterGroup"), researcherFilter: document.getElementById("researcherFilter"), regionFilter: document.getElementById("regionFilter"), districtFilter: document.getElementById("districtFilter"), statusFilter: document.getElementById("statusFilter"), saveAllButton: document.getElementById("saveAllButton"), resultsMeta: document.getElementById("resultsMeta"), tableHead: document.getElementById("tableHead"), tableBody: document.getElementById("tableBody"), cardsGrid: document.getElementById("cardsGrid"), emptyState: document.getElementById("emptyState"), refreshButton: document.getElementById("refreshButton"), logoutButton: document.getElementById("logoutButton"), toast: document.getElementById("toast"), }; const state = { headers: [], headerMap: {}, rows: [], visibleRows: [], currentUser: "", isManager: false, dirtyRows: new Map(), autosaveTimers: new Map(), toastTimer: null, }; 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.map((value) => String(value ?? "").trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b, "ar", { sensitivity: "base" }), ); } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[char]); } function showToast(message) { clearTimeout(state.toastTimer); els.toast.textContent = message; els.toast.classList.add("show"); state.toastTimer = setTimeout(() => els.toast.classList.remove("show"), 2600); } function findHeader(headers, aliases) { const normalized = new Map(headers.map((header) => [normalize(header), header])); for (const alias of aliases) { const match = normalized.get(normalize(alias)); if (match) return match; } return ""; } function buildHeaderMap(headers) { return Object.fromEntries(Object.entries(FIELD_ALIASES).map(([key, aliases]) => [key, findHeader(headers, aliases)])); } function valueOf(row, key) { const header = state.headerMap[key]; return header ? row.raw[header] ?? "" : ""; } function effectiveValue(row, key) { const dirty = state.dirtyRows.get(Number(row.rowNumber)); return dirty && key in dirty ? dirty[key] : valueOf(row, key); } function setValue(row, key, value) { const header = state.headerMap[key]; if (header) row.raw[header] = value; } function isEntered(row) { return EDITABLE_KEYS.every((key) => String(effectiveValue(row, key)).trim()); } function rowByNumber(rowNumber) { return state.rows.find((row) => Number(row.rowNumber) === Number(rowNumber)); } function rowsFromApiPayload(payload) { const headers = Array.isArray(payload.headers) ? payload.headers.map((header) => String(header ?? "").trim()) : []; const rows = Array.isArray(payload.rows) ? payload.rows : []; return { headers, rows: rows.map((row, index) => ({ rowNumber: Number(row.rowNumber || row._rowNumber || index + 2), raw: { ...row }, })), }; } async function apiRequest(payload) { const response = await fetch(CONFIG.apiUrl, { method: "POST", headers: { "Content-Type": "text/plain;charset=utf-8" }, body: JSON.stringify(payload), cache: "no-store", }); const text = await response.text(); let data; try { data = JSON.parse(text); } catch { throw new Error("استجابة Apps Script ليست JSON. تأكد من نشر doPost و doGet."); } if (!response.ok || data.ok === false || data.success === false) { throw new Error(data.message || "تعذر تنفيذ الطلب عبر Apps Script."); } return data; } async function loadData() { const data = await apiRequest({ action: "read" }); const parsed = rowsFromApiPayload(data); if (!parsed.headers.length) throw new Error("لم يتم العثور على صف العناوين في Google Sheet."); return parsed; } function validateHeaders() { const missing = REQUIRED_HEADERS.filter((key) => !state.headerMap[key]); if (missing.length) { const labels = missing.map((key) => FIELD_ALIASES[key][0]).join("، "); throw new Error(`أعمدة مطلوبة غير موجودة في Google Sheet: ${labels}`); } } function applyLoadedData(data) { state.headers = data.headers; state.headerMap = buildHeaderMap(state.headers); validateHeaders(); state.rows = data.rows; state.autosaveTimers.forEach((timer) => clearTimeout(timer)); state.autosaveTimers.clear(); state.dirtyRows.clear(); updateSaveAllState(); populateSelects(); } function fillSelect(select, placeholder, values) { select.replaceChildren(new Option(placeholder, "")); values.forEach((value) => select.add(new Option(value, value))); } function populateSelects() { const researchers = uniqueSorted(state.rows.map((row) => valueOf(row, "researcher"))); els.researcherSelect.replaceChildren(new Option("اختر اسم الباحث", "")); researchers.forEach((name) => els.researcherSelect.add(new Option(name, name))); els.researcherSelect.add(new Option("دخول المدير", CONFIG.managerValue)); els.researcherSelect.disabled = false; fillSelect(els.researcherFilter, "كل الباحثين", researchers); fillSelect(els.regionFilter, "كل المناطق", uniqueSorted(state.rows.map((row) => valueOf(row, "region")))); fillSelect(els.districtFilter, "كل الأحياء", uniqueSorted(state.rows.map((row) => valueOf(row, "district")))); } function scopedRows() { if (state.isManager) return state.rows; return state.rows.filter((row) => valueOf(row, "researcher") === state.currentUser); } function applyFilters() { const query = normalize(els.searchInput.value); const researcher = els.researcherFilter.value; const region = els.regionFilter.value; const district = els.districtFilter.value; const status = els.statusFilter.value; state.visibleRows = scopedRows() .filter((row) => { const searchText = normalize([ valueOf(row, "commercial"), valueOf(row, "facility"), valueOf(row, "accountManager"), valueOf(row, "researcher"), valueOf(row, "region"), valueOf(row, "district"), valueOf(row, "name"), valueOf(row, "phone"), ].join(" ")); const entered = isEntered(row); return ( (!query || searchText.includes(query)) && (!researcher || valueOf(row, "researcher") === researcher) && (!region || valueOf(row, "region") === region) && (!district || valueOf(row, "district") === district) && (!status || (status === "entered" ? entered : !entered)) ); }) .sort((a, b) => Number(isEntered(a)) - Number(isEntered(b)) || String(valueOf(a, "facility")).localeCompare(String(valueOf(b, "facility")), "ar")); render(); } function computeStats(rows) { const total = rows.length; const entered = rows.filter(isEntered).length; const missing = total - entered; const progress = total ? Math.round((entered / total) * 100) : 0; return { total, entered, missing, progress }; } function researcherStats() { return uniqueSorted(state.rows.map((row) => valueOf(row, "researcher"))).map((name) => { const rows = state.rows.filter((row) => valueOf(row, "researcher") === name); return { name, ...computeStats(rows) }; }); } function renderSummary() { const stats = computeStats(scopedRows()); els.totalCount.textContent = stats.total; els.enteredCount.textContent = stats.entered; els.missingCount.textContent = stats.missing; els.progressCount.textContent = `${stats.progress}%`; els.progressLabel.textContent = `نسبة الإنجاز ${stats.progress}%`; els.progressBar.style.width = `${stats.progress}%`; els.resultsMeta.textContent = `${state.visibleRows.length} من أصل ${stats.total} منشأة`; } function renderAdminStats() { if (!state.isManager) return; const stats = researcherStats(); els.researcherStatsGrid.replaceChildren(...stats.map((item) => { const card = document.createElement("article"); card.className = "researcher-card"; card.innerHTML = `

${escapeHtml(item.name)}

الإجمالي: ${item.total} | المكتملة: ${item.entered} | غير المكتملة: ${item.missing}
${item.progress}% إنجاز `; return card; })); const sorted = [...stats].sort((a, b) => b.progress - a.progress || b.entered - a.entered || a.name.localeCompare(b.name, "ar")); els.bestResearcher.textContent = sorted[0] ? `${sorted[0].name} (${sorted[0].progress}%)` : "-"; els.leastResearcher.textContent = sorted.at(-1) ? `${sorted.at(-1).name} (${sorted.at(-1).progress}%)` : "-"; } function renderTableHead() { els.tableHead.replaceChildren(...DISPLAY_COLUMNS.map(([, label]) => { const th = document.createElement("th"); th.textContent = label; return th; })); } function createInput(row, key) { const input = document.createElement("input"); input.className = "cell-input"; input.value = valueOf(row, key); input.dataset.row = row.rowNumber; input.dataset.key = key; input.placeholder = LABELS[key]; if (key === "email") input.type = "email"; if (key === "phone") { input.inputMode = "numeric"; input.maxLength = 10; input.pattern = "05[0-9]{8}"; } input.addEventListener("input", () => handleFieldInput(row, key, input.value)); return input; } function mapUrl(row) { const x = String(valueOf(row, "x")).trim(); const y = String(valueOf(row, "y")).trim(); if (!x || !y) return ""; return `https://www.google.com/maps?q=${encodeURIComponent(`${y},${x}`)}`; } function phoneDigits(value) { const digits = String(value ?? "").replace(/\D/g, ""); if (!digits) return ""; if (digits.startsWith("966")) return digits; if (digits.startsWith("05") && digits.length === 10) return `966${digits.slice(1)}`; if (digits.startsWith("5") && digits.length === 9) return `966${digits}`; return digits; } function handleFieldInput(row, key, value) { const rowNumber = Number(row.rowNumber); const current = state.dirtyRows.get(rowNumber) || Object.fromEntries(EDITABLE_KEYS.map((item) => [item, valueOf(row, item)])); current[key] = value; state.dirtyRows.set(rowNumber, current); syncInputs(rowNumber, key, value); markDirty(rowNumber, true); updateSaveAllState(); updateRowShellStatus(row); scheduleAutosave(row); } function syncInputs(rowNumber, key, value) { document.querySelectorAll(`[data-row="${rowNumber}"][data-key="${key}"]`).forEach((input) => { if (input.value !== value) input.value = value; }); } function markDirty(rowNumber, isDirty) { document.querySelectorAll(`[data-row-shell="${rowNumber}"]`).forEach((node) => node.classList.toggle("is-dirty", isDirty)); } function markSaving(rowNumber, isSaving) { document.querySelectorAll(`[data-row-shell="${rowNumber}"]`).forEach((node) => node.classList.toggle("is-saving", isSaving)); } function updateRowShellStatus(row) { const complete = isEntered(row); document.querySelectorAll(`[data-row-shell="${row.rowNumber}"]`).forEach((node) => { node.classList.toggle("complete", complete); node.classList.toggle("incomplete", !complete); const badge = node.querySelector(".status-badge"); if (badge) { badge.className = `status-badge ${complete ? "status-entered" : "status-missing"}`; badge.textContent = complete ? "مكتملة" : "غير مكتملة"; } }); } function updateSaveAllState() { const count = state.dirtyRows.size; els.saveAllButton.disabled = count === 0; els.saveAllButton.textContent = count ? `حفظ الكل (${count})` : "حفظ الكل"; } function scheduleAutosave(row) { const rowNumber = Number(row.rowNumber); clearTimeout(state.autosaveTimers.get(rowNumber)); const timer = setTimeout(() => { if (!state.dirtyRows.has(rowNumber)) return; saveRow(row, null, { silent: true, autosave: true }); }, 1200); state.autosaveTimers.set(rowNumber, timer); } function renderTable() { els.tableBody.replaceChildren(...state.visibleRows.map((row) => { const tr = document.createElement("tr"); tr.dataset.rowShell = row.rowNumber; tr.classList.toggle("is-dirty", state.dirtyRows.has(row.rowNumber)); DISPLAY_COLUMNS.forEach(([key]) => { const td = document.createElement("td"); if (EDITABLE_KEYS.includes(key)) { td.append(createInput(row, key)); } else if (key === "map") { td.append(createMapLink(row)); } else if (key === "status") { td.append(createStatusBadge(row)); } else if (key === "actions") { const button = document.createElement("button"); button.type = "button"; button.className = "secondary-button save-button"; button.textContent = "حفظ"; button.addEventListener("click", () => saveRow(row, button)); td.append(button); } else { td.textContent = valueOf(row, key) || "-"; if (key === "commercial") td.className = "ltr-value"; } tr.append(td); }); return tr; })); } function createStatusBadge(row) { const badge = document.createElement("span"); badge.className = `status-badge ${isEntered(row) ? "status-entered" : "status-missing"}`; badge.textContent = isEntered(row) ? "مكتملة" : "غير مكتملة"; return badge; } function createMapLink(row) { const href = mapUrl(row); if (!href) return document.createTextNode("-"); const link = document.createElement("a"); link.className = "map-link"; link.href = href; link.target = "_blank"; link.rel = "noopener"; link.textContent = "الموقع"; return link; } function renderCards() { els.cardsGrid.replaceChildren(...state.visibleRows.map((row) => { const card = document.createElement("article"); const entered = isEntered(row); card.className = `facility-card ${entered ? "complete" : "incomplete"}`; card.dataset.rowShell = row.rowNumber; card.classList.toggle("is-dirty", state.dirtyRows.has(row.rowNumber)); const phone = valueOf(row, "phone"); const phoneNumber = phoneDigits(phone); const actions = document.createElement("div"); actions.className = "card-actions"; actions.append(createMapLink(row)); if (phoneNumber) { const call = document.createElement("a"); call.className = "secondary-button card-action-button"; call.href = `tel:${phoneNumber}`; call.textContent = "اتصال"; const whatsapp = document.createElement("a"); whatsapp.className = "secondary-button card-action-button"; whatsapp.href = `https://wa.me/${phoneNumber}`; whatsapp.target = "_blank"; whatsapp.rel = "noopener"; whatsapp.textContent = "واتساب"; actions.append(call, whatsapp); } const fields = document.createElement("div"); fields.className = "card-fields"; EDITABLE_KEYS.forEach((key) => { const group = document.createElement("label"); group.textContent = LABELS[key]; group.append(createInput(row, key)); fields.append(group); }); const save = document.createElement("button"); save.type = "button"; save.className = "primary-button card-save-button"; save.textContent = "حفظ"; save.addEventListener("click", () => saveRow(row, save)); card.innerHTML = `
السجل التجاري: ${escapeHtml(valueOf(row, "commercial") || "-")}

${escapeHtml(valueOf(row, "facility") || "منشأة دون اسم")}

مدير الحساب${escapeHtml(valueOf(row, "accountManager") || "-")}
المنطقة${escapeHtml(valueOf(row, "region") || "-")}
الحي${escapeHtml(valueOf(row, "district") || "-")}
الباحث${escapeHtml(valueOf(row, "researcher") || "-")}
`; card.querySelector(".facility-card-head").append(createStatusBadge(row)); card.append(actions, fields, save); return card; })); } function render() { renderSummary(); renderAdminStats(); renderTableHead(); renderTable(); renderCards(); els.emptyState.hidden = state.visibleRows.length !== 0; } function valuesForRow(row) { return state.dirtyRows.get(Number(row.rowNumber)) || Object.fromEntries(EDITABLE_KEYS.map((key) => [key, valueOf(row, key)])); } function validateSaveValues(values) { if (EDITABLE_KEYS.every((key) => !String(values[key] || "").trim())) { return "أدخل بيانات التواصل قبل الحفظ."; } const phone = String(values.phone || "").trim(); if (phone && !/^05\d{8}$/.test(phone)) { return "رقم الجوال يجب أن يكون 10 أرقام ويبدأ بـ 05."; } return ""; } async function saveRow(row, button, options = {}) { const values = valuesForRow(row); const validation = validateSaveValues(values); if (validation) { if (!options.silent) showToast(validation); return; } clearTimeout(state.autosaveTimers.get(Number(row.rowNumber))); const updates = Object.fromEntries(EDITABLE_KEYS.map((key) => [state.headerMap[key], values[key] || ""])); let originalText = ""; if (button) { button.disabled = true; originalText = button.textContent; button.textContent = "جاري الحفظ..."; } markSaving(row.rowNumber, true); try { await apiRequest({ action: "update", rowNumber: row.rowNumber, commercialRecord: valueOf(row, "commercial"), facilityName: valueOf(row, "facility"), researcher: valueOf(row, "researcher"), updates, }); EDITABLE_KEYS.forEach((key) => setValue(row, key, values[key] || "")); state.dirtyRows.delete(Number(row.rowNumber)); markDirty(row.rowNumber, false); updateRowShellStatus(row); updateSaveAllState(); showToast(options.autosave ? "تم الحفظ تلقائيًا" : "تم الحفظ بنجاح"); applyFilters(); } catch (error) { showToast(error.message || "تعذر الحفظ"); } finally { markSaving(row.rowNumber, false); if (button) { button.disabled = false; button.textContent = originalText; } } } async function saveAllDirtyRows() { const dirtyNumbers = [...state.dirtyRows.keys()]; if (!dirtyNumbers.length) return; els.saveAllButton.disabled = true; els.saveAllButton.textContent = "جاري حفظ الكل..."; try { for (const rowNumber of dirtyNumbers) { const row = rowByNumber(rowNumber); if (!row) continue; const values = valuesForRow(row); const validation = validateSaveValues(values); if (validation) throw new Error(`الصف ${rowNumber}: ${validation}`); const updates = Object.fromEntries(EDITABLE_KEYS.map((key) => [state.headerMap[key], values[key] || ""])); clearTimeout(state.autosaveTimers.get(rowNumber)); await apiRequest({ action: "update", rowNumber, updates }); EDITABLE_KEYS.forEach((key) => setValue(row, key, values[key] || "")); state.dirtyRows.delete(rowNumber); markDirty(rowNumber, false); } showToast("تم حفظ جميع التعديلات بنجاح"); applyFilters(); } catch (error) { showToast(error.message || "تعذر حفظ جميع التعديلات"); } finally { updateSaveAllState(); } } function aggregateCount(rows, key) { const map = new Map(); rows.forEach((row) => { const value = valueOf(row, key) || "غير محدد"; map.set(value, (map.get(value) || 0) + 1); }); return [...map.entries()].map(([label, count]) => [label, count]); } function ensureExcelLibrary() { if (!window.XLSX) { showToast("تعذر تحميل مكتبة Excel. تحقق من اتصال الإنترنت ثم حاول مرة أخرى."); return false; } return true; } function makeWorksheet(rows, columnWidths = []) { const sheet = XLSX.utils.aoa_to_sheet(rows); sheet["!cols"] = columnWidths.map((width) => ({ wch: width })); sheet["!autofilter"] = { ref: XLSX.utils.encode_range({ s: { r: 0, c: 0 }, e: { r: Math.max(rows.length - 1, 0), c: Math.max((rows[0] || []).length - 1, 0) } }) }; return sheet; } function appendSheet(workbook, name, rows, widths) { const sheet = makeWorksheet(rows, widths); XLSX.utils.book_append_sheet(workbook, sheet, name.slice(0, 31)); } function writeWorkbook(filename, workbook) { workbook.Workbook = { Views: [{ RTL: true }] }; XLSX.writeFile(workbook, filename, { bookType: "xlsx", compression: true }); } function fullDataRows() { const exportHeaders = [ "السجل التجاري", "اسم المنشأة", "مدير الحساب", "الاسم", "المسمى الوظيفي", "البريد الإلكتروني", "رقم التواصل", "X", "Y", "المنطقة", "الحي", "اسم الباحث", "حالة الإدخال", ]; const rows = state.rows.map((row) => [ valueOf(row, "commercial"), valueOf(row, "facility"), valueOf(row, "accountManager"), valueOf(row, "name"), valueOf(row, "jobTitle"), valueOf(row, "email"), valueOf(row, "phone"), valueOf(row, "x"), valueOf(row, "y"), valueOf(row, "region"), valueOf(row, "district"), valueOf(row, "researcher"), isEntered(row) ? "مكتملة" : "غير مكتملة", ]); return [exportHeaders, ...rows]; } function exportSummary() { if (!ensureExcelLibrary()) return; const total = computeStats(state.rows); const workbook = XLSX.utils.book_new(); appendSheet(workbook, "الملخص", [ ["تقرير إحصائي لمشروع رصد بيانات المنشآت"], ["تاريخ التصدير", new Date().toLocaleString("ar-SA")], [], ["إجمالي العينات", "المكتملة", "غير المكتملة", "نسبة الإنجاز"], [total.total, total.entered, total.missing, `${total.progress}%`], ], [26, 16, 16, 16]); appendSheet(workbook, "حسب الباحث", [ ["الباحث", "عدد العينات", "مكتملة", "غير مكتملة", "نسبة الإنجاز"], ...researcherStats().map((item) => [item.name, item.total, item.entered, item.missing, `${item.progress}%`]), ], [28, 16, 16, 16, 16]); appendSheet(workbook, "حسب المنطقة", [ ["المنطقة", "عدد العينات"], ...aggregateCount(state.rows, "region"), ], [30, 16]); appendSheet(workbook, "حسب الحي", [ ["الحي", "عدد العينات"], ...aggregateCount(state.rows, "district"), ], [34, 16]); writeWorkbook("تقرير-إحصائي-رصد-المنشآت.xlsx", workbook); } function exportFull() { if (!ensureExcelLibrary()) return; const workbook = XLSX.utils.book_new(); appendSheet(workbook, "جميع البيانات", fullDataRows(), [15, 46, 22, 20, 22, 28, 16, 16, 16, 22, 28, 22, 16]); appendSheet(workbook, "إحصائيات الباحثين", [ ["الباحث", "عدد العينات", "مكتملة", "غير مكتملة", "نسبة الإنجاز"], ...researcherStats().map((item) => [item.name, item.total, item.entered, item.missing, `${item.progress}%`]), ], [28, 16, 16, 16, 16]); writeWorkbook("تقرير-تفصيلي-كامل-رصد-المنشآت.xlsx", workbook); } async function bootstrap() { els.researcherSelect.disabled = true; els.loginError.textContent = ""; try { applyLoadedData(await loadData()); } catch (error) { els.researcherSelect.replaceChildren(new Option("تعذر تحميل الباحثين", "")); els.loginError.textContent = error.message || "تعذر الاتصال بـ Apps Script."; } } function hasUnsavedChanges() { return state.dirtyRows.size > 0; } function confirmIfDirty() { return !hasUnsavedChanges() || window.confirm("توجد بيانات معدلة لم يتم حفظها. هل تريد المتابعة بدون حفظ؟"); } function login() { const selected = els.researcherSelect.value; if (!selected) { els.loginError.textContent = "اختر اسم الباحث أو دخول المدير."; return; } const expectedPassword = selected === CONFIG.managerValue ? CONFIG.managerPassword : CONFIG.researcherPassword; if (els.password.value !== expectedPassword) { els.loginError.textContent = "الرقم السري غير صحيح."; els.password.value = ""; els.password.focus(); return; } state.isManager = selected === CONFIG.managerValue; state.currentUser = state.isManager ? "المدير" : selected; els.roleLabel.textContent = state.isManager ? "لوحة المدير" : "لوحة الباحث"; els.welcomeLabel.textContent = state.isManager ? "دخول المدير" : `مرحبًا ${state.currentUser}`; els.heroTitle.textContent = state.isManager ? "لوحة شاملة لجميع الباحثين والبيانات" : "العينات المسندة لك فقط"; els.heroDescription.textContent = state.isManager ? "يمكنك مشاهدة جميع المنشآت والإحصائيات والتصدير والتعديل." : "يمكنك فتح البطاقة، تعبئة بيانات التواصل، فتح الموقع، ثم الحفظ مباشرة."; els.adminStats.hidden = !state.isManager; els.researcherFilterGroup.hidden = !state.isManager; els.exportSummaryButton.hidden = !state.isManager; els.exportFullButton.hidden = !state.isManager; els.researcherFilter.value = ""; els.loginView.hidden = true; els.appView.hidden = false; applyFilters(); } function logout() { if (!confirmIfDirty()) return; state.currentUser = ""; state.isManager = false; state.autosaveTimers.forEach((timer) => clearTimeout(timer)); state.autosaveTimers.clear(); state.dirtyRows.clear(); updateSaveAllState(); els.password.value = ""; els.searchInput.value = ""; els.researcherFilter.value = ""; els.regionFilter.value = ""; els.districtFilter.value = ""; els.statusFilter.value = ""; els.appView.hidden = true; els.loginView.hidden = false; } els.loginForm.addEventListener("submit", (event) => { event.preventDefault(); login(); }); els.togglePassword.addEventListener("click", () => { const showing = els.password.type === "text"; els.password.type = showing ? "password" : "text"; els.togglePassword.setAttribute("aria-label", showing ? "إظهار الرقم السري" : "إخفاء الرقم السري"); }); [els.searchInput, els.researcherFilter, els.regionFilter, els.districtFilter, els.statusFilter].forEach((control) => { control.addEventListener("input", applyFilters); control.addEventListener("change", applyFilters); }); document.querySelectorAll(".quick-filter").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll(".quick-filter").forEach((item) => item.classList.remove("active")); button.classList.add("active"); els.statusFilter.value = button.dataset.status || ""; applyFilters(); }); }); els.exportSummaryButton.addEventListener("click", exportSummary); els.exportFullButton.addEventListener("click", exportFull); els.saveAllButton.addEventListener("click", saveAllDirtyRows); els.logoutButton.addEventListener("click", logout); els.refreshButton.addEventListener("click", async () => { if (!confirmIfDirty()) return; els.refreshButton.disabled = true; try { applyLoadedData(await loadData()); applyFilters(); showToast("تم تحديث البيانات من Google Sheet"); } catch (error) { showToast(error.message || "تعذر تحديث البيانات"); } finally { els.refreshButton.disabled = false; } }); window.addEventListener("beforeunload", (event) => { if (!hasUnsavedChanges()) return; event.preventDefault(); event.returnValue = ""; }); bootstrap();