stat2025 commited on
Commit
5df7340
·
verified ·
1 Parent(s): b3e6801

Update app.js

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