| <!doctype html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>الإقرارات</title> |
|
|
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet"> |
|
|
| |
| <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> |
|
|
| <style> |
| :root{ |
| --gastat-green:#53CD3F; |
| --gastat-blue:#00B2DF; |
| --gastat-purple:#4137A8; |
| --gastat-gray:#8492A2; |
| |
| --bg:#071321; |
| --card:#0b1d34; |
| --card2:#081a2f; |
| --text:#eef6ff; |
| --muted:#c3d1ea; |
| --line:rgba(255,255,255,.12); |
| --shadow: 0 18px 60px rgba(0,0,0,.35); |
| --radius:18px; |
| |
| --warn:#f59e0b; |
| --bad:#ef4444; |
| } |
| |
| *{ box-sizing:border-box; } |
| body{ |
| margin:0; |
| font-family:"Cairo", system-ui, -apple-system, Segoe UI, Arial, sans-serif; |
| background: |
| radial-gradient(900px 520px at 15% 15%, rgba(0,178,223,.22), transparent 60%), |
| radial-gradient(900px 520px at 85% 10%, rgba(83,205,63,.18), transparent 60%), |
| radial-gradient(1000px 650px at 60% 70%, rgba(65,55,168,.18), transparent 60%), |
| var(--bg); |
| color:var(--text); |
| } |
| |
| .wrap{ max-width:1200px; margin:18px auto 60px; padding:0 14px; } |
| |
| .hero{ |
| border:1px solid rgba(255,255,255,.12); |
| box-shadow: var(--shadow); |
| border-radius: var(--radius); |
| padding:18px 18px 14px; |
| background: linear-gradient(135deg, rgba(0,178,223,.20), rgba(65,55,168,.18)); |
| position:relative; overflow:hidden; |
| } |
| .hero:before{ |
| content:""; |
| position:absolute; inset:-60px -80px auto auto; |
| width:260px; height:260px; |
| background: radial-gradient(circle, rgba(255,255,255,.18), transparent 60%); |
| transform: rotate(12deg); |
| pointer-events:none; |
| } |
| |
| .creditLine{ |
| margin:0 0 6px; |
| text-align:center; |
| font-size:12px; |
| color: rgba(238,246,255,.9); |
| letter-spacing:.2px; |
| opacity:.92; |
| } |
| |
| .titleTop{ text-align:center; margin:0 0 8px; color:rgba(255,255,255,.9); font-size:16px; } |
| .titleBar{ |
| margin:0; text-align:center; font-size:18px; font-weight:800; |
| background: rgba(255,255,255,.06); |
| border:1px solid rgba(255,255,255,.12); |
| padding:12px 10px; border-radius:14px; |
| } |
| .subNote{ margin:10px 0 0; text-align:center; color:rgba(195,209,234,.92); font-size:13px; line-height:1.7; } |
| |
| .grid{ |
| display:grid; grid-template-columns: 1.2fr 1fr; gap:14px; margin-top:14px; |
| } |
| @media (max-width: 980px){ .grid{ grid-template-columns:1fr; } } |
| |
| .card{ |
| background: linear-gradient(180deg, rgba(11,29,52,.92), rgba(8,26,47,.92)); |
| border:1px solid rgba(255,255,255,.12); |
| box-shadow: var(--shadow); |
| border-radius: var(--radius); |
| padding:14px; |
| } |
| |
| .card h3{ margin:0 0 10px; font-size:15px; display:flex; gap:10px; align-items:center; } |
| .badge{ |
| font-size:12px; padding:4px 10px; border-radius:999px; |
| border:1px solid rgba(255,255,255,.16); |
| background: rgba(255,255,255,.06); |
| color: var(--muted); |
| } |
| |
| .formGrid{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; } |
| @media (max-width: 680px){ .formGrid{ grid-template-columns:1fr; } } |
| |
| label{ display:block; font-size:12px; color: var(--muted); margin:0 0 6px; } |
| input, select{ |
| width:100%; |
| padding:12px 12px; |
| border-radius:14px; |
| border:1px solid rgba(255,255,255,.14); |
| background: rgba(255,255,255,.06); |
| color: var(--text); |
| outline:none; |
| transition:.15s; |
| font-size:14px; |
| } |
| input:focus, select:focus{ |
| border-color: rgba(0,178,223,.55); |
| box-shadow: 0 0 0 4px rgba(0,178,223,.16); |
| } |
| input::placeholder{ color: rgba(195,209,234,.7); } |
| |
| .actions{ display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; } |
| .btn{ |
| border:0; border-radius:14px; padding:11px 14px; |
| font-family:inherit; font-weight:800; cursor:pointer; |
| display:inline-flex; align-items:center; gap:8px; |
| border:1px solid rgba(255,255,255,.14); |
| background: rgba(255,255,255,.08); |
| color: var(--text); |
| transition:.15s; |
| } |
| .btn:hover{ transform: translateY(-1px); } |
| .btn:active{ transform: translateY(0px); } |
| |
| .btn.primary{ |
| background: linear-gradient(135deg, rgba(83,205,63,.92), rgba(34,197,94,.78)); |
| border-color: rgba(83,205,63,.35); |
| } |
| .btn.export{ |
| background: linear-gradient(135deg, rgba(0,178,223,.92), rgba(65,55,168,.72)); |
| border-color: rgba(0,178,223,.35); |
| } |
| .btn.warn{ |
| background: linear-gradient(135deg, rgba(245,158,11,.95), rgba(217,119,6,.82)); |
| border-color: rgba(245,158,11,.35); |
| } |
| .btn.danger{ |
| background: linear-gradient(135deg, rgba(239,68,68,.95), rgba(220,38,38,.82)); |
| border-color: rgba(239,68,68,.35); |
| } |
| |
| .noteBox{ |
| margin-top:10px; |
| border-radius:14px; |
| border:1px solid rgba(255,255,255,.14); |
| background: rgba(0,178,223,.10); |
| padding:10px 12px; |
| color: rgba(238,246,255,.95); |
| font-size:13px; |
| line-height:1.7; |
| } |
| .noteBox.warn{ |
| background: rgba(245,158,11,.12); |
| border-color: rgba(245,158,11,.28); |
| } |
| |
| .signatureBox{ |
| width:100%; |
| height:180px; |
| background:#ffffff; |
| border-radius:14px; |
| border:2px dashed rgba(0,178,223,.6); |
| overflow:hidden; |
| position:relative; |
| } |
| .signatureBox canvas{ |
| width:100%; |
| height:100%; |
| display:block; |
| touch-action:none; |
| } |
| .sigActions{ |
| margin-top:8px; |
| text-align:left; |
| } |
| |
| .tableWrap{ |
| overflow:auto; |
| border-radius: var(--radius); |
| border:1px solid rgba(255,255,255,.12); |
| background: rgba(255,255,255,.04); |
| } |
| table{ width:100%; border-collapse:collapse; min-width:1100px; } |
| thead th{ |
| position:sticky; top:0; |
| background: rgba(255,255,255,.08); |
| backdrop-filter: blur(8px); |
| font-size:12px; color: rgba(238,246,255,.95); |
| text-align:center; |
| padding:10px 8px; |
| border-bottom:1px solid rgba(255,255,255,.12); |
| white-space:nowrap; |
| } |
| tbody td{ |
| font-size:13px; color: rgba(238,246,255,.92); |
| padding:10px 8px; |
| border-bottom:1px solid rgba(255,255,255,.08); |
| text-align:center; |
| vertical-align:middle; |
| white-space:nowrap; |
| } |
| tbody tr:hover{ background: rgba(255,255,255,.05); } |
| |
| .tinyBtn{ |
| padding:7px 10px; border-radius:12px; font-weight:800; font-size:12px; |
| border:1px solid rgba(255,255,255,.14); |
| background: rgba(255,255,255,.07); |
| color: var(--text); |
| cursor:pointer; |
| } |
| .tinyBtn.danger{ background: rgba(239,68,68,.18); border-color: rgba(239,68,68,.35); } |
| .tinyBtn.edit{ background: rgba(245,158,11,.16); border-color: rgba(245,158,11,.35); } |
| |
| .statusRow{ |
| display:flex; align-items:center; justify-content:space-between; |
| gap:10px; flex-wrap:wrap; margin-bottom:10px; |
| } |
| .pill{ |
| font-size:12px; padding:6px 10px; border-radius:999px; |
| border:1px solid rgba(255,255,255,.14); |
| background: rgba(255,255,255,.06); |
| color: rgba(195,209,234,.95); |
| } |
| .searchBox input{ |
| width:320px; max-width:70vw; |
| padding:10px 12px; border-radius:999px; |
| } |
| |
| @media (max-width: 480px){ |
| .wrap{ padding:0 10px; } |
| .titleBar{ font-size:16px; } |
| .btn{ width:100%; justify-content:center; } |
| .searchBox input{ width:100%; } |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="wrap"> |
| <div class="hero"> |
| <p class="titleTop">"بيان استلام"</p> |
| <h1 class="titleBar">إدارة حالات الإقرارات وتوثيق الاستلام</h1> |
| <p class="creditLine">تصميم وإعداد: نوف الناصر</p> |
| <p class="subNote"> |
| أدخل البيانات ثم اختر الحالة. يتم الحفظ تلقائيًا ولا تختفي السجلات عند إغلاق الصفحة إلا بالحذف. |
| </p> |
| </div> |
|
|
| <div class="grid"> |
| |
| <div class="card"> |
| <h3>إدخال بيانات <span class="badge">يدوي + حفظ</span></h3> |
|
|
| <div class="formGrid"> |
| <div> |
| <label>اسم الشركة (مطلوب)</label> |
| <input id="company" type="text" placeholder="مثال: شركة ...." /> |
| </div> |
|
|
| <div> |
| <label>رقم السجل التجاري</label> |
| <input id="cr" type="text" inputmode="numeric" placeholder="مثال: 1010xxxxxx" /> |
| </div> |
|
|
| <div style="grid-column:1/-1"> |
| <label>الحالة</label> |
| <select id="status"> |
| <option value="RESPONDED">استجاب</option> |
| <option value="BRANCH_NO_SEPARATE">فرع ليس له حسابات مستقلة</option> |
| <option value="TEMP_CLOSED">مغلقة مؤقتًا</option> |
| <option value="PERM_CLOSED">مغلقة نهائيًا</option> |
| </select> |
| </div> |
|
|
| <div id="respondedBlock" style="grid-column:1/-1; display:none;"> |
| <div class="formGrid"> |
| <div style="grid-column:1/-1"> |
| <label>نوع الكيان عند الاستجابة</label> |
| <select id="respondedType"> |
| <option value="">— اختر —</option> |
| <option value="MAIN_CENTER">مركز رئيسي</option> |
| <option value="BRANCH_SEPARATE">فرع له حسابات مستقلة</option> |
| <option value="SINGLE">مفردة</option> |
| </select> |
| </div> |
|
|
| <div> |
| <label>اسم المستلم</label> |
| <input id="receiver" type="text" placeholder="الاسم الثلاثي" /> |
| </div> |
|
|
| <div> |
| <label>رقم الجوال</label> |
| <input id="mobile" type="tel" inputmode="numeric" placeholder="05xxxxxxxx أو 9665xxxxxxxx" /> |
| </div> |
|
|
| <div> |
| <label>البريد الإلكتروني</label> |
| <input id="email" type="email" placeholder="name@example.com" /> |
| </div> |
|
|
| <div> |
| <label>التاريخ</label> |
| <input id="date" type="date" /> |
| </div> |
|
|
| <div style="grid-column:1/-1"> |
| <label>التوقيع اليدوي</label> |
| <div class="signatureBox"> |
| <canvas id="signaturePad"></canvas> |
| </div> |
| <div class="sigActions"> |
| <button type="button" class="tinyBtn" id="clearSignature">مسح التوقيع</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="noteBox" id="respondedHelp"> |
| عند اختيار <b>استجاب</b> يُفضّل تحديد (مركز/فرع مستقل/مفردة)، ثم تُكمل بيانات الاستلام إن توفرت. |
| </div> |
| </div> |
|
|
| <div id="photoNote" style="grid-column:1/-1; display:none;"> |
| <div class="noteBox warn"> |
| 📸 <b>ملاحظة:</b> التقط صورة وسترفق لاحقًا بالنظام من قبلك. |
| </div> |
| </div> |
|
|
| <div style="grid-column:1/-1"> |
| <label>ملاحظة إضافية (اختياري)</label> |
| <input id="extraNote" type="text" placeholder="أي تعليق أو توضيح..." /> |
| </div> |
| </div> |
|
|
| <div class="actions"> |
| <button class="btn primary" id="saveBtn">💾 حفظ</button> |
| <button class="btn warn" id="clearFormBtn">🧹 تفريغ الحقول</button> |
| <button class="btn export" id="exportBtn">📤 تصدير Excel</button> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h3>إدارة السجلات <span class="badge">بحث + حذف</span></h3> |
|
|
| <div class="statusRow"> |
| <div class="pill" id="countPill">عدد السجلات: 0</div> |
| <div class="searchBox"> |
| <input id="search" type="text" placeholder="🔎 بحث: شركة / سجل / حالة / جوال / بريد" /> |
| </div> |
| </div> |
|
|
| <div class="actions"> |
| <button class="btn danger" id="deleteSelectedBtn">🗑️ حذف المحدد</button> |
| <button class="btn danger" id="deleteAllBtn">⚠️ حذف الكل</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="card" style="margin-top:14px;"> |
| <h3>الجدول</h3> |
|
|
| <div class="tableWrap"> |
| <table> |
| <thead> |
| <tr> |
| <th>تحديد</th> |
| <th>م</th> |
| <th>اسم الشركة</th> |
| <th>رقم السجل التجاري</th> |
| <th>الحالة</th> |
| <th>تفصيل الاستجابة</th> |
| <th>اسم المستلم</th> |
| <th>رقم الجوال</th> |
| <th>البريد الإلكتروني</th> |
| <th>التاريخ</th> |
| <th>التوقيع</th> |
| <th>ملاحظة</th> |
| <th>إجراءات</th> |
| </tr> |
| </thead> |
| <tbody id="tbody"></tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const STORAGE_KEY = "eq_records_v3"; |
| |
| const el = { |
| company: document.getElementById("company"), |
| cr: document.getElementById("cr"), |
| status: document.getElementById("status"), |
| respondedBlock: document.getElementById("respondedBlock"), |
| respondedType: document.getElementById("respondedType"), |
| receiver: document.getElementById("receiver"), |
| mobile: document.getElementById("mobile"), |
| email: document.getElementById("email"), |
| date: document.getElementById("date"), |
| extraNote: document.getElementById("extraNote"), |
| photoNote: document.getElementById("photoNote"), |
| |
| saveBtn: document.getElementById("saveBtn"), |
| clearFormBtn: document.getElementById("clearFormBtn"), |
| exportBtn: document.getElementById("exportBtn"), |
| |
| tbody: document.getElementById("tbody"), |
| countPill: document.getElementById("countPill"), |
| search: document.getElementById("search"), |
| |
| deleteSelectedBtn: document.getElementById("deleteSelectedBtn"), |
| deleteAllBtn: document.getElementById("deleteAllBtn"), |
| }; |
| |
| const canvas = document.getElementById("signaturePad"); |
| const ctx = canvas.getContext("2d"); |
| let drawing = false; |
| let signatureData = null; |
| let scaledOnce = false; |
| |
| let records = []; |
| let editId = null; |
| |
| function todayISO(){ |
| const 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 escapeHtml(str){ |
| return String(str ?? "") |
| .replaceAll("&","&") |
| .replaceAll("<","<") |
| .replaceAll(">",">") |
| .replaceAll('"',""") |
| .replaceAll("'","'"); |
| } |
| |
| function sanitize(s){ return String(s ?? "").trim(); } |
| |
| function load(){ |
| try{ |
| const raw = localStorage.getItem(STORAGE_KEY); |
| records = raw ? JSON.parse(raw) : []; |
| }catch(e){ |
| records = []; |
| } |
| } |
| function persist(){ |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(records)); |
| } |
| |
| function isValidEmail(v){ |
| if(!v) return true; |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); |
| } |
| |
| function normalizeMobile(v){ |
| return sanitize(v).replace(/\s+/g,"").replace(/[^\d+]/g,""); |
| } |
| |
| function isValidMobile(v){ |
| if(!v) return true; |
| const x = normalizeMobile(v); |
| return /^(05\d{8}|9665\d{8}|\+9665\d{8})$/.test(x); |
| } |
| |
| function statusLabel(code){ |
| switch(code){ |
| case "RESPONDED": return "استجاب"; |
| case "BRANCH_NO_SEPARATE": return "فرع ليس له حسابات مستقلة"; |
| case "TEMP_CLOSED": return "مغلقة مؤقتًا"; |
| case "PERM_CLOSED": return "مغلقة نهائيًا"; |
| default: return code || ""; |
| } |
| } |
| |
| function respondedTypeLabel(code){ |
| switch(code){ |
| case "MAIN_CENTER": return "مركز رئيسي"; |
| case "BRANCH_SEPARATE": return "فرع له حسابات مستقلة"; |
| case "SINGLE": return "مفردة"; |
| default: return ""; |
| } |
| } |
| |
| function updateConditionalUI(){ |
| const s = el.status.value; |
| const isResponded = (s === "RESPONDED"); |
| const needsPhoto = (s === "TEMP_CLOSED" || s === "PERM_CLOSED"); |
| |
| el.respondedBlock.style.display = isResponded ? "block" : "none"; |
| el.photoNote.style.display = needsPhoto ? "block" : "none"; |
| |
| if(!isResponded){ |
| el.respondedType.value = ""; |
| if(el.receiver) el.receiver.value = ""; |
| if(el.mobile) el.mobile.value = ""; |
| if(el.email) el.email.value = ""; |
| clearSignaturePad(); |
| } |
| } |
| |
| function resetForm(){ |
| el.company.value = ""; |
| el.cr.value = ""; |
| el.status.value = "RESPONDED"; |
| el.respondedType.value = ""; |
| if(el.receiver) el.receiver.value = ""; |
| if(el.mobile) el.mobile.value = ""; |
| if(el.email) el.email.value = ""; |
| el.date.value = todayISO(); |
| el.extraNote.value = ""; |
| editId = null; |
| el.saveBtn.textContent = "💾 حفظ"; |
| clearSignaturePad(); |
| updateConditionalUI(); |
| } |
| |
| function fillForm(rec){ |
| el.company.value = rec.company || ""; |
| el.cr.value = rec.cr || ""; |
| el.status.value = rec.status || "RESPONDED"; |
| el.respondedType.value = rec.respondedType || ""; |
| if(el.receiver) el.receiver.value = rec.receiver || ""; |
| if(el.mobile) el.mobile.value = rec.mobile || ""; |
| if(el.email) el.email.value = rec.email || ""; |
| el.date.value = rec.date || todayISO(); |
| el.extraNote.value = rec.extraNote || ""; |
| editId = rec.id; |
| el.saveBtn.textContent = "✅ حفظ التعديل"; |
| updateConditionalUI(); |
| |
| clearSignaturePad(); |
| if(rec.signature){ |
| const img = new Image(); |
| img.onload = () => { |
| const rect = canvas.getBoundingClientRect(); |
| ctx.drawImage(img, 0, 0, rect.width, rect.height); |
| signatureData = rec.signature; |
| }; |
| img.src = rec.signature; |
| } |
| |
| window.scrollTo({ top: 0, behavior: "smooth" }); |
| } |
| |
| function filteredRecords(){ |
| const q = sanitize(el.search.value).toLowerCase(); |
| if(!q) return records; |
| return records.filter(r => { |
| const hay = [ |
| r.company, r.cr, statusLabel(r.status), respondedTypeLabel(r.respondedType), |
| r.receiver, r.mobile, r.email, r.date, r.note, r.extraNote |
| ].join(" ").toLowerCase(); |
| return hay.includes(q); |
| }); |
| } |
| |
| function render(){ |
| const list = filteredRecords(); |
| el.tbody.innerHTML = ""; |
| |
| list.forEach((r, idx) => { |
| const tr = document.createElement("tr"); |
| tr.innerHTML = ` |
| <td><input type="checkbox" data-id="${r.id}" class="rowCheck"></td> |
| <td>${idx + 1}</td> |
| <td>${escapeHtml(r.company)}</td> |
| <td>${escapeHtml(r.cr)}</td> |
| <td>${escapeHtml(statusLabel(r.status))}</td> |
| <td>${escapeHtml(respondedTypeLabel(r.respondedType))}</td> |
| <td>${escapeHtml(r.receiver)}</td> |
| <td>${escapeHtml(r.mobile)}</td> |
| <td>${escapeHtml(r.email)}</td> |
| <td>${escapeHtml(r.date)}</td> |
| <td>${r.signature ? `<img src="${r.signature}" style="height:50px; background:#fff; border-radius:8px; padding:2px;">` : ""}</td> |
| <td>${escapeHtml(r.note || "")}${r.extraNote ? " | " + escapeHtml(r.extraNote) : ""}</td> |
| <td> |
| <button class="tinyBtn edit" data-action="edit" data-id="${r.id}">تعديل</button> |
| <button class="tinyBtn danger" data-action="delete" data-id="${r.id}">حذف</button> |
| </td> |
| `; |
| el.tbody.appendChild(tr); |
| }); |
| |
| el.countPill.textContent = `عدد السجلات: ${records.length}`; |
| } |
| |
| function buildAutoNote(status){ |
| if(status === "BRANCH_NO_SEPARATE"){ |
| return "فرع ليس له حسابات مستقلة"; |
| } |
| if(status === "TEMP_CLOSED"){ |
| return "مغلقة مؤقتًا - التقط صورة وسترفق لاحقًا بالنظام من قبلك"; |
| } |
| if(status === "PERM_CLOSED"){ |
| return "مغلقة نهائيًا - التقط صورة وسترفق لاحقًا بالنظام من قبلك"; |
| } |
| return ""; |
| } |
| |
| function resizeCanvas(){ |
| const ratio = window.devicePixelRatio || 1; |
| const rect = canvas.getBoundingClientRect(); |
| |
| const oldData = signatureData; |
| |
| canvas.width = Math.max(1, Math.floor(rect.width * ratio)); |
| canvas.height = Math.max(1, Math.floor(rect.height * ratio)); |
| |
| ctx.setTransform(ratio, 0, 0, ratio, 0, 0); |
| ctx.lineWidth = 2; |
| ctx.lineCap = "round"; |
| ctx.strokeStyle = "#000"; |
| |
| if(oldData){ |
| const img = new Image(); |
| img.onload = () => { |
| ctx.drawImage(img, 0, 0, rect.width, rect.height); |
| signatureData = oldData; |
| }; |
| img.src = oldData; |
| } |
| } |
| |
| function clearSignaturePad(){ |
| const rect = canvas.getBoundingClientRect(); |
| ctx.clearRect(0, 0, rect.width, rect.height); |
| signatureData = null; |
| } |
| |
| function getPos(e){ |
| const rect = canvas.getBoundingClientRect(); |
| if(e.touches && e.touches[0]){ |
| return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top }; |
| } |
| return { x: e.clientX - rect.left, y: e.clientY - rect.top }; |
| } |
| |
| function startDraw(e){ |
| drawing = true; |
| const p = getPos(e); |
| ctx.beginPath(); |
| ctx.moveTo(p.x, p.y); |
| } |
| |
| function draw(e){ |
| if(!drawing) return; |
| e.preventDefault(); |
| const p = getPos(e); |
| ctx.lineTo(p.x, p.y); |
| ctx.stroke(); |
| } |
| |
| function endDraw(){ |
| if(!drawing) return; |
| drawing = false; |
| signatureData = canvas.toDataURL("image/png"); |
| } |
| |
| canvas.addEventListener("mousedown", startDraw); |
| canvas.addEventListener("mousemove", draw); |
| canvas.addEventListener("mouseup", endDraw); |
| canvas.addEventListener("mouseleave", endDraw); |
| |
| canvas.addEventListener("touchstart", startDraw, { passive:false }); |
| canvas.addEventListener("touchmove", draw, { passive:false }); |
| canvas.addEventListener("touchend", endDraw); |
| |
| document.getElementById("clearSignature").addEventListener("click", clearSignaturePad); |
| |
| function save(){ |
| const company = sanitize(el.company.value); |
| if(!company){ |
| alert("فضلاً أدخل اسم الشركة (مطلوب)."); |
| return; |
| } |
| |
| const status = el.status.value; |
| const cr = sanitize(el.cr.value); |
| |
| const respondedType = sanitize(el.respondedType.value); |
| const receiver = el.receiver ? sanitize(el.receiver.value) : ""; |
| const mobileRaw = el.mobile ? sanitize(el.mobile.value) : ""; |
| const mobile = normalizeMobile(mobileRaw); |
| const email = el.email ? sanitize(el.email.value) : ""; |
| const date = el.date.value || todayISO(); |
| const extraNote = sanitize(el.extraNote.value); |
| |
| if(email && !isValidEmail(email)){ |
| alert("البريد الإلكتروني غير صحيح."); |
| return; |
| } |
| if(mobileRaw && !isValidMobile(mobileRaw)){ |
| alert("رقم الجوال غير صحيح. مثال: 05xxxxxxxx أو 9665xxxxxxxx أو +9665xxxxxxxx"); |
| return; |
| } |
| |
| let note = buildAutoNote(status); |
| |
| const rec = { |
| id: editId ?? crypto.randomUUID(), |
| company, |
| cr, |
| status, |
| respondedType: status === "RESPONDED" ? respondedType : "", |
| receiver: status === "RESPONDED" ? receiver : "", |
| mobile: status === "RESPONDED" ? mobile : "", |
| email: status === "RESPONDED" ? email : "", |
| date: status === "RESPONDED" ? date : (date || todayISO()), |
| signature: (status === "RESPONDED") ? (signatureData || "") : "", |
| note, |
| extraNote, |
| updatedAt: new Date().toISOString() |
| }; |
| |
| if(editId){ |
| const i = records.findIndex(x => x.id === editId); |
| if(i !== -1) records[i] = { ...records[i], ...rec }; |
| }else{ |
| records.push(rec); |
| } |
| |
| persist(); |
| render(); |
| resetForm(); |
| } |
| |
| function deleteOne(id){ |
| const r = records.find(x => x.id === id); |
| if(!r) return; |
| if(!confirm(`تأكيد حذف سجل الشركة: ${r.company} ؟`)) return; |
| records = records.filter(x => x.id !== id); |
| persist(); |
| render(); |
| if(editId === id) resetForm(); |
| } |
| |
| function deleteSelected(){ |
| const checks = [...document.querySelectorAll(".rowCheck:checked")]; |
| if(checks.length === 0){ alert("لم يتم تحديد أي سجلات."); return; } |
| if(!confirm(`تأكيد حذف (${checks.length}) سجل/سجلات؟`)) return; |
| const ids = new Set(checks.map(c => c.getAttribute("data-id"))); |
| records = records.filter(r => !ids.has(r.id)); |
| persist(); |
| render(); |
| if(editId && ids.has(editId)) resetForm(); |
| } |
| |
| function deleteAll(){ |
| if(records.length === 0){ alert("لا توجد سجلات للحذف."); return; } |
| if(!confirm("تحذير: سيتم حذف جميع السجلات نهائيًا من هذه الصفحة. هل أنت متأكد؟")) return; |
| records = []; |
| persist(); |
| render(); |
| resetForm(); |
| } |
| |
| function exportExcel(){ |
| const list = filteredRecords(); |
| if(list.length === 0){ alert("لا توجد بيانات للتصدير."); return; } |
| |
| const rows = list.map((r, idx) => ({ |
| "م": idx + 1, |
| "اسم الشركة": r.company, |
| "رقم السجل التجاري": r.cr, |
| "الحالة": statusLabel(r.status), |
| "تفصيل الاستجابة": respondedTypeLabel(r.respondedType), |
| "اسم المستلم": r.receiver, |
| "رقم الجوال": r.mobile, |
| "البريد الإلكتروني": r.email, |
| "التاريخ": r.date, |
| "التوقيع": r.signature ? "مرفق (صورة)" : "", |
| "ملاحظة": (r.note || "") + (r.extraNote ? " | " + r.extraNote : "") |
| })); |
| |
| const ws = XLSX.utils.json_to_sheet(rows, { cellDates: true }); |
| |
| ws["!cols"] = [ |
| { wch: 5 }, |
| { wch: 32 }, |
| { wch: 18 }, |
| { wch: 18 }, |
| { wch: 20 }, |
| { wch: 22 }, |
| { wch: 18 }, |
| { wch: 28 }, |
| { wch: 14 }, |
| { wch: 14 }, |
| { wch: 42 }, |
| ]; |
| |
| const wb = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(wb, ws, "الإقرارات"); |
| XLSX.writeFile(wb, `EQ_${todayISO()}.xlsx`); |
| } |
| |
| |
| el.status.addEventListener("change", updateConditionalUI); |
| el.saveBtn.addEventListener("click", save); |
| el.clearFormBtn.addEventListener("click", resetForm); |
| el.exportBtn.addEventListener("click", exportExcel); |
| el.search.addEventListener("input", render); |
| el.deleteSelectedBtn.addEventListener("click", deleteSelected); |
| el.deleteAllBtn.addEventListener("click", deleteAll); |
| |
| el.tbody.addEventListener("click", (e) => { |
| const btn = e.target.closest("button"); |
| if(!btn) return; |
| const action = btn.getAttribute("data-action"); |
| const id = btn.getAttribute("data-id"); |
| if(action === "delete") deleteOne(id); |
| if(action === "edit"){ |
| const rec = records.find(x => x.id === id); |
| if(rec) fillForm(rec); |
| } |
| }); |
| |
| |
| el.date.value = todayISO(); |
| load(); |
| updateConditionalUI(); |
| render(); |
| |
| setTimeout(() => { resizeCanvas(); }, 0); |
| window.addEventListener("resize", () => { resizeCanvas(); }); |
| </script> |
| </body> |
| </html> |