Spaces:
Running
Running
Update app.js
Browse files
app.js
CHANGED
|
@@ -39,16 +39,30 @@ function normalizeTime(val){
|
|
| 39 |
}
|
| 40 |
function normalizeDate(v){
|
| 41 |
v=(v||"").trim();
|
|
|
|
| 42 |
let m=v.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/);
|
| 43 |
if(m){
|
| 44 |
let d=+m[1], mo=+m[2], y=+m[3]; if(y<100) y+=2000;
|
| 45 |
return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
|
| 46 |
}
|
|
|
|
| 47 |
m=v.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
|
| 48 |
if(m){
|
| 49 |
let y=+m[1], mo=+m[2], d=+m[3];
|
| 50 |
return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
|
| 51 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
return v;
|
| 53 |
}
|
| 54 |
function splitTickets(raw){
|
|
@@ -64,7 +78,7 @@ function compileFieldPatterns(){
|
|
| 64 |
const lbls = labels.map(l => l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
|
| 65 |
pats[canonical] = [
|
| 66 |
new RegExp(`(?:^|\\n)\\s*(?:${lbls})\\s*${LABEL_SEP}(.+)$`, "mi"),
|
| 67 |
-
new RegExp(`(?:^|\\n)\\s*(?:${lbls})\\s*${LABEL_SEP}\\n
|
| 68 |
];
|
| 69 |
}
|
| 70 |
return pats;
|
|
@@ -189,7 +203,7 @@ function toast(msg){
|
|
| 189 |
setTimeout(()=>{ t.hidden = true; }, 2000);
|
| 190 |
}
|
| 191 |
|
| 192 |
-
/* تصدير/مشاركة Excel
|
| 193 |
async function exportExcel(){
|
| 194 |
const rows = readTable();
|
| 195 |
if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
|
|
@@ -197,6 +211,27 @@ async function exportExcel(){
|
|
| 197 |
const aoa = [EXPORT_COLUMNS, ...rows.map(r=>EXPORT_COLUMNS.map(c=>r[c]||""))];
|
| 198 |
const ws = XLSX.utils.aoa_to_sheet(aoa);
|
| 199 |
ws["!rtl"] = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
const wb = XLSX.utils.book_new();
|
| 201 |
XLSX.utils.book_append_sheet(wb, ws, "التذاكر");
|
| 202 |
|
|
@@ -209,10 +244,12 @@ async function exportExcel(){
|
|
| 209 |
const blob = new Blob([wbArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
| 210 |
const file = new File([blob], filename, { type: blob.type });
|
| 211 |
|
|
|
|
| 212 |
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
| 213 |
try { await navigator.share({ files: [file], title: "ملف التذاكر" }); toast("تمت المشاركة/الحفظ."); return; }
|
| 214 |
catch(e){ /* المستخدم أغلق ورقة المشاركة */ }
|
| 215 |
}
|
|
|
|
| 216 |
const url = URL.createObjectURL(blob);
|
| 217 |
const a = document.createElement("a"); a.href = url; a.download = filename;
|
| 218 |
document.body.appendChild(a); a.click(); a.remove();
|
|
@@ -220,18 +257,32 @@ async function exportExcel(){
|
|
| 220 |
toast("تم تنزيل الملف.");
|
| 221 |
}
|
| 222 |
|
| 223 |
-
/* نسخ
|
| 224 |
async function copyToClipboardTSV(){
|
| 225 |
const rows = readTable();
|
| 226 |
if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
|
|
|
|
|
|
|
| 227 |
const header = EXPORT_COLUMNS.join("\t");
|
| 228 |
-
const body = rows.map(r=>
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
|
| 234 |
-
toast("تم النسخ — الصق/ي في Excel.");
|
| 235 |
}
|
| 236 |
}
|
| 237 |
|
|
@@ -245,13 +296,9 @@ const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاست
|
|
| 245 |
اسم المسح: الخبر
|
| 246 |
المنطقة: الشرقية`;
|
| 247 |
|
| 248 |
-
/* تنظيف كامل
|
| 249 |
function wipeAll(){
|
| 250 |
-
try{
|
| 251 |
-
// لا نخزّن شيئًا؛ احتياطًا نمسح أي أثر جلسة
|
| 252 |
-
sessionStorage.clear();
|
| 253 |
-
localStorage.removeItem("rawTickets");
|
| 254 |
-
}catch{}
|
| 255 |
const rawEl = document.getElementById("raw");
|
| 256 |
const tbody = document.getElementById("tbody");
|
| 257 |
if(rawEl) rawEl.value = "";
|
|
@@ -289,7 +336,7 @@ function init(){
|
|
| 289 |
|
| 290 |
sampleBtn.addEventListener("click", ()=>{ rawEl.value = SAMPLE; });
|
| 291 |
|
| 292 |
-
// عند الخروج/إغلاق
|
| 293 |
window.addEventListener("pagehide", wipeAll, { once:true });
|
| 294 |
window.addEventListener("beforeunload", wipeAll, { once:true });
|
| 295 |
document.addEventListener("visibilitychange", ()=>{
|
|
|
|
| 39 |
}
|
| 40 |
function normalizeDate(v){
|
| 41 |
v=(v||"").trim();
|
| 42 |
+
// 1) 21/8/2025 أو 21-08-25
|
| 43 |
let m=v.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/);
|
| 44 |
if(m){
|
| 45 |
let d=+m[1], mo=+m[2], y=+m[3]; if(y<100) y+=2000;
|
| 46 |
return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
|
| 47 |
}
|
| 48 |
+
// 2) 2025/08/21
|
| 49 |
m=v.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
|
| 50 |
if(m){
|
| 51 |
let y=+m[1], mo=+m[2], d=+m[3];
|
| 52 |
return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
|
| 53 |
}
|
| 54 |
+
// 3) 21 أغسطس 2025 / 21 آب 2025
|
| 55 |
+
const months = {
|
| 56 |
+
"يناير":1,"فبراير":2,"مارس":3,"أبريل":4,"ابريل":4,"مايو":5,"يونيو":6,"يوليو":7,"أغسطس":8,"اغسطس":8,"سبتمبر":9,"أكتوبر":10,"اكتوبر":10,"نوفمبر":11,"ديسمبر":12,
|
| 57 |
+
"كانون الثاني":1,"شباط":2,"آذار":3,"نيسان":4,"أيار":5,"حزيران":6,"تموز":7,"آب":8,"أيلول":9,"تشرين الأول":10,"تشرين الثاني":11,"كانون الأول":12
|
| 58 |
+
};
|
| 59 |
+
const rx = new RegExp(`(\\d{1,2})\\s+(${Object.keys(months).join("|")})\\s+(\\d{2,4})`,"i");
|
| 60 |
+
m = v.match(rx);
|
| 61 |
+
if(m){
|
| 62 |
+
const d = +m[1]; const mo = months[m[2].toLowerCase()] || months[m[2]];
|
| 63 |
+
let y = +m[3]; if(y<100) y+=2000;
|
| 64 |
+
if(mo) return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
|
| 65 |
+
}
|
| 66 |
return v;
|
| 67 |
}
|
| 68 |
function splitTickets(raw){
|
|
|
|
| 78 |
const lbls = labels.map(l => l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
|
| 79 |
pats[canonical] = [
|
| 80 |
new RegExp(`(?:^|\\n)\\s*(?:${lbls})\\s*${LABEL_SEP}(.+)$`, "mi"),
|
| 81 |
+
new RegExp(`(?:^|\\n)\\s*(?:${lbls})\\s*${LABEL_SEP}\\n\\س*(.+)`, "mi"),
|
| 82 |
];
|
| 83 |
}
|
| 84 |
return pats;
|
|
|
|
| 203 |
setTimeout(()=>{ t.hidden = true; }, 2000);
|
| 204 |
}
|
| 205 |
|
| 206 |
+
/* ======== تصدير/مشاركة Excel مع حفظ الأصفار كنص (هوية/جهاز/جوال) ======== */
|
| 207 |
async function exportExcel(){
|
| 208 |
const rows = readTable();
|
| 209 |
if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
|
|
|
|
| 211 |
const aoa = [EXPORT_COLUMNS, ...rows.map(r=>EXPORT_COLUMNS.map(c=>r[c]||""))];
|
| 212 |
const ws = XLSX.utils.aoa_to_sheet(aoa);
|
| 213 |
ws["!rtl"] = true;
|
| 214 |
+
|
| 215 |
+
// أعمدة نصية للحفاظ على الأصفار
|
| 216 |
+
const textCols = new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
|
| 217 |
+
const nRows = rows.length + 1; // +1 للهيدر
|
| 218 |
+
EXPORT_COLUMNS.forEach((colName, colIdx)=>{
|
| 219 |
+
if(!textCols.has(colName)) return;
|
| 220 |
+
for(let r = 1; r < nRows; r++){ // ابدأ من 1 (تجاهل الهيدر في 0)
|
| 221 |
+
const addr = XLSX.utils.encode_cell({ c: colIdx, r });
|
| 222 |
+
const cell = ws[addr];
|
| 223 |
+
if(cell){
|
| 224 |
+
cell.t = "s";
|
| 225 |
+
cell.z = "@";
|
| 226 |
+
if(typeof cell.v !== "string") cell.v = String(cell.v ?? "");
|
| 227 |
+
if(typeof cell.w !== "string") cell.w = cell.v;
|
| 228 |
+
}else{
|
| 229 |
+
// إن كانت الخلية فارغة، أنشئ خلية نصية فارغة لضمان التنسيق
|
| 230 |
+
ws[addr] = { t:"s", v:"", z:"@" };
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
const wb = XLSX.utils.book_new();
|
| 236 |
XLSX.utils.book_append_sheet(wb, ws, "التذاكر");
|
| 237 |
|
|
|
|
| 244 |
const blob = new Blob([wbArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
| 245 |
const file = new File([blob], filename, { type: blob.type });
|
| 246 |
|
| 247 |
+
// موبايل: مشاركة/حفظ من ورقة المشاركة
|
| 248 |
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
| 249 |
try { await navigator.share({ files: [file], title: "ملف التذاكر" }); toast("تمت المشاركة/الحفظ."); return; }
|
| 250 |
catch(e){ /* المستخدم أغلق ورقة المشاركة */ }
|
| 251 |
}
|
| 252 |
+
// بديل: تنزيل عادي
|
| 253 |
const url = URL.createObjectURL(blob);
|
| 254 |
const a = document.createElement("a"); a.href = url; a.download = filename;
|
| 255 |
document.body.appendChild(a); a.click(); a.remove();
|
|
|
|
| 257 |
toast("تم تنزيل الملف.");
|
| 258 |
}
|
| 259 |
|
| 260 |
+
/* ======== نسخ إلى الحافظة (TSV) مع BOM وحفظ الأصفار قدر الإمكان ======== */
|
| 261 |
async function copyToClipboardTSV(){
|
| 262 |
const rows = readTable();
|
| 263 |
if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
|
| 264 |
+
|
| 265 |
+
const textCols = new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
|
| 266 |
const header = EXPORT_COLUMNS.join("\t");
|
| 267 |
+
const body = rows.map(r =>
|
| 268 |
+
EXPORT_COLUMNS.map(c => {
|
| 269 |
+
let v = (r[c] ?? "").toString().replace(/\t/g," ");
|
| 270 |
+
// إذا العمود نصي ونظنه رقمي/قد يبدأ بصفر، أضف ' لمنع إزالة الأصفار في Excel
|
| 271 |
+
if(textCols.has(c) && v && /^[0-9]+$/.test(v)) v = "'" + v;
|
| 272 |
+
return v;
|
| 273 |
+
}).join("\t")
|
| 274 |
+
).join("\r\n");
|
| 275 |
+
|
| 276 |
+
const tsv = "\uFEFF" + header + "\r\n" + body; // BOM + CRLF
|
| 277 |
+
|
| 278 |
+
try{
|
| 279 |
+
await navigator.clipboard.writeText(tsv);
|
| 280 |
+
toast("تم النسخ — الصق/ي مباشرة في Excel.");
|
| 281 |
+
}catch(e){
|
| 282 |
+
const ta = document.createElement("textarea");
|
| 283 |
+
ta.value = tsv; document.body.appendChild(ta);
|
| 284 |
ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
|
| 285 |
+
toast("تم النسخ — الصق/ي مباشرة في Excel.");
|
| 286 |
}
|
| 287 |
}
|
| 288 |
|
|
|
|
| 296 |
اسم المسح: الخبر
|
| 297 |
المنطقة: الشرقية`;
|
| 298 |
|
| 299 |
+
/* تنظيف كامل عند المسح أو الخروج لسرعة الصفحة */
|
| 300 |
function wipeAll(){
|
| 301 |
+
try{ sessionStorage.clear(); localStorage.removeItem("rawTickets"); }catch{}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
const rawEl = document.getElementById("raw");
|
| 303 |
const tbody = document.getElementById("tbody");
|
| 304 |
if(rawEl) rawEl.value = "";
|
|
|
|
| 336 |
|
| 337 |
sampleBtn.addEventListener("click", ()=>{ rawEl.value = SAMPLE; });
|
| 338 |
|
| 339 |
+
// عند الخروج/إغلاق الصفحة
|
| 340 |
window.addEventListener("pagehide", wipeAll, { once:true });
|
| 341 |
window.addEventListener("beforeunload", wipeAll, { once:true });
|
| 342 |
document.addEventListener("visibilitychange", ()=>{
|