Download script.js from NMA2030/n: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/NMA2030/n/resolve/main/script.js
- Command line
-
hf download hf://spaces/NMA2030/n/script.js
-
curl -L -o script.js https://huggingface.co/spaces/NMA2030/n/resolve/main/script.js
14.6 kB
| /* | |
| ضع رابط Web App الناتج من Google Apps Script هنا فقط. | |
| مثال: | |
| const API_URL = "https://script.google.com/macros/s/XXXXX/exec"; | |
| */ | |
| const API_URL = "https://script.google.com/macros/s/AKfycbzP3FbtCu3u5PitxRyxm1f1fiIU7UWuoEvcco3oK_MlTLFhlA0D13RgTK_V8e9TXOI/exec"; | |
| const $ = (id) => document.getElementById(id); | |
| let allTasks = []; | |
| let selectedDates = []; | |
| function sortDates(list) { | |
| return [...new Set(list)].sort((a,b) => a.localeCompare(b)); | |
| } | |
| function renderSelectedDates() { | |
| const box = $("selectedDates"); | |
| const addBtn = $("addDateBtn"); | |
| if (addBtn) addBtn.textContent = selectedDates.length ? `+ إضافة تاريخ آخر (${selectedDates.length})` : "+ إضافة التاريخ"; | |
| if (!selectedDates.length) { | |
| box.innerHTML = `<span class="selected-date-label">لم تتم إضافة أي تواريخ بعد</span>`; | |
| return; | |
| } | |
| box.innerHTML = selectedDates.map(date => ` | |
| <span class="date-chip"> | |
| 📅 ${escapeHTML(formatArabicDate(date, true))} | |
| <button type="button" data-remove-date="${escapeHTML(date)}" aria-label="حذف التاريخ">×</button> | |
| </span> | |
| `).join(""); | |
| } | |
| function addCurrentDate() { | |
| const date = $("taskDate").value; | |
| if (!date) return toast("اختر التاريخ أولًا", true); | |
| if (selectedDates.includes(date)) return toast("هذا التاريخ مضاف بالفعل", true); | |
| selectedDates = sortDates([...selectedDates, date]); | |
| renderSelectedDates(); | |
| } | |
| function localISODate(d = new Date()) { | |
| const y = d.getFullYear(); | |
| const m = String(d.getMonth() + 1).padStart(2, "0"); | |
| const day = String(d.getDate()).padStart(2, "0"); | |
| return `${y}-${m}-${day}`; | |
| } | |
| function formatArabicDate(value, withWeekday = false) { | |
| if (!value) return "—"; | |
| const d = new Date(`${value}T12:00:00`); | |
| if (Number.isNaN(d.getTime())) return value; | |
| return new Intl.DateTimeFormat("ar-SA-u-ca-gregory", { | |
| weekday: withWeekday ? "long" : undefined, | |
| day: "numeric", | |
| month: "long", | |
| year: "numeric" | |
| }).format(d); | |
| } | |
| function dayDateLabel(value) { | |
| if (!value) return "—"; | |
| const d = new Date(`${value}T12:00:00`); | |
| if (Number.isNaN(d.getTime())) return value; | |
| return "📅 " + new Intl.DateTimeFormat("ar-SA-u-ca-gregory", { | |
| weekday:"long", day:"numeric", month:"long", year:"numeric" | |
| }).format(d); | |
| } | |
| function updateSelectedDateLabels() { | |
| const singleLabel = $("selectedDateLabel"); | |
| if (singleLabel && $("taskDate")) { | |
| singleLabel.textContent = dayDateLabel($("taskDate").value); | |
| } | |
| if ($("editDate") && $("editDateLabel")) { | |
| $("editDateLabel").textContent = dayDateLabel($("editDate").value); | |
| } | |
| } | |
| function escapeHTML(str = "") { | |
| return String(str).replace(/[&<>"']/g, c => ({ | |
| "&":"&","<":"<",">":">",'"':""","'":"'" | |
| }[c])); | |
| } | |
| function toast(message, isError = false) { | |
| const el = $("toast"); | |
| el.textContent = message; | |
| el.className = `toast show${isError ? " error" : ""}`; | |
| clearTimeout(toast.timer); | |
| toast.timer = setTimeout(() => el.className = "toast", 3200); | |
| } | |
| function apiReady() { | |
| return API_URL && !API_URL.includes("PUT_YOUR_"); | |
| } | |
| async function apiRequest(payload) { | |
| if (!apiReady()) throw new Error("لم يتم وضع رابط Google Apps Script داخل script.js"); | |
| const res = await fetch(API_URL, { | |
| method: "POST", | |
| headers: {"Content-Type":"text/plain;charset=utf-8"}, | |
| body: JSON.stringify({...payload, _ts: Date.now()}), | |
| redirect: "follow" | |
| }); | |
| if (!res.ok) throw new Error("تعذر الاتصال بالخادم"); | |
| const data = await res.json(); | |
| if (!data.ok) throw new Error(data.message || "حدث خطأ"); | |
| return data; | |
| } | |
| async function loadTasks() { | |
| $("statusBox").hidden = false; | |
| $("statusBox").textContent = "جاري تحميل المهام..."; | |
| $("taskList").hidden = true; | |
| $("emptyState").hidden = true; | |
| try { | |
| const data = await apiRequest({action:"list"}); | |
| allTasks = Array.isArray(data.tasks) ? data.tasks : []; | |
| updateDepartments(); | |
| updateStats(); | |
| renderTasks(); | |
| $("lastUpdated").textContent = `آخر تحديث: ${new Intl.DateTimeFormat("ar-SA-u-ca-gregory",{hour:"numeric",minute:"2-digit"}).format(new Date())}`; | |
| } catch (err) { | |
| $("statusBox").hidden = false; | |
| $("statusBox").textContent = err.message; | |
| toast(err.message, true); | |
| } | |
| } | |
| function updateStats() { | |
| const today = localISODate(); | |
| $("totalCount").textContent = allTasks.length; | |
| $("todayCount").textContent = allTasks.filter(t => t.date === today).length; | |
| $("deptCount").textContent = new Set(allTasks.map(t => (t.department || "").trim()).filter(Boolean)).size; | |
| } | |
| function updateDepartments() { | |
| const current = $("filterDept").value; | |
| const depts = [...new Set(allTasks.map(t => (t.department || "").trim()).filter(Boolean))] | |
| .sort((a,b)=>a.localeCompare(b,"ar")); | |
| $("filterDept").innerHTML = `<option value="">جميع الأقسام</option>` + | |
| depts.map(d => `<option value="${escapeHTML(d)}">${escapeHTML(d)}</option>`).join(""); | |
| if (depts.includes(current)) $("filterDept").value = current; | |
| } | |
| function filteredTasks() { | |
| const q = $("searchInput").value.trim().toLowerCase(); | |
| const date = $("filterDate").value; | |
| const dept = $("filterDept").value; | |
| return allTasks.filter(t => { | |
| const hay = `${t.task || ""} ${t.department || ""}`.toLowerCase(); | |
| return (!q || hay.includes(q)) && | |
| (!date || t.date === date) && | |
| (!dept || t.department === dept); | |
| }); | |
| } | |
| function renderTasks() { | |
| const tasks = filteredTasks(); | |
| $("statusBox").hidden = true; | |
| if (!tasks.length) { | |
| $("taskList").hidden = true; | |
| $("emptyState").hidden = false; | |
| return; | |
| } | |
| $("emptyState").hidden = true; | |
| $("taskList").hidden = false; | |
| $("taskList").innerHTML = tasks.map(t => { | |
| const d = new Date(`${t.date}T12:00:00`); | |
| const day = Number.isNaN(d.getTime()) ? "" : new Intl.DateTimeFormat("ar-SA-u-ca-gregory",{day:"numeric"}).format(d); | |
| const month = Number.isNaN(d.getTime()) ? "" : new Intl.DateTimeFormat("ar-SA-u-ca-gregory",{month:"long"}).format(d); | |
| const weekday = Number.isNaN(d.getTime()) ? "" : new Intl.DateTimeFormat("ar-SA-u-ca-gregory",{weekday:"long"}).format(d); | |
| return ` | |
| <article class="task-card"> | |
| <div class="task-date"> | |
| <strong>📅 ${escapeHTML(weekday)}</strong> | |
| <span>${escapeHTML(day)} ${escapeHTML(month)} • ${escapeHTML(t.date || "")}</span> | |
| </div> | |
| <div class="task-body"> | |
| <h3>${escapeHTML(t.task || "")}</h3> | |
| <p>تم تسجيل المهمة ضمن سجل الإنجازات</p> | |
| </div> | |
| <div class="task-meta-side"> | |
| <span class="dept-badge">${escapeHTML(t.department || "بدون قسم")}</span> | |
| ${t.status ? `<span class="status-badge ${t.status === "تم الإنجاز" ? "status-done" : t.status === "جاري العمل" ? "status-progress" : "status-planned"}">${escapeHTML(t.status)}</span>` : ""} | |
| <div class="quick-status"> | |
| <button type="button" class="${t.status === "تم الإنجاز" ? "is-selected" : ""}" data-status-row="${t.row}" data-status-value="تم الإنجاز">✓ تم الإنجاز</button> | |
| <button type="button" class="${t.status === "جاري العمل" ? "is-selected" : ""}" data-status-row="${t.row}" data-status-value="جاري العمل">◷ جاري العمل</button> | |
| <button type="button" class="${t.status === "مخطط لها" ? "is-selected" : ""}" data-status-row="${t.row}" data-status-value="مخطط لها">○ مخطط لها</button> | |
| <button type="button" class="${!t.status ? "is-selected" : ""}" data-status-row="${t.row}" data-status-value="">— بدون حالة</button> | |
| </div> | |
| <div class="task-actions"> | |
| <button class="action-btn edit-btn" type="button" data-edit-row="${t.row}">تعديل</button> | |
| <button class="action-btn delete-btn" type="button" data-delete-row="${t.row}">حذف</button> | |
| </div> | |
| </div> | |
| </article>`; | |
| }).join(""); | |
| } | |
| async function saveTask(e) { | |
| e.preventDefault(); | |
| const task = $("taskText").value.trim(); | |
| const department = $("taskDept").value.trim(); | |
| // إذا اختار المستخدم تاريخًا ولم يضغط "إضافة التاريخ"، نضيفه تلقائيًا. | |
| let dates = [...selectedDates]; | |
| const currentDate = $("taskDate").value; | |
| if (currentDate && !dates.includes(currentDate)) dates.push(currentDate); | |
| dates = sortDates(dates); | |
| if (!dates.length || !task || !department) return toast("أكمل التاريخ والمهمة والقسم", true); | |
| const btn = $("saveBtn"); | |
| btn.disabled = true; | |
| $("saveBtnText").textContent = dates.length > 1 ? `جاري حفظ ${dates.length} تواريخ...` : "جاري الحفظ..."; | |
| try { | |
| for (const date of dates) { | |
| await apiRequest({action:"add", date, task, department}); | |
| } | |
| toast(dates.length > 1 ? `تم تسجيل المهمة في ${dates.length} تواريخ ✓` : "تم تسجيل المهمة بنجاح ✓"); | |
| $("taskText").value = ""; | |
| $("charCount").textContent = "0"; | |
| selectedDates = []; | |
| $("taskDate").value = localISODate(); | |
| renderSelectedDates(); | |
| updateSelectedDateLabels(); | |
| await loadTasks(); | |
| } catch (err) { | |
| toast(err.message, true); | |
| } finally { | |
| btn.disabled = false; | |
| $("saveBtnText").textContent = "حفظ المهمة"; | |
| } | |
| } | |
| function taskByRow(row) { | |
| return allTasks.find(t => Number(t.row) === Number(row)); | |
| } | |
| function openEdit(row) { | |
| const t = taskByRow(row); | |
| if (!t) return; | |
| $("editRow").value = t.row; | |
| $("editDate").value = t.date || ""; | |
| $("editDept").value = t.department || ""; | |
| $("editText").value = t.task || ""; | |
| updateSelectedDateLabels(); | |
| $("editModal").hidden = false; | |
| document.body.style.overflow = "hidden"; | |
| } | |
| function closeEdit() { | |
| $("editModal").hidden = true; | |
| document.body.style.overflow = ""; | |
| } | |
| async function updateTask(e) { | |
| e.preventDefault(); | |
| const btn = $("editSaveBtn"); | |
| btn.disabled = true; | |
| btn.textContent = "جاري الحفظ..."; | |
| try { | |
| await apiRequest({ | |
| action:"update", | |
| row:Number($("editRow").value), | |
| date:$("editDate").value, | |
| task:$("editText").value.trim(), | |
| department:$("editDept").value, | |
| status:(taskByRow(Number($("editRow").value)) || {}).status || "" | |
| }); | |
| closeEdit(); | |
| toast("تم تعديل المهمة بنجاح ✓"); | |
| await loadTasks(); | |
| } catch (err) { | |
| toast(err.message, true); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "حفظ التعديل"; | |
| } | |
| } | |
| async function updateStatus(row, status) { | |
| const t = taskByRow(row); | |
| if (!t) return; | |
| try { | |
| await apiRequest({ | |
| action:"update", | |
| row:Number(row), | |
| date:t.date, | |
| task:t.task, | |
| department:t.department || "", | |
| status:status || "" | |
| }); | |
| toast(status ? `تم تحديث الحالة إلى: ${status}` : "تم إزالة الحالة"); | |
| await loadTasks(); | |
| } catch (err) { | |
| toast(err.message, true); | |
| } | |
| } | |
| async function deleteTask(row) { | |
| const t = taskByRow(row); | |
| if (!t) return; | |
| if (!confirm(`هل تريد حذف المهمة: "${t.task}"؟`)) return; | |
| try { | |
| await apiRequest({action:"delete", row:Number(row)}); | |
| toast("تم حذف المهمة"); | |
| await loadTasks(); | |
| } catch (err) { | |
| toast(err.message, true); | |
| } | |
| } | |
| document.addEventListener("DOMContentLoaded", () => { | |
| $("taskDate").value = localISODate(); | |
| $("todayText").textContent = "📅 " + formatArabicDate(localISODate(), true); | |
| selectedDates = [localISODate()]; | |
| try { renderSelectedDates(); } catch (e) { console.warn(e); } | |
| try { updateSelectedDateLabels(); } catch (e) { console.warn(e); } | |
| // تحميل السجل مبكرًا وبشكل مستقل حتى تظهر المهام السابقة دائمًا | |
| loadTasks(); | |
| if ($("entryToggle") && $("entryPanel")) { | |
| $("entryToggle").addEventListener("click", () => { | |
| const panel = $("entryPanel"); | |
| const willOpen = panel.hidden; | |
| panel.hidden = !willOpen; | |
| $("entryToggle").setAttribute("aria-expanded", String(willOpen)); | |
| $("entryToggle").classList.toggle("is-open", willOpen); | |
| const hint = $("entryToggle").querySelector("p"); | |
| if (hint) hint.textContent = willOpen | |
| ? "اضغط هنا لإخفاء منطقة إدخال المهام." | |
| : "اضغط هنا لإظهار منطقة إدخال المهام."; | |
| if (willOpen) setTimeout(() => panel.scrollIntoView({behavior:"smooth", block:"start"}), 50); | |
| }); | |
| } | |
| $("taskForm").addEventListener("submit", saveTask); | |
| $("taskDate").addEventListener("change", updateSelectedDateLabels); | |
| if ($("addDateBtn")) $("addDateBtn").addEventListener("click", addCurrentDate); | |
| if ($("selectedDates")) $("selectedDates").addEventListener("click", e => { | |
| const btn = e.target.closest("[data-remove-date]"); | |
| if (!btn) return; | |
| selectedDates = selectedDates.filter(d => d !== btn.dataset.removeDate); | |
| renderSelectedDates(); | |
| }); | |
| $("editDate").addEventListener("change", updateSelectedDateLabels); | |
| $("editForm").addEventListener("submit", updateTask); | |
| document.querySelectorAll("[data-close-modal]").forEach(el => el.addEventListener("click", closeEdit)); | |
| $("taskList").addEventListener("click", e => { | |
| const edit = e.target.closest("[data-edit-row]"); | |
| const del = e.target.closest("[data-delete-row]"); | |
| const status = e.target.closest("[data-status-row]"); | |
| if (status) return updateStatus(status.dataset.statusRow, status.dataset.statusValue); | |
| if (edit) openEdit(edit.dataset.editRow); | |
| if (del) deleteTask(del.dataset.deleteRow); | |
| }); | |
| $("taskText").addEventListener("input", e => $("charCount").textContent = e.target.value.length); | |
| $("refreshBtn").addEventListener("click", loadTasks); | |
| $("searchInput").addEventListener("input", renderTasks); | |
| $("filterDate").addEventListener("change", renderTasks); | |
| $("filterDept").addEventListener("change", renderTasks); | |
| $("clearFilters").addEventListener("click", () => { | |
| $("searchInput").value = ""; | |
| $("filterDate").value = ""; | |
| $("filterDept").value = ""; | |
| renderTasks(); | |
| }); | |
| $("taskForm").addEventListener("reset", () => { | |
| setTimeout(() => { | |
| $("taskDate").value = localISODate(); | |
| $("charCount").textContent = "0"; | |
| selectedDates = [localISODate()]; | |
| renderSelectedDates(); | |
| updateSelectedDateLabels(); | |
| }, 0); | |
| }); | |
| }); | |