"use strict"; const FIELD_LABELS = [ "اسم المشرف", "اسم الباحث", "رقم السجل(العينة)", "اسم العينة", "حالة الاكتمال", "حالة الاقرار", "حالة الاستيفاء", "نوع الاستيفاء", "حالة الاعتماد", "النشاط", "الايميل", "الموقع", "رقم التواصل", ]; const FIELD_KEYS = [ "supervisor", "researcher", "recordNumber", "sampleName", "completionStatus", "declarationStatus", "fulfillmentStatus", "fulfillmentType", "approvalStatus", "activity", "email", "location", "phone", ]; const DISPLAY_FIELDS = [ ["recordNumber", "رقم السجل", false], ["completionStatus", "حالة الاكتمال", false], ["declarationStatus", "حالة الإقرار", false], ["fulfillmentType", "نوع الاستيفاء", false], ["approvalStatus", "حالة الاعتماد", false], ["activity", "النشاط", true], ["email", "البريد الإلكتروني", true], ["location", "الموقع", true], ["phone", "رقم التواصل", true], ]; const loginView = document.getElementById("loginView"); const dashboardView = document.getElementById("dashboardView"); const loginForm = document.getElementById("loginForm"); const passwordInput = document.getElementById("password"); const loginButton = document.getElementById("loginButton"); const loginError = document.getElementById("loginError"); const togglePassword = document.getElementById("togglePassword"); const logoutButton = document.getElementById("logoutButton"); const supervisorSelect = document.getElementById("supervisorSelect"); const researcherSelect = document.getElementById("researcherSelect"); const statusSelect = document.getElementById("statusSelect"); const searchInput = document.getElementById("searchInput"); const emptyState = document.getElementById("emptyState"); const resultsSection = document.getElementById("resultsSection"); const resultsMeta = document.getElementById("resultsMeta"); const resultsTitle = document.getElementById("resultsTitle"); const statusChips = document.getElementById("statusChips"); const samplesGrid = document.getElementById("samplesGrid"); const noResults = document.getElementById("noResults"); const copyAllButton = document.getElementById("copyAllButton"); const loadMoreWrap = document.getElementById("loadMoreWrap"); const loadMoreButton = document.getElementById("loadMoreButton"); const loadMoreMeta = document.getElementById("loadMoreMeta"); const clearFiltersButton = document.getElementById("clearFiltersButton"); const toast = document.getElementById("toast"); let allRows = []; let visibleRows = []; let failedAttempts = 0; let toastTimer; let inactivityTimer; let inactivityWarningTimer; let renderedCount = 0; const ALL_SUPERVISORS_VALUE = "__all_supervisors__"; const PAGE_SIZE = 24; const INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000; const INACTIVITY_WARNING_MS = 29 * 60 * 1000; const ACTIVITY_EVENTS = ["click", "keydown", "pointerdown", "touchstart", "scroll"]; function base64ToBytes(value) { const binary = atob(value); return Uint8Array.from(binary, (char) => char.charCodeAt(0)); } async function deriveKey(password, salt) { const material = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"], ); return crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: ENCRYPTED_DATA.iterations, hash: "SHA-256", }, material, { name: "AES-GCM", length: 256 }, false, ["decrypt"], ); } async function decryptData(password) { const salt = base64ToBytes(ENCRYPTED_DATA.salt); const iv = base64ToBytes(ENCRYPTED_DATA.iv); const cipherText = base64ToBytes(ENCRYPTED_DATA.payload); const key = await deriveKey(password, salt); const plainBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipherText); return JSON.parse(new TextDecoder().decode(plainBuffer)); } function normalize(value) { return String(value ?? "") .normalize("NFKD") .replace(/[\u064B-\u065F\u0670]/g, "") .replace(/[أإآ]/g, "ا") .replace(/ى/g, "ي") .replace(/ة/g, "ه") .replace(/\s+/g, " ") .trim() .toLowerCase(); } function formatPhone(value) { const raw = String(value ?? "").trim(); if (!raw) return ""; const digits = raw.replace(/\D/g, ""); if (!digits) return raw; if (digits.startsWith("0")) return digits; if (digits.startsWith("966")) return `0${digits.slice(3)}`; return `0${digits}`; } function rowToObject(row) { const item = Object.fromEntries(FIELD_KEYS.map((key, index) => [key, row[index] ?? ""])); item.phone = formatPhone(item.phone); return item; } function uniqueSorted(values) { return [...new Set(values.filter(Boolean))].sort((a, b) => String(a).localeCompare(String(b), "ar", { sensitivity: "base" }), ); } function setOptions(select, values, placeholder, counts = null) { select.replaceChildren(new Option(placeholder, "")); values.forEach((value) => { const label = counts ? `${value} (${counts.get(value) || 0})` : value; select.add(new Option(label, value)); }); } function showToast(message) { clearTimeout(toastTimer); toast.textContent = message; toast.classList.add("show"); toastTimer = setTimeout(() => toast.classList.remove("show"), 2300); } async function copyText(text, successMessage) { try { await navigator.clipboard.writeText(text); } catch { const area = document.createElement("textarea"); area.value = text; area.style.position = "fixed"; area.style.opacity = "0"; document.body.append(area); area.select(); document.execCommand("copy"); area.remove(); } showToast(successMessage); } function buildMessage(row, index = null) { const title = index === null ? "بيانات العينة للتواصل" : `العينة رقم (${index})`; const sampleLines = [ title, "────────────────────", row.sampleName && `اسم العينة: ${row.sampleName}`, row.recordNumber && `رقم السجل: ${row.recordNumber}`, row.fulfillmentStatus && `حالة الاستيفاء: ${row.fulfillmentStatus}`, row.activity && `النشاط: ${row.activity}`, ].filter(Boolean); const contactLines = [ "بيانات التواصل", "────────────────────", row.phone && `رقم التواصل: ${formatPhone(row.phone)}`, row.email && `البريد الإلكتروني: ${row.email}`, row.location && `الموقع: ${row.location}`, ].filter(Boolean); const sections = []; if (index === null) { sections.push( "السلام عليكم ورحمة الله وبركاته،", "نأمل التكرم بالتواصل مع العينة التالية واستكمال الإجراء اللازم:", ); } sections.push(sampleLines.join("\n")); if (contactLines.length > 2) sections.push(contactLines.join("\n")); if (index === null) sections.push("شاكرين لكم تعاونكم."); return sections.join("\n\n"); } function createDetail(key, label, isContact, value) { const wrapper = document.createElement("div"); wrapper.className = `detail-item${isContact ? " contact-detail" : ""}`; const labelElement = document.createElement("span"); labelElement.className = "detail-label"; labelElement.textContent = label; const valueRow = document.createElement("div"); valueRow.className = "detail-value-row"; const valueElement = document.createElement("span"); valueElement.className = "detail-value"; valueElement.textContent = String(value); if (key === "phone" || key === "recordNumber") { valueElement.dir = "ltr"; valueElement.style.textAlign = "right"; } valueRow.append(valueElement); if (key === "phone" || key === "email") { const copyFieldButton = document.createElement("button"); copyFieldButton.type = "button"; copyFieldButton.className = "copy-field-button"; copyFieldButton.setAttribute("aria-label", key === "phone" ? "نسخ رقم التواصل" : "نسخ البريد الإلكتروني"); copyFieldButton.title = key === "phone" ? "نسخ رقم التواصل" : "نسخ البريد الإلكتروني"; copyFieldButton.innerHTML = ` `; copyFieldButton.addEventListener("click", () => copyText(String(value), key === "phone" ? "تم نسخ رقم التواصل" : "تم نسخ البريد الإلكتروني"), ); valueRow.append(copyFieldButton); } wrapper.append(labelElement, valueRow); return wrapper; } function createSampleCard(row, index) { const card = document.createElement("article"); card.className = "sample-card"; const header = document.createElement("div"); header.className = "sample-card-header"; const headingWrap = document.createElement("div"); const number = document.createElement("span"); number.className = "sample-index"; number.textContent = `العينة ${index + 1}`; const title = document.createElement("h3"); title.textContent = row.sampleName || "عينة دون اسم"; headingWrap.append(number, title); const badge = document.createElement("span"); badge.className = "status-badge"; badge.textContent = row.fulfillmentStatus || "غير محدد"; header.append(headingWrap, badge); const details = document.createElement("div"); details.className = "sample-details"; DISPLAY_FIELDS.forEach(([key, label, isContact]) => { if (String(row[key] ?? "").trim()) { details.append(createDetail(key, label, isContact, row[key])); } }); const footer = document.createElement("div"); footer.className = "sample-card-footer"; const copyButton = document.createElement("button"); copyButton.className = "copy-sample"; copyButton.type = "button"; copyButton.textContent = "نسخ رسالة هذه العينة"; copyButton.addEventListener("click", () => copyText(buildMessage(row), "تم نسخ رسالة العينة"), ); footer.append(copyButton); card.append(header, details, footer); return card; } function renderVisibleRows(reset = false) { if (reset) { renderedCount = 0; samplesGrid.replaceChildren(); } const nextRows = visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE); const fragment = document.createDocumentFragment(); nextRows.forEach((row, offset) => { fragment.append(createSampleCard(row, renderedCount + offset)); }); samplesGrid.append(fragment); renderedCount += nextRows.length; const remaining = visibleRows.length - renderedCount; loadMoreWrap.hidden = remaining <= 0; loadMoreButton.textContent = `عرض المزيد (${Math.min(PAGE_SIZE, remaining)})`; loadMoreMeta.textContent = `تم عرض ${renderedCount} من أصل ${visibleRows.length} عينة`; } function renderStatusChips(rows) { const counts = new Map(); rows.forEach((row) => { const status = row.fulfillmentStatus || "غير محدد"; counts.set(status, (counts.get(status) || 0) + 1); }); const createChip = (label, value, count) => { const chip = document.createElement("button"); chip.type = "button"; chip.className = `status-chip${statusSelect.value === value ? " active" : ""}`; chip.setAttribute("aria-pressed", String(statusSelect.value === value)); chip.append(document.createTextNode(label)); const strong = document.createElement("strong"); strong.textContent = count; chip.append(strong); chip.addEventListener("click", () => { statusSelect.value = value; applyFilters(); }); return chip; }; const chips = [createChip("الكل", "", rows.length)]; [...counts.entries()].forEach(([status, count]) => { chips.push(createChip(status, status, count)); }); statusChips.replaceChildren(...chips); } function applyFilters() { const supervisor = supervisorSelect.value; const researcher = researcherSelect.value; const query = normalize(searchInput.value); if (!supervisor && !query) { resultsSection.hidden = true; emptyState.hidden = false; return; } const status = statusSelect.value; const supervisorRows = !supervisor || supervisor === ALL_SUPERVISORS_VALUE ? allRows : allRows.filter((row) => row.supervisor === supervisor); const scopedRows = researcher ? supervisorRows.filter((row) => row.researcher === researcher) : supervisorRows; visibleRows = scopedRows.filter((row) => { const matchesStatus = !status || row.fulfillmentStatus === status; const matchesSearch = !query || normalize(row.sampleName).includes(query) || normalize(row.recordNumber).includes(query); return matchesStatus && matchesSearch; }); emptyState.hidden = true; resultsSection.hidden = false; resultsTitle.textContent = researcher || (supervisor === ALL_SUPERVISORS_VALUE ? "جميع العينات" : supervisor ? `عينات المشرف: ${supervisor}` : "نتائج البحث"); resultsMeta.textContent = `${visibleRows.length} من أصل ${scopedRows.length} عينة`; copyAllButton.disabled = visibleRows.length === 0; copyAllButton.textContent = researcher ? "نسخ جميع عينات الباحث" : "نسخ جميع النتائج المعروضة"; renderStatusChips(scopedRows); renderVisibleRows(true); noResults.hidden = visibleRows.length !== 0; } function resetResearcherControls() { setOptions(researcherSelect, [], "جميع الباحثين"); setOptions(statusSelect, [], "جميع الحالات"); researcherSelect.disabled = true; statusSelect.disabled = true; resultsSection.hidden = true; emptyState.hidden = false; emptyState.querySelector("h3").textContent = "ابحث أو اختر المشرف"; emptyState.querySelector("p").textContent = "يمكنك البحث مباشرة باسم العينة أو رقم السجل، أو اختيار المشرف لعرض كامل عيناته."; } function initializeDashboard(data) { allRows = data.rows.map(rowToObject); const supervisorCounts = new Map(); allRows.forEach((row) => supervisorCounts.set(row.supervisor, (supervisorCounts.get(row.supervisor) || 0) + 1), ); setOptions( supervisorSelect, uniqueSorted(allRows.map((row) => row.supervisor)), "اختر المشرف", supervisorCounts, ); supervisorSelect.add( new Option(`إبراهيم - جميع العينات (${allRows.length})`, ALL_SUPERVISORS_VALUE), 1, ); searchInput.value = ""; searchInput.disabled = false; resetResearcherControls(); startInactivityTimer(); } function clearInactivityTimers() { clearTimeout(inactivityTimer); clearTimeout(inactivityWarningTimer); } function startInactivityTimer() { clearInactivityTimers(); if (dashboardView.hidden) return; inactivityWarningTimer = setTimeout(() => { showToast("سيتم تسجيل الخروج تلقائيًا بعد دقيقة لعدم وجود نشاط"); }, INACTIVITY_WARNING_MS); inactivityTimer = setTimeout(() => { performLogout("تم تسجيل الخروج تلقائيًا بعد 30 دقيقة من عدم النشاط"); }, INACTIVITY_TIMEOUT_MS); } function performLogout(message = "") { clearInactivityTimers(); allRows = []; visibleRows = []; renderedCount = 0; supervisorSelect.value = ""; searchInput.value = ""; resetResearcherControls(); dashboardView.hidden = true; loginView.hidden = false; passwordInput.focus(); if (message) showToast(message); } function clearFilters() { supervisorSelect.value = ""; searchInput.value = ""; resetResearcherControls(); window.scrollTo({ top: 0, behavior: "smooth" }); showToast("تم مسح جميع الفلاتر"); } function handleActivity() { if (!dashboardView.hidden) startInactivityTimer(); } loginForm.addEventListener("submit", async (event) => { event.preventDefault(); const password = passwordInput.value.trim(); if (!password) return; loginButton.disabled = true; loginButton.querySelector("span").textContent = "جارٍ التحقق..."; loginError.textContent = ""; if (failedAttempts >= 3) { await new Promise((resolve) => setTimeout(resolve, Math.min(failedAttempts * 800, 5000))); } try { const data = await decryptData(password); initializeDashboard(data); passwordInput.value = ""; loginView.hidden = true; dashboardView.hidden = false; failedAttempts = 0; } catch { failedAttempts += 1; loginError.textContent = "رمز الدخول غير صحيح. تحقق منه وحاول مرة أخرى."; passwordInput.select(); } finally { loginButton.disabled = false; loginButton.querySelector("span").textContent = "دخول آمن"; } }); togglePassword.addEventListener("click", () => { const showing = passwordInput.type === "text"; passwordInput.type = showing ? "password" : "text"; togglePassword.setAttribute("aria-label", showing ? "إظهار رمز الدخول" : "إخفاء رمز الدخول"); }); logoutButton.addEventListener("click", () => { performLogout(); }); supervisorSelect.addEventListener("change", () => { resetResearcherControls(); searchInput.value = ""; if (!supervisorSelect.value) { applyFilters(); return; } const researchers = uniqueSorted( allRows .filter( (row) => supervisorSelect.value === ALL_SUPERVISORS_VALUE || row.supervisor === supervisorSelect.value, ) .map((row) => row.researcher), ); const scopeRows = supervisorSelect.value === ALL_SUPERVISORS_VALUE ? allRows : allRows.filter((row) => row.supervisor === supervisorSelect.value); const researcherCounts = new Map(); scopeRows.forEach((row) => researcherCounts.set(row.researcher, (researcherCounts.get(row.researcher) || 0) + 1), ); setOptions(researcherSelect, researchers, "جميع الباحثين", researcherCounts); setOptions( statusSelect, uniqueSorted(scopeRows.map((row) => row.fulfillmentStatus)), "جميع الحالات", ); researcherSelect.disabled = false; statusSelect.disabled = false; applyFilters(); }); researcherSelect.addEventListener("change", () => { const rows = allRows.filter((row) => { const matchesSupervisor = supervisorSelect.value === ALL_SUPERVISORS_VALUE || row.supervisor === supervisorSelect.value; const matchesResearcher = !researcherSelect.value || row.researcher === researcherSelect.value; return matchesSupervisor && matchesResearcher; }); const statuses = uniqueSorted(rows.map((row) => row.fulfillmentStatus)); setOptions(statusSelect, statuses, "جميع الحالات"); statusSelect.disabled = false; applyFilters(); }); statusSelect.addEventListener("change", applyFilters); searchInput.addEventListener("input", applyFilters); loadMoreButton.addEventListener("click", () => renderVisibleRows()); clearFiltersButton.addEventListener("click", clearFilters); ACTIVITY_EVENTS.forEach((eventName) => document.addEventListener(eventName, handleActivity, { passive: true }), ); copyAllButton.addEventListener("click", () => { if (!visibleRows.length) return; const heading = researcherSelect.value ? `بيانات العينات الخاصة بالباحث: ${researcherSelect.value}` : `بيانات العينات المعروضة (${visibleRows.length} عينة)`; const messages = visibleRows.map((row, index) => buildMessage(row, index + 1)); copyText( [heading, "════════════════════", ...messages].join("\n\n"), "تم نسخ جميع العينات المعروضة", ); }); if (!window.crypto?.subtle || typeof ENCRYPTED_DATA === "undefined") { loginButton.disabled = true; loginError.textContent = "تعذر تشغيل التشفير في هذا المتصفح. استخدم متصفحًا حديثًا."; }