n / script.js
NMA2030's picture
Upload 4 files
69ea80f verified
Raw History Blame Contribute Delete
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 => ({
"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"
}[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);
});
});