stat2025 commited on
Commit
6ef90a1
·
verified ·
1 Parent(s): 5e37d3b

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +190 -275
app.js CHANGED
@@ -1,5 +1,4 @@
1
  const ACCESS_PASSWORDS = ["12345", "12345678"];
2
- const ACCESS_KEY = "ticket_gate_ok_v1";
3
 
4
  const EXPORT_COLUMNS = [
5
  "التصنيف","نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
@@ -43,228 +42,193 @@ const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","
43
 
44
  function normalizeText(s){
45
  if(typeof s!=="string") return "";
46
- return s
47
- .replace(/\r\n/g,"\n")
48
- .replace(/[\u200f\u200e\u202a-\u202e\u2066-\u2069\u00a0]/g," ")
49
- .replace(/[٠-٩]/g,d=>arabicDigitsMap[d])
50
- .replace(/[ــ]+/g,"")
51
- .replace(/[ \t]+\n/g,"\n")
52
- .replace(/\n{3,}/g,"\n\n")
53
- .trim();
54
- }
55
- function lettersOnly(ar){ return (ar||"").replace(/[^A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]/g,"").replace(/\s{2,}/g," ").trim() }
56
- function alnumAr(s){ return (s||"").replace(/[^0-9A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s\-\._/]/g,"").replace(/\s{2,}/g," ").trim() }
57
- function digitsOnly(s){ return (s||"").replace(/\D+/g,"") }
58
 
59
  const LABEL_FIXES = [
60
- [/(^|\n)\s*نوع\s*المشكله/gi, "$1نوع المشكلة"],
61
- [/(^|\n)\s*وقت\s*حدوث\s*المشكله/gi, "$1وقت حدوث المشكلة"],
62
- [/(^|\n)\s*اسم\s*صاحب\s*المشكله/gi, "$1اسم صاحب المشكلة"],
63
- [/(^|\n)\s*رقم\s*الهويه/gi, "$1رقم الهوية"],
64
- [/(^|\n)\s*المنطقه/gi, "$1المنطقة"],
65
- [/(^|\n)\s*اسم\s*المنطقة/gi, "$1المنطقة"],
66
- [/(^|\n)\س*اسم\s*المسح/gi, "$1المسح"],
67
- [/(^|\n)\s*الهاتف/gi, "$1رقم الجوال"],
68
- [/(^|\n)\s*جوال/gi, "$1رقم الجوال"]
69
  ];
70
- function fixLabels(s){
71
- let t=s;
72
- LABEL_FIXES.forEach(([re,rep])=> t=t.replace(re,rep));
73
- return t;
74
- }
75
 
76
- const H_MONTHS = ["محرم","صفر","ربيع الأول","ربيع الاول","ربيع الآخر","ربيع الاخر","جمادى الأولى","جمادى الاولى","جمادى الآخرة","جمادى الاخرة","رجب","شعبان","رمضان","شوال","ذو القعدة","ذو القعده","ذو الحجة","ذو الحجه"];
77
  function monthIndexHijri(name){
78
- const i = H_MONTHS.findIndex(m => new RegExp("^"+m+"$","i").test(name.trim()));
79
- if(i<0) return -1;
80
- const map = {0:1,1:2,2:3,3:3,4:4,5:4,6:5,7:5,8:6,9:6,10:7,11:8,12:9,13:10,14:11,15:11,16:12,17:12};
81
- return map[i] || -1;
82
  }
83
  function hijriToGregorian(hy,hm,hd){
84
- const jd = Math.floor((11*hy+3)/30) + 354*hy + 30*hm - Math.floor((hm-1)/2) + hd + 1948440 - 385;
85
- let l = jd + 68569;
86
- let n = Math.floor(4*l/146097); l = l - Math.floor((146097*n + 3)/4);
87
- let i = Math.floor(4000*(l+1)/1461001); l = l - Math.floor(1461*i/4) + 31;
88
- let j = Math.floor(80*l/2447); const d = l - Math.floor(2447*j/80);
89
- l = Math.floor(j/11); const m = j + 2 - 12*l; const y = 100*(n-49) + i + l;
90
- return [y,m,d];
91
  }
92
  function detectHijriDate(str){
93
- const t = normalizeText(str);
94
- let m = t.match(/(\d{1,2})\s+([^\s]+)\s+(\d{3,4})\s*(هـ|ه|هجري)?/i);
95
- if(m){
96
- const d = +m[1];
97
- const hm = monthIndexHijri(m[2]);
98
- const y = +m[3];
99
- if(hm>=1 && hm<=12) return {hy:y, hm:hm, hd:d};
100
- }
101
- m = t.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{3,4})\s*(هـ|ه|هجري)/i);
102
- if(m) return {hy:+m[3], hm:+m[2], hd:+m[1]};
103
  return null;
104
  }
105
  function isTimeOnly(t){
106
- const a = /(^|\s)\d{1,2}\s*(?:[:٫\.\-]\d{2})\s*(?:ص|صباح(?:اً|ا)?|am|م|مساء|pm)?($|\s)/i.test(t);
107
- const b = /(^|\s)\d{1,2}\s*(?:ص|صباح(?:اً|ا)?|am|م|مساء|pm)($|\s)/i.test(t);
108
- return a || b;
109
  }
110
  function normalizeDateOnly(raw){
111
- const t = normalizeText(raw);
112
- const hj = detectHijriDate(t);
113
- if(isTimeOnly(t) && !hj && !/(\d{3,4}).(\d{1,2}).(\d{1,2})/.test(t)) return "";
114
- if(hj){
115
- const [gy,gm,gd] = hijriToGregorian(hj.hy, hj.hm, hj.hd);
116
- return `${String(gy).padStart(4,"0")}-${String(gm).padStart(2,"0")}-${String(gd).padStart(2,"0")}`;
117
- }
118
- const m = t.match(/(\d{1,4})[\/\-](\d{1,2})[\/\-](\d{1,4})/);
119
  if(m){
120
- let a=+m[1], b=+m[2], c=+m[3], y, mo, d;
121
- if(String(m[1]).length===4){ y=a; mo=b; d=c; }
122
- else if(String(m[3]).length===4){ y=c; mo=b; d=a; }
123
- else if(a>31){ y=a; mo=b; d=c; }
124
- else if(c>31){ y=c; mo=b; d=a; }
125
- else { y=c; mo=b; d=a; }
126
- if(y<100) y+=2000;
127
- if(mo>12 && d<=12){ const tmp=mo; mo=d; d=tmp; }
128
- if(mo<1||mo>12||d<1||d>31) return "";
129
- return `${String(y).padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
130
  }
131
  return t;
132
  }
133
 
134
- function findStartsByLabels(text, labels){
135
- const lblRe = labels.map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
136
- const re = new RegExp(`(^|\\n)\\s*(?:[-–—\\*•]+|\\d+[\\)\\.]\\s*)?\\s*(?:${lblRe})(?:\\s*[::]|\\s+)`,"gi");
137
- const idxs=[]; let m; while((m=re.exec(text))){ idxs.push(m.index + (m[1]?m[1].length:0)); }
138
- return idxs;
139
  }
140
- function findAfterLabel(text, labels){
141
- const hay = "\n" + normalizeText(text) + "\n";
142
  for(const rawLbl of labels){
143
- const lbl = rawLbl.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
144
- let m = hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s*[::]\\s*([^\\n]+)`, "i")); if(m) return m[1].trim();
145
- m = hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s+([^\\n]+)`, "i")); if(m) return m[1].trim();
146
  }
147
- return "";
148
  }
149
- function findBlockAfterLabel(text, labels, allLabels = START_LABELS){
150
- const hay = "\n" + normalizeText(text) + "\n";
151
- const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
152
- const lblAlt = labels.map(esc).join("|");
153
- const allAlt = allLabels.map(esc).join("|");
154
- const re = new RegExp(
155
- `(?:^|\\n)\\s*(?:${lblAlt})\\s*(?::|:|\\s)\\s*([\\s\\S]*?)(?=\\n\\s*(?:${allAlt})\\s*(?::|:|\\s)|$)`,
156
- "i"
157
- );
158
- const m = hay.match(re);
159
- return m ? m[1].trim() : "";
160
  }
161
 
162
  function splitTickets(raw){
163
- const text = normalizeText(fixLabels(raw));
164
- if(!text) return [];
165
- if(TICKET_SEP.test(text)) return text.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
166
- const niu = findStartsByLabels(text, ["نوع المشكلة","نوع المشكله"]).sort((a,b)=>a-b);
167
  if(niu.length>=2){
168
- const parts=[];
169
- for(let i=0;i<niu.length;i++){
170
- const s=niu[i]; const e=i+1<niu.length?niu[i+1]:text.length;
171
- const slice=text.slice(s,e).trim(); if(slice) parts.push(slice);
172
- }
173
- if(parts.length) return parts;
174
  }
175
- return [text];
176
  }
177
 
178
  function extractFields(ticketText){
179
- const text = normalizeText(fixLabels(ticketText));
180
- const out = {"نوع المشكلة":"","وقت حدوث المشكلة":"","اسم صاحب المشكلة":"","رقم الهوية":"","رقم الجهاز":"","رقم الجوال":"","المسح":"","المنطقة":""};
181
 
182
- let v = findBlockAfterLabel(text, FIELD_ALIASES["نوع المشكلة"], START_LABELS);
183
- if(!v) v = findAfterLabel(text, FIELD_ALIASES["نوع المشكلة"]);
184
- if(v) out["نوع المشكلة"] = normalizeText(v);
185
 
186
- v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
187
- if(v) out["وقت حدوث المشكلة"] = normalizeDateOnly(v);
188
 
189
- v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
190
- if(v) out["اسم صاحب المشكلة"] = v;
191
 
192
- v = findAfterLabel(text, FIELD_ALIASES["رقم الهوية"]);
193
- if(v) out["رقم الهوية"] = digitsOnly(v);
194
- if(!out["رقم الهوية"]){ const m=text.match(/(?:^|\D)((?:1|2)\d{9})(?:\D|$)/); if(m) out["رقم الهوية"]=m[1]; }
195
 
196
- v = findAfterLabel(text, FIELD_ALIASES["رقم الجهاز"]);
197
- if(v) out["رقم الجهاز"] = digitsOnly(v);
198
- if(!out["رقم الجهاز"]){ const m=text.match(/(?:^|\D)(\d{5,20})(?:\D|$)/); if(m) out["رقم الجهاز"]=m[1]; }
199
 
200
- v = findAfterLabel(text, FIELD_ALIASES["رقم الجوال"]);
201
- if(v) out["رقم الجوال"] = digitsOnly(v);
202
- if(!out["رقم الجوال"]){ const m=text.match(/(?:^|\D)(05\d{7,})(?:\D|$)/); if(m) out["رقم الجوال"]=m[1]; }
203
 
204
- v = findAfterLabel(text, FIELD_ALIASES["المسح"]);
205
- if(v) out["المسح"] = alnumAr(v);
206
 
207
- v = findAfterLabel(text, FIELD_ALIASES["المنطقة"]);
208
- if(v) out["المنطقة"] = lettersOnly(v);
209
 
210
  return out;
211
  }
212
 
213
- function classifyTicket(text, fields){
214
- const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
215
  for(const label of CLASS_PRIORITY){
216
- const kws = CLASS_RULES[label] || [];
217
  for(const kw of kws){
218
- const needle = normalizeText(kw).toLowerCase();
219
- if(needle && hay.includes(needle)) return label;
220
  }
221
  }
222
- return "استفسار";
223
  }
224
  function catClass(label){
225
- if(/تسجيل دخول/.test(label)) return "login";
226
- if(/الشبكة/.test(label)) return "network";
227
- if(/الاستمارة/.test(label)) return "form";
228
- if(/النسخة|تحديث/.test(label)) return "update";
229
- if(/أجهزة/.test(label)) return "device";
230
- return "default";
231
  }
232
 
233
- function parseTicketsWithExtras(raw, agentName, defaultRegion){
234
- const regionChosen = (defaultRegion || "").toString();
235
  return splitTickets(raw||"").map(t=>{
236
- const f = extractFields(t);
237
- const cls = classifyTicket(t, f);
238
- const region = regionChosen ? regionChosen : (f["المنطقة"] || "");
239
- return {
240
- "التصنيف": cls,
241
- "نوع المشكلة": f["نوع المشكلة"] || "",
242
- "وقت حدوث المشكلة": f["وقت حدوث المشكلة"] || "",
243
- "اسم صاحب المشكلة": f["اسم صاحب المشكلة"] || "",
244
- "رقم الهوية": f["رقم الهوية"] || "",
245
- "رقم الجهاز": f["رقم الجهاز"] || "",
246
- "رقم الجوال": f["رقم الجوال"] || "",
247
- "المسح": f["المسح"] || "",
248
- "المنطقة": region,
249
- "اسم الدعم الفني": agentName || "",
250
- "الحالة": "تم الحل"
251
  };
252
  });
253
  }
254
 
255
  function buildTable(rows){
256
- const theadRow = document.getElementById("theadRow");
257
- const tbody = document.getElementById("tbody");
258
- theadRow.innerHTML = "";
259
- EXPORT_COLUMNS.forEach(col=>{ const th=document.createElement("th"); th.textContent=col; theadRow.appendChild(th); });
260
- tbody.innerHTML = "";
261
  rows.forEach(r=>{
262
  const tr=document.createElement("tr");
263
  EXPORT_COLUMNS.forEach(col=>{
264
  const td=document.createElement("td");
265
  if(col==="التصنيف"){
266
  const span=document.createElement("span");
267
- const type = catClass(r[col]||"");
268
  span.className=`cat ${type}`;
269
  span.textContent=r[col]||"";
270
  td.appendChild(span);
@@ -333,13 +297,8 @@ function validateCells(){
333
  });
334
  });
335
  const warn=document.getElementById("warn");
336
- if(missing>0){
337
- warn.hidden=false;
338
- warn.textContent=`هناك ${missing} حقول مطلوبة فارغة أو غير صحيحة. يرجى إكمالها.`;
339
- }else{
340
- warn.hidden=true;
341
- warn.textContent="";
342
- }
343
  }
344
 
345
  document.addEventListener("input",e=>{
@@ -353,34 +312,19 @@ function toast(msg){
353
  const t=document.getElementById("toast");
354
  t.textContent=msg;
355
  t.hidden=false;
356
- t.classList.remove("show"); void t.offsetWidth; t.classList.add("show");
357
- setTimeout(()=>{ t.hidden=true; },2000);
358
  }
359
 
360
  async function exportExcel(){
361
- const TEMPLATE_HEADERS=[
362
- "التصنيف","نوع المشكلة","المنطقة","اسم المسح","اسم المشغل",
363
- "رقم الجوال","رقم الهوية ID","رقم الجهاز","وقت حدوث المشكلة","الحالة","اسم الدعم الفني"
364
- ];
365
  const rows=readTable();
366
- if(!rows.length){toast("لا يوجد بيانات لتصديرها.");return;}
367
- const mapRow=r=>({
368
- "التصنيف":r["التصنيف"]||"",
369
- "نوع المشكلة":r["نوع المشكلة"]||"",
370
- "المنطقة":r["المنطقة"]||"",
371
- "اسم المسح":r["المسح"]||"",
372
- "اسم المشغل":r["اسم صاحب المشكلة"]||"",
373
- "رقم الجوال":(r["رقم الجوال"]||"").toString(),
374
- "رقم الهوية ID":(r["رقم الهوية"]||"").toString(),
375
- "رقم الجهاز":(r["رقم الجهاز"]||"").toString(),
376
- "وقت حدوث المشكلة":r["وقت حدوث المشكلة"]||"",
377
- "الحالة":r["الحالة"]||"تم الحل",
378
- "اسم الدعم الفني":r["اسم الدعم الفني"]||""
379
- });
380
  const wb=new ExcelJS.Workbook();
381
  const ws=wb.addWorksheet("التذاكر",{views:[{rightToLeft:true}]});
382
  const colWidths=[16,18,16,18,20,18,18,18,20,14,18];
383
- TEMPLATE_HEADERS.forEach((h,i)=> ws.getColumn(i+1).width=colWidths[i]||18);
384
  ws.addRow(TEMPLATE_HEADERS);
385
  const headerRow=ws.getRow(1);
386
  headerRow.height=24;
@@ -388,26 +332,21 @@ async function exportExcel(){
388
  cell.font={bold:true,color:{argb:"FFFFFFFF"}};
389
  cell.alignment={horizontal:"center",vertical:"middle"};
390
  cell.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FF4137A8"}};
391
- cell.border={
392
- top:{style:"thin",color:{argb:"FFCDD2E1"}},
393
- bottom:{style:"thin",color:{argb:"FFCDD2E1"}},
394
- left:{style:"thin",color:{argb:"FFE5E7EB"}},
395
- right:{style:"thin",color:{argb:"FFE5E7EB"}}
396
- };
397
  });
398
  const toTextCols=new Set(["رقم الجوال","رقم الهوية ID","رقم الجهاز"]);
399
  const rawRows=readTable();
400
  rawRows.forEach((r,idx)=>{
401
  const m=mapRow(r);
402
- const vals=TEMPLATE_HEADERS.map(h => (m[h] ?? ""));
403
  const row=ws.addRow(vals);
404
  row.alignment={horizontal:"center",vertical:"middle"};
405
  const even=(idx%2)===1;
406
  row.eachCell((cell,colNumber)=>{
407
  cell.border={top:{style:"thin",color:{argb:"FFE5E7EB"}},bottom:{style:"thin",color:{argb:"FFE5E7EB"}},left:{style:"thin",color:{argb:"FFE5E7EB"}},right:{style:"thin",color:{argb:"FFE5E7EB"}}};
408
- if(even) cell.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FFF5F8FF"}};
409
  const header=TEMPLATE_HEADERS[colNumber-1];
410
- if(toTextCols.has(header)) cell.value=String(cell.value ?? "");
411
  });
412
  });
413
  const ts=new Date().toISOString().replace(/\D/g,"").slice(0,14);
@@ -415,37 +354,26 @@ async function exportExcel(){
415
  const buffer=await wb.xlsx.writeBuffer();
416
  const blob=new Blob([buffer],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
417
  const file=new File([blob],filename,{type:blob.type});
418
- if(navigator.canShare&&navigator.canShare({files:[file]})){
419
- try{await navigator.share({files:[file],title:"ملف التذاكر"});toast("تمت المشاركة/الحفظ.");return;}catch(e){}
420
- }
421
  const url=URL.createObjectURL(blob);
422
- const a=document.createElement("a"); a.href=url; a.download=filename; document.body.appendChild(a); a.click(); a.remove();
423
  setTimeout(()=>URL.revokeObjectURL(url),1000);
424
  toast("تم تنزيل الملف بتنسيق القالب.");
425
  }
426
 
427
  async function copyToClipboardTSV(){
428
  const rows=readTable();
429
- if(!rows.length){toast("لا يوجد بيانات لنسخها.");return;}
430
  const textCols=new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
431
  const header=EXPORT_COLUMNS.join("\t");
432
- const body=rows.map(r =>
433
- EXPORT_COLUMNS.map(c => {
434
- let v=(r[c]??"").toString().replace(/\t/g," ");
435
- if(textCols.has(c) && v && /^[0-9]+$/.test(v)) v="'"+v;
436
- return v;
437
- }).join("\t")
438
- ).join("\r\n");
439
  const tsv="\uFEFF"+header+"\r\n"+body;
440
- try{ await navigator.clipboard.writeText(tsv); toast("تم النسخ — الصق/ي مباشرة في Excel."); }
441
- catch(e){
442
- const ta=document.createElement("textarea");
443
- ta.value=tsv; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
444
- toast("تم النسخ — الصق/ي مباشرة في Excel.");
445
  }
446
  }
447
 
448
- const SAMPLE = `نوع المشكلة: لا استطيع اكمال الاستمارة بسبب تعليق عند الحفظ
449
  وقت حدوث المشكلة: 1446/09/10 هـ
450
  اسم صاحب المشكلة: نوف الناصر
451
  رقم الهوية: 1234567890
@@ -454,27 +382,17 @@ const SAMPLE = `نوع المشكلة: لا استطيع اكمال الاستم
454
  المسح: الخبر
455
  المنطقة: الشرقية`;
456
 
457
- const STATE_KEY="ticketParserState_v10_15";
458
- const ALL_STATE_KEYS=[
459
- "ticketParserState_v8","ticketParserState_v9","ticketParserState_v10",
460
- "ticketParserState_v10_1","ticketParserState_v10_2","ticketParserState_v10_3",
461
- "ticketParserState_v10_5","ticketParserState_v10_6","ticketParserState_v10_7",
462
- "ticketParserState_v10_8","ticketParserState_v10_9","ticketParserState_v10_10",
463
- "ticketParserState_v10_11","ticketParserState_v10_12","ticketParserState_v10_13",
464
- "ticketParserState_v10_14","ticketParserState_v10_15"
465
- ];
466
 
467
- function ensureColumns(rows, agentName, defaultRegion){
468
- if(!Array.isArray(rows)) return rows||[];
469
  return rows.map(r=>{
470
  const out={...r};
471
- if(!("التصنيف" in out) || !out["التصنيف"]){
472
- const fakeText=Object.values(out).join("\n");
473
- out["التصنيف"]=classifyTicket(fakeText, out);
474
- }
475
- if(!("اسم الدعم الفني" in out)) out["اسم الدعم الفني"]=agentName||out["اسم الدعم الفني"]||"";
476
- if(!("الحالة" in out) || !out["الحالة"]) out["الحالة"]="تم الحل";
477
- if(defaultRegion) out["المنطقة"]=defaultRegion;
478
  return out;
479
  });
480
  }
@@ -485,24 +403,24 @@ function saveState(){
485
  const agent=document.getElementById("agentName")?.value||"";
486
  const region=document.getElementById("regionDefault")?.value||"";
487
  const rows=readTable();
488
- localStorage.setItem(STATE_KEY, JSON.stringify({raw,agent,region,rows,theme:document.body.classList.contains('dark')?'dark':'light'}));
489
  }catch{}
490
  }
491
 
492
  function loadState(){
493
  try{
494
  const s=localStorage.getItem(STATE_KEY);
495
- if(!s) return false;
496
- let {raw,agent,region,rows,theme}=JSON.parse(s);
497
- const rawEl=document.getElementById("raw"); if(typeof raw==="string"&&rawEl) rawEl.value=raw;
498
- const agentEl=document.getElementById("agentName"); if(typeof agent==="string"&&agentEl) agentEl.value=agent;
499
- const regionEl=document.getElementById("regionDefault"); if(typeof region==="string"&&regionEl) regionEl.value=region;
500
- if(theme==='dark') document.body.classList.add('dark');
501
  updateThemeLabel();
502
  rows=ensureColumns(rows,agent,region);
503
- if(Array.isArray(rows)&&rows.length){ buildTable(rows); validateCells(); updateBadge(rows.length); setButtonsEnabled(true); }
504
  return true;
505
- }catch{ return false; }
506
  }
507
 
508
  function clearAll(){
@@ -510,29 +428,29 @@ function clearAll(){
510
  const tbody=document.getElementById("tbody");
511
  const agentEl=document.getElementById("agentName");
512
  const regionEl=document.getElementById("regionDefault");
513
- if(rawEl) rawEl.value="";
514
- if(tbody) tbody.innerHTML="";
515
- if(agentEl) agentEl.value="";
516
- if(regionEl) regionEl.value="";
517
- updateBadge(0); setButtonsEnabled(false);
518
  document.getElementById("warn").hidden=true;
519
- try{ ALL_STATE_KEYS.forEach(k=>localStorage.removeItem(k)); }catch{}
520
  toast("تم مسح كل البيانات والتخزين.");
521
  }
522
 
523
  function mergeDuplicatesRows(rows){
524
- if(!rows.length) return rows;
525
  const map=new Map();
526
  rows.forEach(r=>{
527
  const key=[r["رقم الهوية"]||"",r["رقم الجهاز"]||"",r["رقم الجوال"]||"",r["وقت حدوث المشكلة"]||"",(r["نوع المشكلة"]||"").slice(0,40)].join("|");
528
- if(!map.has(key)) map.set(key,r);
529
  });
530
- return [...map.values()];
531
  }
532
 
533
  function updateThemeLabel(){
534
  const btn=document.getElementById("btn-theme");
535
- if(!btn) return;
536
  btn.textContent=document.body.classList.contains("dark")?"☀️ وضع نهار":"🌙 وضع ليلي";
537
  }
538
 
@@ -545,10 +463,7 @@ function hideGate(){
545
  }
546
  function tryUnlock(){
547
  const p=document.getElementById("lockPass").value||"";
548
- if(ACCESS_PASSWORDS.includes(p)){
549
- localStorage.setItem(ACCESS_KEY,"1");
550
- hideGate();
551
- }else{
552
  const m=document.getElementById("lockMsg");
553
  m.hidden=false;
554
  setTimeout(()=>m.hidden=true,1500);
@@ -569,48 +484,48 @@ function init(){
569
 
570
  rawEl.placeholder=SAMPLE;
571
 
572
- if(localStorage.getItem(ACCESS_KEY)==="1") hideGate(); else showGate();
573
 
574
  loadState();
575
 
576
- parseBtn.addEventListener("click", ()=>{
577
  const raw=(rawEl.value||"").trim();
578
- if(!raw){ toast("فضلاً الصق/ي تذاكر أولاً."); return; }
579
  const cleaned=normalizeText(fixLabels(raw));
580
  const agent=agentEl.value||"";
581
  const defRegion=regionEl.value||"";
582
  let rows=parseTicketsWithExtras(cleaned,agent,defRegion);
583
  rows=mergeDuplicatesRows(rows);
584
- buildTable(rows); validateCells();
585
- updateBadge(rows.length); setButtonsEnabled(rows.length>0);
586
  saveState();
587
  toast(`تم استخراج ${rows.length} تذكرة.`);
588
  });
589
 
590
- exportBtn.addEventListener("click", exportExcel);
591
- copyBtn.addEventListener("click", copyToClipboardTSV);
592
- clearBtn.addEventListener("click", clearAll);
593
 
594
- themeBtn.addEventListener("click", ()=>{
595
  document.body.classList.toggle("dark");
596
  updateThemeLabel();
597
  saveState();
598
  });
599
 
600
- rawEl.addEventListener("input", saveState);
601
- agentEl.addEventListener("input", saveState);
602
- regionEl.addEventListener("change", saveState);
603
 
604
- document.addEventListener("keydown",(e)=>{
605
  const ctrl=e.ctrlKey||e.metaKey;
606
- if(ctrl && e.key==="Enter"){ e.preventDefault(); parseBtn.click(); }
607
- else if(ctrl && e.key.toLowerCase()==="e"){ e.preventDefault(); exportBtn.click(); }
608
- else if(ctrl && e.shiftKey && e.key.toLowerCase()==="c"){ e.preventDefault(); copyBtn.click(); }
609
- else if(e.key==="Escape"){ e.preventDefault(); clearAll(); }
610
  });
611
 
612
- lockBtn.addEventListener("click", tryUnlock);
613
- lockPass.addEventListener("keydown", e=>{ if(e.key==="Enter") tryUnlock(); });
614
 
615
  setButtonsEnabled(!!document.getElementById("tbody")?.children.length);
616
  updateThemeLabel();
 
1
  const ACCESS_PASSWORDS = ["12345", "12345678"];
 
2
 
3
  const EXPORT_COLUMNS = [
4
  "التصنيف","نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
 
42
 
43
  function normalizeText(s){
44
  if(typeof s!=="string") return "";
45
+ return s.replace(/\r\n/g,"\n").replace(/[\u200f\u200e\u202a-\u202e\u2066-\u2069\u00a0]/g," ").replace(/[٠-٩]/g,d=>arabicDigitsMap[d]).replace(/[ــ]+/g,"").replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n").trim();
46
+ }
47
+ function lettersOnly(ar){return(ar||"").replace(/[^A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]/g,"").replace(/\s{2,}/g," ").trim()}
48
+ function alnumAr(s){return(s||"").replace(/[^0-9A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s\-\._/]/g,"").replace(/\s{2,}/g," ").trim()}
49
+ function digitsOnly(s){return(s||"").replace(/\D+/g,"")}
 
 
 
 
 
 
 
50
 
51
  const LABEL_FIXES = [
52
+ [/(^|\n)\s*نوع\s*المشكله/gi,"$1نوع المشكلة"],
53
+ [/(^|\n)\s*وقت\s*حدوث\s*المشكله/gi,"$1وقت حدوث المشكلة"],
54
+ [/(^|\n)\s*اسم\s*صاحب\s*المشكله/gi,"$1اسم صاحب المشكلة"],
55
+ [/(^|\n)\s*رقم\s*الهويه/gi,"$1رقم الهوية"],
56
+ [/(^|\n)\s*المنطقه/gi,"$1المنطقة"],
57
+ [/(^|\n)\s*اسم\s*المنطقة/gi,"$1المنطقة"],
58
+ [/(^|\n)\s*اسم\s*المسح/gi,"$1المسح"],
59
+ [/(^|\n)\s*الهاتف/gi,"$1رقم الجوال"],
60
+ [/(^|\n)\s*جوال/gi,"$1رقم الجوال"]
61
  ];
62
+ function fixLabels(s){let t=s;LABEL_FIXES.forEach(([re,rep])=>t=t.replace(re,rep));return t}
 
 
 
 
63
 
64
+ const H_MONTHS=["محرم","صفر","ربيع الأول","ربيع الاول","ربيع الآخر","ربيع الاخر","جمادى الأولى","جمادى الاولى","جمادى الآخرة","جمادى الاخرة","رجب","شعبان","رمضان","شوال","ذو القعدة","ذو القعده","ذو الحجة","ذو الحجه"];
65
  function monthIndexHijri(name){
66
+ const i=H_MONTHS.findIndex(m=>new RegExp("^"+m+"$","i").test(name.trim()));
67
+ if(i<0)return-1;
68
+ const map={0:1,1:2,2:3,3:3,4:4,5:4,6:5,7:5,8:6,9:6,10:7,11:8,12:9,13:10,14:11,15:11,16:12,17:12};
69
+ return map[i]||-1;
70
  }
71
  function hijriToGregorian(hy,hm,hd){
72
+ const jd=Math.floor((11*hy+3)/30)+354*hy+30*hm-Math.floor((hm-1)/2)+hd+1948440-385;
73
+ let l=jd+68569;
74
+ let n=Math.floor(4*l/146097);l=l-Math.floor((146097*n+3)/4);
75
+ let i=Math.floor(4000*(l+1)/1461001);l=l-Math.floor(1461*i/4)+31;
76
+ let j=Math.floor(80*l/2447);const d=l-Math.floor(2447*j/80);
77
+ l=Math.floor(j/11);const m=j+2-12*l;const y=100*(n-49)+i+l;
78
+ return[y,m,d];
79
  }
80
  function detectHijriDate(str){
81
+ const t=normalizeText(str);
82
+ let m=t.match(/(\d{1,2})\s+([^\s]+)\s+(\d{3,4})\s*(هـ|ه|هجري)?/i);
83
+ if(m){const d=+m[1];const hm=monthIndexHijri(m[2]);const y=+m[3];if(hm>=1&&hm<=12)return{hy:y,hm:hm,hd:d}}
84
+ m=t.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{3,4})\s*(هـ|ه|هجري)/i);
85
+ if(m)return{hy:+m[3],hm:+m[2],hd:+m[1]};
 
 
 
 
 
86
  return null;
87
  }
88
  function isTimeOnly(t){
89
+ const a=/(^|\s)\d{1,2}\s*(?:[:٫\.\-]\d{2})\s*(?:ص|صباح(?:اً|ا)?|am|م|مساء|pm)?($|\s)/i.test(t);
90
+ const b=/(^|\s)\d{1,2}\s*(?:ص|صباح(?:اً|ا)?|am|م|مساء|pm)($|\s)/i.test(t);
91
+ return a||b;
92
  }
93
  function normalizeDateOnly(raw){
94
+ const t=normalizeText(raw);
95
+ const hj=detectHijriDate(t);
96
+ if(isTimeOnly(t)&&!hj&&!/(\d{3,4}).(\d{1,2}).(\d{1,2})/.test(t))return"";
97
+ if(hj){const[gy,gm,gd]=hijriToGregorian(hj.hy,hj.hm,hj.hd);return`${String(gy).padStart(4,"0")}-${String(gm).padStart(2,"0")}-${String(gd).padStart(2,"0")}`}
98
+ const m=t.match(/(\d{1,4})[\/\-](\d{1,2})[\/\-](\d{1,4})/);
 
 
 
99
  if(m){
100
+ let a=+m[1],b=+m[2],c=+m[3],y,mo,d;
101
+ if(String(m[1]).length===4){y=a;mo=b;d=c}
102
+ else if(String(m[3]).length===4){y=c;mo=b;d=a}
103
+ else if(a>31){y=a;mo=b;d=c}
104
+ else if(c>31){y=c;mo=b;d=a}
105
+ else{y=c;mo=b;d=a}
106
+ if(y<100)y+=2000;
107
+ if(mo>12&&d<=12){const tmp=mo;mo=d;d=tmp}
108
+ if(mo<1||mo>12||d<1||d>31)return"";
109
+ return`${String(y).padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`
110
  }
111
  return t;
112
  }
113
 
114
+ function findStartsByLabels(text,labels){
115
+ const lblRe=labels.map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
116
+ const re=new RegExp(`(^|\\n)\\s*(?:[-–—\\*•]+|\\d+[\\)\\.]\\s*)?\\s*(?:${lblRe})(?:\\s*[::]|\\s+)`,"gi");
117
+ const idxs=[];let m;while((m=re.exec(text))){idxs.push(m.index+(m[1]?m[1].length:0))}return idxs;
 
118
  }
119
+ function findAfterLabel(text,labels){
120
+ const hay="\n"+normalizeText(text)+"\n";
121
  for(const rawLbl of labels){
122
+ const lbl=rawLbl.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
123
+ let m=hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s*[::]\\s*([^\\n]+)`,"i"));if(m)return m[1].trim();
124
+ m=hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s+([^\\n]+)`,"i"));if(m)return m[1].trim();
125
  }
126
+ return"";
127
  }
128
+ function findBlockAfterLabel(text,labels,allLabels=START_LABELS){
129
+ const hay="\n"+normalizeText(text)+"\n";
130
+ const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
131
+ const lblAlt=labels.map(esc).join("|");
132
+ const allAlt=allLabels.map(esc).join("|");
133
+ const re=new RegExp(`(?:^|\\n)\\s*(?:${lblAlt})\\s*(?::|:|\\s)\\s*([\\s\\S]*?)(?=\\n\\s*(?:${allAlt})\\s*(?::|:|\\s)|$)`,"i");
134
+ const m=hay.match(re);
135
+ return m?m[1].trim():"";
 
 
 
136
  }
137
 
138
  function splitTickets(raw){
139
+ const text=normalizeText(fixLabels(raw));
140
+ if(!text)return[];
141
+ if(TICKET_SEP.test(text))return text.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
142
+ const niu=findStartsByLabels(text,["نوع المشكلة","نوع المشكله"]).sort((a,b)=>a-b);
143
  if(niu.length>=2){
144
+ const parts=[];for(let i=0;i<niu.length;i++){const s=niu[i];const e=i+1<niu.length?niu[i+1]:text.length;const slice=text.slice(s,e).trim();if(slice)parts.push(slice)}
145
+ if(parts.length)return parts;
 
 
 
 
146
  }
147
+ return[text];
148
  }
149
 
150
  function extractFields(ticketText){
151
+ const text=normalizeText(fixLabels(ticketText));
152
+ const out={"نوع المشكلة":"","وقت حدوث المشكلة":"","اسم صاحب المشكلة":"","رقم الهوية":"","رقم الجهاز":"","رقم الجوال":"","المسح":"","المنطقة":""};
153
 
154
+ let v=findBlockAfterLabel(text,FIELD_ALIASES["نوع المشكلة"],START_LABELS);
155
+ if(!v)v=findAfterLabel(text,FIELD_ALIASES["نوع المشكلة"]);
156
+ if(v)out["نوع المشكلة"]=normalizeText(v);
157
 
158
+ v=findAfterLabel(text,FIELD_ALIASES["وقت حدوث المشكلة"]);
159
+ if(v)out["وقت حدوث المشكلة"]=normalizeDateOnly(v);
160
 
161
+ v=findAfterLabel(text,FIELD_ALIASES["اسم صاحب المشكلة"]);
162
+ if(v)out["اسم صاحب المشكلة"]=v;
163
 
164
+ v=findAfterLabel(text,FIELD_ALIASES["رقم الهوية"]);
165
+ if(v)out["رقم الهوية"]=digitsOnly(v);
166
+ if(!out["رقم الهوية"]){const m=text.match(/(?:^|\D)((?:1|2)\d{9})(?:\D|$)/);if(m)out["رقم الهوية"]=m[1]}
167
 
168
+ v=findAfterLabel(text,FIELD_ALIASES["رقم الجهاز"]);
169
+ if(v)out["رقم الجهاز"]=digitsOnly(v);
170
+ if(!out["رقم الجهاز"]){const m=text.match(/(?:^|\D)(\d{5,20})(?:\D|$)/);if(m)out["رقم الجهاز"]=m[1]}
171
 
172
+ v=findAfterLabel(text,FIELD_ALIASES["رقم الجوال"]);
173
+ if(v)out["رقم الجوال"]=digitsOnly(v);
174
+ if(!out["رقم الجوال"]){const m=text.match(/(?:^|\D)(05\d{7,})(?:\D|$)/);if(m)out["رقم الجوال"]=m[1]}
175
 
176
+ v=findAfterLabel(text,FIELD_ALIASES["المسح"]);
177
+ if(v)out["المسح"]=alnumAr(v);
178
 
179
+ v=findAfterLabel(text,FIELD_ALIASES["المنطقة"]);
180
+ if(v)out["المنطقة"]=lettersOnly(v);
181
 
182
  return out;
183
  }
184
 
185
+ function classifyTicket(text,fields){
186
+ const hay=normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
187
  for(const label of CLASS_PRIORITY){
188
+ const kws=CLASS_RULES[label]||[];
189
  for(const kw of kws){
190
+ const needle=normalizeText(kw).toLowerCase();
191
+ if(needle&&hay.includes(needle))return label;
192
  }
193
  }
194
+ return"استفسار";
195
  }
196
  function catClass(label){
197
+ if(/تسجيل دخول/.test(label))return"login";
198
+ if(/الشبكة/.test(label))return"network";
199
+ if(/الاستمارة/.test(label))return"form";
200
+ if(/النسخة|تحديث/.test(label))return"update";
201
+ if(/أجهزة/.test(label))return"device";
202
+ return"default";
203
  }
204
 
205
+ function parseTicketsWithExtras(raw,agentName,defaultRegion){
206
+ const regionChosen=(defaultRegion||"").toString();
207
  return splitTickets(raw||"").map(t=>{
208
+ const f=extractFields(t);
209
+ const cls=classifyTicket(t,f);
210
+ const region=regionChosen?regionChosen:(f["المنطقة"]||"");
211
+ return{
212
+ "التصنيف":cls,"نوع المشكلة":f["نوع المشكلة"]||"","وقت حدوث المشكلة":f["وقت حدوث المشكلة"]||"",
213
+ "اسم صاحب المشكلة":f["اسم صاحب المشكلة"]||"","رقم الهوية":f["رقم الهوية"]||"","رقم الجهاز":f["رقم الجهاز"]||"",
214
+ "رقم الجوال":f["رقم الجوال"]||"","المسح":f["المسح"]||"","المنطقة":region,"اسم الدعم الفني":agentName||"","الحالة":"تم الحل"
 
 
 
 
 
 
 
 
215
  };
216
  });
217
  }
218
 
219
  function buildTable(rows){
220
+ const theadRow=document.getElementById("theadRow");
221
+ const tbody=document.getElementById("tbody");
222
+ theadRow.innerHTML="";
223
+ EXPORT_COLUMNS.forEach(col=>{const th=document.createElement("th");th.textContent=col;theadRow.appendChild(th)});
224
+ tbody.innerHTML="";
225
  rows.forEach(r=>{
226
  const tr=document.createElement("tr");
227
  EXPORT_COLUMNS.forEach(col=>{
228
  const td=document.createElement("td");
229
  if(col==="التصنيف"){
230
  const span=document.createElement("span");
231
+ const type=catClass(r[col]||"");
232
  span.className=`cat ${type}`;
233
  span.textContent=r[col]||"";
234
  td.appendChild(span);
 
297
  });
298
  });
299
  const warn=document.getElementById("warn");
300
+ if(missing>0){warn.hidden=false;warn.textContent=`هناك ${missing} حقول مطلوبة فارغة أو غير صحيحة. يرجى إكمالها.`}
301
+ else{warn.hidden=true;warn.textContent=""}
 
 
 
 
 
302
  }
303
 
304
  document.addEventListener("input",e=>{
 
312
  const t=document.getElementById("toast");
313
  t.textContent=msg;
314
  t.hidden=false;
315
+ t.classList.remove("show");void t.offsetWidth;t.classList.add("show");
316
+ setTimeout(()=>{t.hidden=true},2000);
317
  }
318
 
319
  async function exportExcel(){
320
+ const TEMPLATE_HEADERS=["التصنيف","نوع المشكلة","المنطقة","اسم المسح","اسم المشغل","رقم الجوال","رقم الهوية ID","رقم الجهاز","وقت حدوث المشكلة","الحالة","اسم الدعم الفني"];
 
 
 
321
  const rows=readTable();
322
+ if(!rows.length){toast("لا يوجد بيانات لتصديرها.");return}
323
+ const mapRow=r=>({"التصنيف":r["التصنيف"]||"","نوع المشكلة":r["نوع المشكلة"]||"","المنطقة":r["المنطقة"]||"","اسم المسح":r["المسح"]||"","اسم المشغل":r["اسم صاحب المشكلة"]||"","رقم الجوال":(r["رقم الجوال"]||"").toString(),"رقم الهوية ID":(r["رقم الهوية"]||"").toString(),"رقم الجهاز":(r["رقم الجهاز"]||"").toString(),"وقت حدوث المشكلة":r["وقت حدوث المشكلة"]||"","الحالة":r["الحالة"]||"تم الحل","اسم الدعم الفني":r["اسم الدعم الفني"]||""});
 
 
 
 
 
 
 
 
 
 
 
 
324
  const wb=new ExcelJS.Workbook();
325
  const ws=wb.addWorksheet("التذاكر",{views:[{rightToLeft:true}]});
326
  const colWidths=[16,18,16,18,20,18,18,18,20,14,18];
327
+ TEMPLATE_HEADERS.forEach((h,i)=>ws.getColumn(i+1).width=colWidths[i]||18);
328
  ws.addRow(TEMPLATE_HEADERS);
329
  const headerRow=ws.getRow(1);
330
  headerRow.height=24;
 
332
  cell.font={bold:true,color:{argb:"FFFFFFFF"}};
333
  cell.alignment={horizontal:"center",vertical:"middle"};
334
  cell.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FF4137A8"}};
335
+ cell.border={top:{style:"thin",color:{argb:"FFCDD2E1"}},bottom:{style:"thin",color:{argb:"FFCDD2E1"}},left:{style:"thin",color:{argb:"FFE5E7EB"}},right:{style:"thin",color:{argb:"FFE5E7EB"}}};
 
 
 
 
 
336
  });
337
  const toTextCols=new Set(["رقم الجوال","رقم الهوية ID","رقم الجهاز"]);
338
  const rawRows=readTable();
339
  rawRows.forEach((r,idx)=>{
340
  const m=mapRow(r);
341
+ const vals=TEMPLATE_HEADERS.map(h=>(m[h]??""));
342
  const row=ws.addRow(vals);
343
  row.alignment={horizontal:"center",vertical:"middle"};
344
  const even=(idx%2)===1;
345
  row.eachCell((cell,colNumber)=>{
346
  cell.border={top:{style:"thin",color:{argb:"FFE5E7EB"}},bottom:{style:"thin",color:{argb:"FFE5E7EB"}},left:{style:"thin",color:{argb:"FFE5E7EB"}},right:{style:"thin",color:{argb:"FFE5E7EB"}}};
347
+ if(even)cell.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FFF5F8FF"}};
348
  const header=TEMPLATE_HEADERS[colNumber-1];
349
+ if(toTextCols.has(header))cell.value=String(cell.value??"");
350
  });
351
  });
352
  const ts=new Date().toISOString().replace(/\D/g,"").slice(0,14);
 
354
  const buffer=await wb.xlsx.writeBuffer();
355
  const blob=new Blob([buffer],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
356
  const file=new File([blob],filename,{type:blob.type});
357
+ if(navigator.canShare&&navigator.canShare({files:[file]})){try{await navigator.share({files:[file],title:"ملف التذاكر"});toast("تمت المشاركة/الحفظ.");return}catch(e){}}
 
 
358
  const url=URL.createObjectURL(blob);
359
+ const a=document.createElement("a");a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();
360
  setTimeout(()=>URL.revokeObjectURL(url),1000);
361
  toast("تم تنزيل الملف بتنسيق القالب.");
362
  }
363
 
364
  async function copyToClipboardTSV(){
365
  const rows=readTable();
366
+ if(!rows.length){toast("لا يوجد بيانات لنسخها.");return}
367
  const textCols=new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
368
  const header=EXPORT_COLUMNS.join("\t");
369
+ const body=rows.map(r=>EXPORT_COLUMNS.map(c=>{let v=(r[c]??"").toString().replace(/\t/g," ");if(textCols.has(c)&&v&&/^[0-9]+$/.test(v))v="'"+v;return v}).join("\t")).join("\r\n");
 
 
 
 
 
 
370
  const tsv="\uFEFF"+header+"\r\n"+body;
371
+ try{await navigator.clipboard.writeText(tsv);toast("تم النسخ — الصق/ي مباشرة في Excel.")}catch(e){
372
+ const ta=document.createElement("textarea");ta.value=tsv;document.body.appendChild(ta);ta.select();document.execCommand("copy");document.body.removeChild(ta);toast("تم النسخ — الصق/ي مباشرة في Excel.")
 
 
 
373
  }
374
  }
375
 
376
+ const SAMPLE=`نوع المشكلة: لا استطيع اكمال الاستمارة بسبب تعليق عند الحفظ
377
  وقت حدوث المشكلة: 1446/09/10 هـ
378
  اسم صاحب المشكلة: نوف الناصر
379
  رقم الهوية: 1234567890
 
382
  المسح: الخبر
383
  المنطقة: الشرقية`;
384
 
385
+ const STATE_KEY="ticketParserState_v10_16";
386
+ const ALL_STATE_KEYS=["ticketParserState_v8","ticketParserState_v9","ticketParserState_v10","ticketParserState_v10_1","ticketParserState_v10_2","ticketParserState_v10_3","ticketParserState_v10_5","ticketParserState_v10_6","ticketParserState_v10_7","ticketParserState_v10_8","ticketParserState_v10_9","ticketParserState_v10_10","ticketParserState_v10_11","ticketParserState_v10_12","ticketParserState_v10_13","ticketParserState_v10_14","ticketParserState_v10_15","ticketParserState_v10_16"];
 
 
 
 
 
 
 
387
 
388
+ function ensureColumns(rows,agentName,defaultRegion){
389
+ if(!Array.isArray(rows))return rows||[];
390
  return rows.map(r=>{
391
  const out={...r};
392
+ if(!("التصنيف"in out)||!out["التصنيف"]){const fakeText=Object.values(out).join("\n");out["التصنيف"]=classifyTicket(fakeText,out)}
393
+ if(!("اسم الدعم الفني"in out))out["اسم الدعم الفني"]=agentName||out["اسم الدعم الفني"]||"";
394
+ if(!("الحالة"in out)||!out["الحالة"])out["الحالة"]="تم الحل";
395
+ if(defaultRegion)out["المنطقة"]=defaultRegion;
 
 
 
396
  return out;
397
  });
398
  }
 
403
  const agent=document.getElementById("agentName")?.value||"";
404
  const region=document.getElementById("regionDefault")?.value||"";
405
  const rows=readTable();
406
+ localStorage.setItem(STATE_KEY,JSON.stringify({raw,agent,region,rows,theme:document.body.classList.contains('dark')?'dark':'light'}));
407
  }catch{}
408
  }
409
 
410
  function loadState(){
411
  try{
412
  const s=localStorage.getItem(STATE_KEY);
413
+ if(!s)return false;
414
+ let{raw,agent,region,rows,theme}=JSON.parse(s);
415
+ const rawEl=document.getElementById("raw");if(typeof raw==="string"&&rawEl)rawEl.value=raw;
416
+ const agentEl=document.getElementById("agentName");if(typeof agent==="string"&&agentEl)agentEl.value=agent;
417
+ const regionEl=document.getElementById("regionDefault");if(typeof region==="string"&&regionEl)regionEl.value=region;
418
+ if(theme==='dark')document.body.classList.add('dark');
419
  updateThemeLabel();
420
  rows=ensureColumns(rows,agent,region);
421
+ if(Array.isArray(rows)&&rows.length){buildTable(rows);validateCells();updateBadge(rows.length);setButtonsEnabled(true)}
422
  return true;
423
+ }catch{return false}
424
  }
425
 
426
  function clearAll(){
 
428
  const tbody=document.getElementById("tbody");
429
  const agentEl=document.getElementById("agentName");
430
  const regionEl=document.getElementById("regionDefault");
431
+ if(rawEl)rawEl.value="";
432
+ if(tbody)tbody.innerHTML="";
433
+ if(agentEl)agentEl.value="";
434
+ if(regionEl)regionEl.value="";
435
+ updateBadge(0);setButtonsEnabled(false);
436
  document.getElementById("warn").hidden=true;
437
+ try{ALL_STATE_KEYS.forEach(k=>localStorage.removeItem(k))}catch{}
438
  toast("تم مسح كل البيانات والتخزين.");
439
  }
440
 
441
  function mergeDuplicatesRows(rows){
442
+ if(!rows.length)return rows;
443
  const map=new Map();
444
  rows.forEach(r=>{
445
  const key=[r["رقم الهوية"]||"",r["رقم الجهاز"]||"",r["رقم الجوال"]||"",r["وقت حدوث المشكلة"]||"",(r["نوع المشكلة"]||"").slice(0,40)].join("|");
446
+ if(!map.has(key))map.set(key,r);
447
  });
448
+ return[...map.values()];
449
  }
450
 
451
  function updateThemeLabel(){
452
  const btn=document.getElementById("btn-theme");
453
+ if(!btn)return;
454
  btn.textContent=document.body.classList.contains("dark")?"☀️ وضع نهار":"🌙 وضع ليلي";
455
  }
456
 
 
463
  }
464
  function tryUnlock(){
465
  const p=document.getElementById("lockPass").value||"";
466
+ if(ACCESS_PASSWORDS.includes(p)){hideGate()}else{
 
 
 
467
  const m=document.getElementById("lockMsg");
468
  m.hidden=false;
469
  setTimeout(()=>m.hidden=true,1500);
 
484
 
485
  rawEl.placeholder=SAMPLE;
486
 
487
+ showGate();
488
 
489
  loadState();
490
 
491
+ parseBtn.addEventListener("click",()=>{
492
  const raw=(rawEl.value||"").trim();
493
+ if(!raw){toast("فضلاً الصق/ي تذاكر أولاً.");return}
494
  const cleaned=normalizeText(fixLabels(raw));
495
  const agent=agentEl.value||"";
496
  const defRegion=regionEl.value||"";
497
  let rows=parseTicketsWithExtras(cleaned,agent,defRegion);
498
  rows=mergeDuplicatesRows(rows);
499
+ buildTable(rows);validateCells();
500
+ updateBadge(rows.length);setButtonsEnabled(rows.length>0);
501
  saveState();
502
  toast(`تم استخراج ${rows.length} تذكرة.`);
503
  });
504
 
505
+ exportBtn.addEventListener("click",exportExcel);
506
+ copyBtn.addEventListener("click",copyToClipboardTSV);
507
+ clearBtn.addEventListener("click",clearAll);
508
 
509
+ themeBtn.addEventListener("click",()=>{
510
  document.body.classList.toggle("dark");
511
  updateThemeLabel();
512
  saveState();
513
  });
514
 
515
+ rawEl.addEventListener("input",saveState);
516
+ agentEl.addEventListener("input",saveState);
517
+ regionEl.addEventListener("change",saveState);
518
 
519
+ document.addEventListener("keydown",e=>{
520
  const ctrl=e.ctrlKey||e.metaKey;
521
+ if(ctrl&&e.key==="Enter"){e.preventDefault();parseBtn.click()}
522
+ else if(ctrl&&e.key.toLowerCase()==="e"){e.preventDefault();exportBtn.click()}
523
+ else if(ctrl&&e.shiftKey&&e.key.toLowerCase()==="c"){e.preventDefault();copyBtn.click()}
524
+ else if(e.key==="Escape"){e.preventDefault();clearAll()}
525
  });
526
 
527
+ lockBtn.addEventListener("click",tryUnlock);
528
+ lockPass.addEventListener("keydown",e=>{if(e.key==="Enter")tryUnlock()});
529
 
530
  setButtonsEnabled(!!document.getElementById("tbody")?.children.length);
531
  updateThemeLabel();