|
|
<!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> |