stat2025 commited on
Commit
11ee858
·
verified ·
1 Parent(s): 544be7a

Update app.js

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