stat2025 commited on
Commit
9e9b2b2
·
verified ·
1 Parent(s): e8b0b71

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +165 -545
app.js CHANGED
@@ -1,589 +1,209 @@
1
- /* v10.3: Smart Paste append + reliable modal fallback + safe splitting + hard clear */
2
-
3
- const EXPORT_COLUMNS = [
4
- "التصنيف","نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
5
- "رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة","اسم الدعم الفني","الحالة"
6
- ];
7
-
8
- const FIELD_ALIASES = {
9
- "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
10
- "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
11
- "اسم صاحب المشكلة": ["اسم صاحب المشكله","اسم صاحب المشكلة","اسم صاحب البلاغ","الاسم"],
12
- "رقم الهوية": ["رقم الهويه","رقم الهوية","الهوية"],
13
- "رقم الجهاز": ["رقم الجهاز","الجهاز"],
14
- "رقم الجوال": ["رقم الجوال","الجوال","الهاتف"],
15
- "المسح": ["المسح","اسم المسح"],
16
- "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
 
 
 
17
  };
18
 
19
- const CLASS_RULES = {
20
- "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
21
- "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
22
- "الاستمارة": ["الاستمارة","استمارة","النموذج","نموذج","الفورم","تعليق الاستمارة","لا استطيع اكمال الاستمارة","التعبئة"],
23
- "التقييم": ["التقييم","تقييم","feedback","survey","رضا","نجوم"],
24
- "الخرائط": ["الخرائط","خرائط","map","gps","تحديد الموقع","احداثيات","إحداثيات","الموقع الجغرافي"],
25
- "السوتي": ["السوتي","سوتي","soti","soti assist","mobicontrol","soti mobicontrol"],
26
- "الشبكة": ["الشبكة","شبكة","نت","انترنت","إنترنت","wifi","واي فاي","4g","5g","ضعف الشبكة","stc","mobily","زين","weak signal","no signal"],
27
- "النسخة": ["النسخة","نسخة","الإصدار","اصدار","version","build","release","تحديث نسخة","ترقية النسخة"],
28
- "النظام المكتبي": ["النظام المكتبي","نسخة ويندوز","ويندوز","windows","pc app","برنامج المكتب","التطبيق على الكمبيوتر","الديسكتوب"],
29
- "تسجيل دخول": ["تسجيل دخول","تسجيل الدخول","login","signin","رفض تسجيل الدخول","لا يقبل الدخول","اسم المستخدم","كلمة المرور","نسيت كلمة السر","إعادة تعيين"],
30
- "تفعيل حساب": ["تفعيل حساب","تفعيل","activation","activate","رمز التفعيل","كود التفعيل"],
31
- "تناقل البيانات": ["تناقل البيانات","ترحيل البيانات","مزامنة","sync","مزامنه","نقل البيانات","رفع البيانات","sync failed","المزامنة"],
32
- "صيانة وتحديث الأجهزة": ["صيانة","تحديث الأجهزة","تحديث جهاز","ترقية الجهاز","اعطال الجهاز","تصليح","صيانة وتحديث الأجهزة","صيانة الجهاز"],
33
- };
34
- const CLASS_PRIORITY = [
35
- "صيانة وتحديث الأجهزة","إضافة أجهزة","تسجيل دخول","تفعيل حساب",
36
- "الاستمارة","التقييم","الخرائط","السوتي","الشبكة","النسخة",
37
- "النظام المكتبي","تناقل البيانات","استفسار",
38
- ];
39
-
40
- const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
41
- const MIN_SPLIT_SPAN = 80;
42
-
43
- const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
44
- function normalizeText(s){
45
- if(typeof s!=="string") return "";
46
- return s.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
- .trim();
51
- }
52
- function lettersOnly(ar){ return (ar||"").replace(/[^A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]/g,"").replace(/\s{2,}/g," ").trim(); }
53
- function alnumAr(s){ return (s||"").replace(/[^0-9A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s\-\._/]/g,"").replace(/\s{2,}/g," ").trim(); }
54
- function digitsOnly(s){ return (s||"").replace(/\D+/g,""); }
55
-
56
- function normalizeTimeLoose(val){
57
- const t = normalizeText(val);
58
- let m = t.match(/(\d{1,2})[:٫\.\-:](\d{2})\s*(ص|صباح|صباحا|am|م|مساء|pm)?/i);
59
- if(m){
60
- let h = parseInt(m[1],10), mn = m[2], ampm = (m[3]||"").toLowerCase();
61
- if(ampm){
62
- if((/م|مساء|pm/).test(ampm) && h<12) h+=12;
63
- if((/ص|صباح|صباحا|am/).test(ampm) && h===12) h=0;
64
- }
65
- return `${String(h).padStart(2,"0")}:${mn}`;
66
- }
67
- m = t.match(/(\d{1,2})\s*(ص|صباح|صباحا|am|م|مساء|pm)/i);
68
- if(m){
69
- let h = parseInt(m[1],10);
70
- const ampm = (m[2]||"").toLowerCase();
71
- if((/م|مساء|pm/).test(ampm) && h<12) h+=12;
72
- if((/ص|صباح|صباحا|am/).test(ampm) && h===12) h=0;
73
- return `${String(h).padStart(2,"0")}:00`;
74
- }
75
- return "";
76
  }
77
- function normalizeDate(v){
78
- v=(v||"").trim();
79
- let m=v.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/);
80
- if(m){
81
- let d=+m[1], mo=+m[2], y=+m[3]; if(y<100) y+=2000;
82
- return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).toString().padStart(2,"0")}`;
83
- }
84
- m=v.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
85
- if(m){
86
- let y=+m[1], mo=+m[2], d=+m[3];
87
- return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).toString().padStart(2,"0")}`;
88
- }
89
- return v;
90
  }
91
- function parseDateTime(raw){
92
- const t = normalizeText(raw);
93
- const d = normalizeDate(t);
94
- const hhmm = normalizeTimeLoose(t);
95
- if(d && /^\d{4}-\d{2}-\d{2}$/.test(d) && hhmm) return `${d} ${hhmm}`;
96
- if(d && /^\d{4}-\d{2}-\d{2}$/.test(d)) return d;
97
- if(hhmm) return hhmm;
98
- return t;
 
 
 
 
99
  }
 
 
 
 
 
 
100
 
101
  function findStartsByLabels(text, labels){
102
- const lblRe = labels.map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
103
- const re = new RegExp(`(^|\\n)\\s*(?:[-–—\\*•]+|\\d+[\\)\\.]\\s*)?\\s*(?:${lblRe})(?:\\s*[::]|\\s+)`,"gi");
104
- const idxs=[]; let m;
105
- while((m = re.exec(text))){ idxs.push(m.index + (m[1]?m[1].length:0)); }
106
- return idxs;
 
 
 
 
107
  }
108
 
109
  function splitTickets(raw){
110
  const text = normalizeText(raw);
111
  if(!text) return [];
112
- if(TICKET_SEP.test(text)){
113
- return text.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
 
 
 
114
  }
115
- const starts = findStartsByLabels(text, ["نوع المشكلة","نوع المشكله"])
116
- .sort((a,b)=>a-b)
117
- .filter((pos,i,arr)=> i===0 || (pos - arr[i-1]) >= MIN_SPLIT_SPAN);
118
  if(starts.length >= 2){
119
  const parts=[];
120
  for(let i=0;i<starts.length;i++){
121
  const s = starts[i];
122
- const e = i+1<starts.length ? starts[i+1] : text.length;
123
  const slice = text.slice(s,e).trim();
124
  if(slice) parts.push(slice);
125
  }
126
- return parts;
127
  }
128
- const blocks = text.split(/\n\s*\n+/).map(p=>p.trim()).filter(Boolean);
129
- if(blocks.length>1) return blocks;
130
- return [text];
131
- }
132
-
133
- function findAfterLabel(text, labels){
134
- const hay = "\n" + normalizeText(text) + "\n";
135
- for(const rawLbl of labels){
136
- const lbl = rawLbl.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
137
- let m = hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s*[::]\\s*([^\\n]+)`, "i"));
138
- if(m) return m[1].trim();
139
- m = hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s+([^\\n]+)`, "i"));
140
- if(m) return m[1].trim();
141
- }
142
- return "";
143
- }
144
- function extractFields(ticketText){
145
- const text = normalizeText(ticketText);
146
- const out = {
147
- "نوع المشكلة":"", "وقت حدوث المشكلة":"", "اسم صاحب المشكلة":"",
148
- "رقم الهوية":"", "رقم الجهاز":"", "رقم الجوال":"", "المسح":"", "المنطقة":""
149
- };
150
-
151
- let v = findAfterLabel(text, FIELD_ALIASES["نوع المشكلة"]);
152
- if(v) out["نوع المشكلة"] = alnumAr(v);
153
 
154
- v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
155
- if(v) out["وقت حدوث المشكلة"] = parseDateTime(v);
156
 
157
- v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
158
- if(v) out["اسم صاحب المشكلة"] = v;
 
159
 
160
- v = findAfterLabel(text, FIELD_ALIASES["رقم الهوية"]);
161
- if(v) out["رقم الهوية"] = digitsOnly(v);
162
- if(!out["رقم الهوية"]){
163
- const m = text.match(/(?:^|\D)((?:1|2)\d{9})(?:\D|$)/);
164
- if(m) out["رقم الهوية"] = m[1];
165
- }
166
 
167
- v = findAfterLabel(text, FIELD_ALIASES["رقم الجهاز"]);
168
- if(v) out["رقم الجهاز"] = digitsOnly(v);
169
- if(!out["رقم الجهاز"]){
170
- const m = text.match(/(?:^|\D)(\d{5,20})(?:\D|$)/);
171
- if(m) out["رقم الجهاز"] = m[1];
172
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- v = findAfterLabel(text, FIELD_ALIASES["رقم الجوال"]);
175
- if(v) out["رقم الجوال"] = digitsOnly(v);
176
- if(!out["رقم الجوال"]){
177
- const m = text.match(/(?:^|\D)(05\d{7,})(?:\D|$)/);
178
- if(m) out["رقم الجوال"] = m[1];
179
- }
180
 
181
- v = findAfterLabel(text, FIELD_ALIASES["المسح"]);
182
- if(v) out["المسح"] = alnumAr(v);
 
 
183
 
184
- v = findAfterLabel(text, FIELD_ALIASES["المنطقة"]);
185
- if(v) out["المنطقة"] = lettersOnly(v);
186
 
187
- return out;
188
  }
189
 
190
- function classifyTicket(text, fields){
191
- const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
192
- for(const label of CLASS_PRIORITY){
193
- const kws = CLASS_RULES[label] || [];
194
- for(const kw of kws){
195
- const needle = normalizeText(kw).toLowerCase();
196
- if(needle && hay.includes(needle)) return label;
197
- }
198
- }
199
- return "استفسار";
200
- }
201
 
202
- function parseTicketsWithExtras(raw, agentName, defaultRegion){
203
- const regionChosen = (defaultRegion || "").toString();
204
- return splitTickets(raw||"").map(t => {
205
- const f = extractFields(t);
206
- const cls = classifyTicket(t, f);
207
- const region = regionChosen ? regionChosen : (f["المنطقة"] || "");
208
- return {
209
- "التصنيف": cls,
210
- "نوع المشكلة": f["نوع المشكلة"] || "",
211
- "وقت حدوث المشكلة": f["وقت حدوث المشكلة"] || "",
212
- "اسم صاحب المشكلة": f["اسم صاحب المشكلة"] || "",
213
- "رقم الهوية": f["رقم الهوية"] || "",
214
- "رقم الجهاز": f["رقم الجهاز"] || "",
215
- "رقم الجوال": f["رقم الجوال"] || "",
216
- "المسح": f["المسح"] || "",
217
- "المنطقة": region,
218
- "اسم الدعم الفني": agentName || "",
219
- "الحالة": "تم الحل",
220
- };
221
- });
222
- }
223
-
224
- function buildTable(rows){
225
- const theadRow = document.getElementById("theadRow");
226
- const tbody = document.getElementById("tbody");
227
- theadRow.innerHTML = "";
228
- EXPORT_COLUMNS.forEach(col=>{
229
- const th=document.createElement("th");
230
- th.textContent=col;
231
- theadRow.appendChild(th);
232
- });
233
  tbody.innerHTML = "";
234
- rows.forEach(r=>{
235
- const tr=document.createElement("tr");
236
- EXPORT_COLUMNS.forEach(col=>{
237
- const td=document.createElement("td");
238
- td.contentEditable="true";
239
- td.textContent = r[col]||"";
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  tr.appendChild(td);
241
- });
242
- tbody.appendChild(tr);
243
- });
244
- }
245
-
246
- function readTable(){
247
- const tbody = document.getElementById("tbody");
248
- const rows = [];
249
- [...tbody.querySelectorAll("tr")].forEach(tr=>{
250
- const obj={};
251
- [...tr.children].forEach((td,idx)=>{ obj[EXPORT_COLUMNS[idx]] = td.textContent.trim(); });
252
- rows.push(obj);
253
- });
254
- return rows;
255
- }
256
-
257
- function updateBadge(n){
258
- const b = document.getElementById("countBadge");
259
- b.textContent = n; b.hidden = (n===0);
260
- }
261
- function setButtonsEnabled(hasRows){
262
- document.getElementById("btn-export").disabled = !hasRows;
263
- document.getElementById("btn-copy").disabled = !hasRows;
264
- }
265
- function validateCells(){
266
- const tbody=document.getElementById("tbody");
267
- const idxPhone = EXPORT_COLUMNS.indexOf("رقم الجوال");
268
- const idxID = EXPORT_COLUMNS.indexOf("رقم الهوية");
269
- [...tbody.rows].forEach(tr=>{
270
- if(idxPhone>=0){
271
- const td=tr.children[idxPhone];
272
- const raw=(td.textContent||"").trim();
273
- const digits = raw.replace(/\D/g,"");
274
- const invalid = !!raw && digits.length < 9;
275
- td.classList.toggle("invalid", invalid);
276
- }
277
- if(idxID>=0){
278
- const td=tr.children[idxID];
279
- const raw=(td.textContent||"").trim();
280
- const digits = raw.replace(/\D/g,"");
281
- const invalid = !!raw && digits.length !== 10;
282
- td.classList.toggle("invalid", invalid);
283
  }
284
- });
285
- }
286
- document.addEventListener("input",(e)=>{
287
- if(e.target && e.target.closest && e.target.closest("#tbody")){
288
- validateCells();
289
- saveState();
290
- }
291
- });
292
-
293
- function toast(msg){
294
- const t = document.getElementById("toast");
295
- t.textContent = msg; t.hidden = false;
296
- t.classList.remove("show"); void t.offsetWidth; t.classList.add("show");
297
- setTimeout(()=>{ t.hidden = true; }, 2000);
298
- }
299
-
300
- async function exportExcel(){
301
- const rows = readTable();
302
- if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
303
-
304
- const TEMPLATE_HEADERS = [
305
- "نوع المشكلة","وصف المشكلة","المنطقة","اسم المسح","اسم المشغل",
306
- "رقم الجوال","رقم الهوية ID","رقم الجهاز","تاريخ اليوم بالميلادي","الحالة","اسم الدعم الفني",
307
- ];
308
- const mapRow = (r)=>{
309
- const today = new Date();
310
- const yyyy=today.getFullYear(), mm=String(today.getMonth()+1).padStart(2,"0"), dd=String(today.getDate()).padStart(2,"0");
311
- const todayStr = `${yyyy}-${mm}-${dd}`;
312
- return {
313
- "نوع المشكلة": r["نوع المشكلة"]||"",
314
- "وصف المشكلة": r["التصنيف"] || r["نوع المشكلة"] || "",
315
- "المنطقة": r["المنطقة"]||"",
316
- "اسم المسح": r["المسح"]||"",
317
- "اسم المشغل": r["اسم صاحب المشكلة"]||"",
318
- "رقم الجوال": (r["رقم الجوال"]||"").toString(),
319
- "رقم الهوية ID":(r["رقم الهوية"]||"").toString(),
320
- "رقم الجهاز": (r["رقم الجهاز"]||"").toString(),
321
- "تاريخ اليوم بالميلادي": todayStr,
322
- "الحالة": r["الحالة"]||"تم الحل",
323
- "اسم الدعم الفني": r["اسم الدعم الفني"]||"",
324
- };
325
- };
326
-
327
- const wb = new ExcelJS.Workbook();
328
- const ws = wb.addWorksheet("التذاكر", { views: [{ rightToLeft: true }] });
329
-
330
- const colWidths = [18,26,16,18,20,18,18,18,22,14,18];
331
- TEMPLATE_HEADERS.forEach((h,i)=> ws.getColumn(i+1).width = colWidths[i]||18);
332
-
333
- ws.addRow(TEMPLATE_HEADERS);
334
- const headerRow = ws.getRow(1);
335
- headerRow.height = 24;
336
- headerRow.eachCell((cell)=>{
337
- cell.font = { bold:true, color:{argb:"FFFFFFFF"} };
338
- cell.alignment = { horizontal:"center", vertical:"middle" };
339
- cell.fill = { type:"pattern", pattern:"solid", fgColor:{argb:"FF4137A8"} };
340
- cell.border = {
341
- top:{style:"thin",color:{argb:"FFCDD2E1"}},
342
- bottom:{style:"thin",color:{argb:"FFCDD2E1"}},
343
- left:{style:"thin",color:{argb:"FFE5E7EB"}},
344
- right:{style:"thin",color:{argb:"FFE5E7EB"}},
345
- };
346
- });
347
-
348
- const toTextCols = new Set(["رقم الجوال","رقم الهوية ID","رقم الجهاز"]);
349
- const rawRows = readTable();
350
- rawRows.forEach((r,idx)=>{
351
- const m = mapRow(r);
352
- const vals = TEMPLATE_HEADERS.map(h => (m[h] ?? ""));
353
- const row = ws.addRow(vals);
354
- row.alignment = { horizontal:"center", vertical:"middle" };
355
- const even = (idx % 2) === 1;
356
- row.eachCell((cell, colNumber)=>{
357
- cell.border = {
358
- top:{style:"thin",color:{argb:"FFE5E7EB"}},
359
- bottom:{style:"thin",color:{argb:"FFE5E7EB"}},
360
- left:{style:"thin",color:{argb:"FFE5E7EB"}},
361
- right:{style:"thin",color:{argb:"FFE5E7EB"}},
362
- };
363
- if(even) cell.fill = { type:"pattern", pattern:"solid", fgColor:{argb:"FFF5F8FF"} };
364
- const header = TEMPLATE_HEADERS[colNumber-1];
365
- if(toTextCols.has(header)) cell.value = String(cell.value ?? "");
366
- });
367
- });
368
-
369
- const ts = new Date().toISOString().replace(/\D/g,"").slice(0,14);
370
- const base = (document.getElementById("fname").value || "Ticket").trim() || "Ticket";
371
- const filename = `${base}_${ts}.xlsx`;
372
-
373
- const buffer = await wb.xlsx.writeBuffer();
374
- const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
375
- const file = new File([blob], filename, { type: blob.type });
376
-
377
- if (navigator.canShare && navigator.canShare({ files: [file] })) {
378
- try { await navigator.share({ files: [file], title: "ملف التذاكر" }); toast("تمت المشاركة/الحفظ."); return; }
379
- catch(e){}
380
- }
381
- const url = URL.createObjectURL(blob);
382
- const a = document.createElement("a"); a.href = url; a.download = filename;
383
- document.body.appendChild(a); a.click(); a.remove();
384
- setTimeout(()=>URL.revokeObjectURL(url), 1000);
385
- toast("تم تنزيل الملف بتنسيق القالب.");
386
- }
387
-
388
- async function copyToClipboardTSV(){
389
- const rows = readTable();
390
- if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
391
- const textCols = new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
392
- const header = EXPORT_COLUMNS.join("\t");
393
- const body = rows.map(r =>
394
- EXPORT_COLUMNS.map(c => {
395
- let v = (r[c] ?? "").toString().replace(/\t/g," ");
396
- if(textCols.has(c) && v && /^[0-9]+$/.test(v)) v = "'" + v;
397
- return v;
398
- }).join("\t")
399
- ).join("\r\n");
400
- const tsv = "\uFEFF" + header + "\r\n" + body;
401
-
402
- try{ await navigator.clipboard.writeText(tsv); toast("تم النسخ — الصق/ي مباشرة في Excel."); }
403
- catch(e){
404
- const ta = document.createElement("textarea");
405
- ta.value = tsv; document.body.appendChild(ta);
406
- ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
407
- toast("تم النسخ — الصق/ي مباشرة في Excel.");
408
  }
 
 
409
  }
410
 
411
- const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
412
- وقت حدوث المشكلة: 21/8/2025 7 صباحا
413
- اسم صاحب المشكلة : نوف الناصر
414
- رقم الهوية: 1234567890
415
- رقم الجهاز: 01234
416
- رقم الجوال: 0558174717
417
- اسم المسح: الخبر 2025
418
- اسم المنطقة: الشرقية`;
419
-
420
- const STATE_KEY = "ticketParserState_v10_3";
421
- const ALL_STATE_KEYS = [
422
- "ticketParserState_v8","ticketParserState_v9","ticketParserState_v10",
423
- "ticketParserState_v10_1","ticketParserState_v10_2","ticketParserState_v10_3"
424
- ];
425
-
426
- function ensureColumns(rows, agentName, defaultRegion){
427
- if(!Array.isArray(rows)) return rows||[];
428
- return rows.map(r=>{
429
- const out = {...r};
430
- if(!("التصنيف" in out) || !out["التصنيف"]){
431
- const fakeText = Object.values(out).join("\n");
432
- out["التصنيف"] = classifyTicket(fakeText, out);
433
- }
434
- if(!("اسم الدعم الفني" in out)) out["اسم الدعم الفني"] = agentName || out["اسم الدعم الفني"] || "";
435
- if(!("الحالة" in out) || !out["الحالة"]) out["الحالة"] = "تم الحل";
436
- if(defaultRegion) out["المنطقة"] = defaultRegion;
437
- return out;
438
- });
439
- }
440
- function saveState(){
441
- try{
442
- const raw = document.getElementById("raw")?.value || "";
443
- const fname = document.getElementById("fname")?.value || "Ticket";
444
- const agent = document.getElementById("agentName")?.value || "";
445
- const region= document.getElementById("regionDefault")?.value || "";
446
- const rows = readTable();
447
- localStorage.setItem(STATE_KEY, JSON.stringify({ raw, fname, agent, region, rows }));
448
- }catch{}
449
- }
450
- function loadState(){
451
- try{
452
- const s = localStorage.getItem(STATE_KEY);
453
- if(!s) return false;
454
- let { raw, fname, agent, region, rows } = JSON.parse(s);
455
- if(typeof raw === "string"){ const el=document.getElementById("raw"); if(el) el.value = raw; }
456
- if(typeof fname === "string"){ const el=document.getElementById("fname"); if(el) el.value = fname; }
457
- if(typeof agent === "string"){ const el=document.getElementById("agentName"); if(el) el.value = agent; }
458
- if(typeof region === "string"){ const el=document.getElementById("regionDefault"); if(el) el.value = region; }
459
- rows = ensureColumns(rows, agent, region);
460
- if(Array.isArray(rows) && rows.length){
461
- buildTable(rows); validateCells(); updateBadge(rows.length); setButtonsEnabled(true);
462
- }
463
- return true;
464
- }catch{ return false; }
465
- }
466
 
467
- function clearAll(){
468
- const rawEl = document.getElementById("raw");
469
- const tbody = document.getElementById("tbody");
470
- const fnameEl = document.getElementById("fname");
471
- const agentEl = document.getElementById("agentName");
472
- const regionEl= document.getElementById("regionDefault");
473
- if(rawEl) rawEl.value = "";
474
- if(tbody) tbody.innerHTML = "";
475
- if(fnameEl) fnameEl.value = "Ticket";
476
- if(agentEl) agentEl.value = "";
477
- if(regionEl) regionEl.value = "";
478
- updateBadge(0); setButtonsEnabled(false);
479
- try{ ALL_STATE_KEYS.forEach(k=>localStorage.removeItem(k)); }catch{}
480
- toast("تم مسح كل البيانات والتخزين.");
481
- }
482
-
483
- function normalizeForPaste(text){
484
- const norm = normalizeText(text||"");
485
- const parts = splitTickets(norm);
486
- return parts.length ? parts.join("\n\n🔴🔴🔴\n") : norm;
487
- }
488
 
489
- async function smartPasteInto(el){
490
- try{
491
- const txt = await navigator.clipboard.readText();
492
- if(txt && txt.trim()){
493
- const formatted = normalizeForPaste(txt);
494
- if(el.value && el.value.trim()){
495
- el.value = el.value.trimEnd() + "\n\n🔴🔴🔴\n" + formatted;
496
- }else{
497
- el.value = formatted;
498
- }
499
- saveState();
500
- toast("تم اللصق والتنظيم.");
501
- return;
502
- }
503
- openPasteModal(el);
504
- }catch{
505
- openPasteModal(el);
506
  }
507
- }
508
 
509
- function openPasteModal(targetEl){
510
- const modal = document.getElementById("pasteModal");
511
- const input = document.getElementById("pasteInput");
512
- const add = document.getElementById("pasteAdd");
513
- const cancel= document.getElementById("pasteCancel");
514
- input.value = "";
515
- modal.hidden = false;
516
- input.focus();
517
-
518
- function close(){ modal.hidden = true; add.removeEventListener("click", onAdd); cancel.removeEventListener("click", onCancel); document.removeEventListener("keydown", onEsc); }
519
- function onAdd(){
520
- const txt = input.value || "";
521
- const formatted = normalizeForPaste(txt);
522
- if(formatted.trim()){
523
- if(targetEl.value && targetEl.value.trim()){
524
- targetEl.value = targetEl.value.trimEnd() + "\n\n🔴🔴🔴\n" + formatted;
525
- }else{
526
- targetEl.value = formatted;
527
- }
528
- saveState();
529
- toast("تمت الإضافة.");
530
- }
531
- close();
532
- }
533
- function onCancel(){ close(); }
534
- function onEsc(e){ if(e.key === "Escape"){ e.preventDefault(); close(); } }
535
-
536
- add.addEventListener("click", onAdd);
537
- cancel.addEventListener("click", onCancel);
538
- document.addEventListener("keydown", onEsc);
539
- }
540
-
541
- function init(){
542
- const parseBtn = document.getElementById("btn-parse");
543
- const exportBtn = document.getElementById("btn-export");
544
- const copyBtn = document.getElementById("btn-copy");
545
- const clearBtn = document.getElementById("btn-clear");
546
- const sampleBtn = document.getElementById("btn-sample");
547
- const smartPaste = document.getElementById("btn-smartpaste");
548
- const rawEl = document.getElementById("raw");
549
- const fnameEl = document.getElementById("fname");
550
- const agentEl = document.getElementById("agentName");
551
- const regionEl = document.getElementById("regionDefault");
552
-
553
- loadState();
554
-
555
- smartPaste.addEventListener("click", ()=> smartPasteInto(rawEl));
556
-
557
- parseBtn.addEventListener("click", ()=>{
558
- const raw = (rawEl.value || "").trim();
559
- if(!raw){ toast("فضلاً الصق/ي تذاكر أولاً."); return; }
560
- const agent = agentEl.value || "";
561
- const defRegion = regionEl.value || "";
562
- const rows = parseTicketsWithExtras(raw, agent, defRegion);
563
- buildTable(rows); validateCells();
564
- updateBadge(rows.length); setButtonsEnabled(rows.length>0);
565
- saveState();
566
- toast(`تم استخراج ${rows.length} تذكرة.`);
567
- });
568
 
569
- exportBtn.addEventListener("click", exportExcel);
570
- copyBtn.addEventListener("click", copyToClipboardTSV);
571
- clearBtn.addEventListener("click", clearAll);
572
- sampleBtn.addEventListener("click", ()=>{ rawEl.value = SAMPLE; saveState(); });
573
-
574
- rawEl.addEventListener("input", saveState);
575
- fnameEl.addEventListener("input", saveState);
576
- agentEl.addEventListener("input", saveState);
577
- regionEl.addEventListener("change", saveState);
578
-
579
- document.addEventListener("keydown", (e)=>{
580
- const ctrl = e.ctrlKey || e.metaKey;
581
- if(ctrl && e.key === "Enter"){ e.preventDefault(); parseBtn.click(); }
582
- else if(ctrl && e.key.toLowerCase() === "e"){ e.preventDefault(); exportBtn.click(); }
583
- else if(ctrl && e.shiftKey && e.key.toLowerCase() === "c"){ e.preventDefault(); copyBtn.click(); }
584
- else if(e.key === "Escape"){ e.preventDefault(); clearAll(); }
585
- });
586
 
587
- setButtonsEnabled(!!document.getElementById("tbody")?.children.length);
588
- }
589
- init();
 
1
+ /* v11.2 - Arabic-smart ticket split & parse, no paste-cache, clean clear */
2
+
3
+ const tbody = document.getElementById("tbody");
4
+ const inputEl = document.getElementById("input");
5
+ const analyze = document.getElementById("analyze");
6
+ const clearBtn= document.getElementById("clear");
7
+ const counter = document.getElementById("counter");
8
+
9
+ let rows = [];
10
+
11
+ const LABELS = {
12
+ problemType: ["نوع المشكلة","نوع المشكله"],
13
+ time: ["وقت حدوث المشكلة","وقت حدوث المشكله"],
14
+ name: ["اسم صاحب المشكلة","اسم صاحب المشكله"],
15
+ idn: ["رقم الهوية","رقم الهويه"],
16
+ device: ["رقم الجهاز"],
17
+ phone: ["رقم الجوال","الجوال"],
18
+ region: ["المنطقة","المنطقه"],
19
+ survey: ["المسح"]
20
  };
21
 
22
+ const ALL_LABELS_FLAT = Object.values(LABELS).flat();
23
+
24
+ /* ---------- Utils ---------- */
25
+
26
+ function toEnglishDigits(str){
27
+ if(!str) return str;
28
+ const map = {
29
+ "٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9",
30
+ "۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9"
31
+ };
32
+ return str.replace(/[٠-٩۰-۹]/g, d => map[d] || d);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
+ function stripDiacritics(s){
35
+ return s.replace(/[\u0610-\u061A\u064B-\u065F\u06D6-\u06ED]/g,'');
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
+ function normalizeText(raw){
38
+ if(!raw) return "";
39
+ let t = raw.replace(/\r\n?/g,"\n");
40
+ t = toEnglishDigits(t);
41
+ t = stripDiacritics(t);
42
+ t = t.replace(/[:﹕؛]/g,":");
43
+ t = t.replace(/[“”]/g,'"');
44
+ t = t.replace(/[–—−]/g,"-");
45
+ t = t.replace(/\u00A0/g," ");
46
+ t = t.replace(/[ \t]+\n/g,"\n");
47
+ t = t.replace(/\n{3,}/g,"\n\n");
48
+ return t.trim();
49
  }
50
+ function escRe(s){return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}
51
+
52
+ /* ---------- Split ---------- */
53
+
54
+ const MIN_SPLIT_SPAN = 40;
55
+ const HARD_SEP = /(?:^|\n)\s*(?:[-=+_*]{3,})\s*(?:\n|$)|\n{2,}/g;
56
 
57
  function findStartsByLabels(text, labels){
58
+ const starts = new Set();
59
+ labels.forEach(lbl=>{
60
+ const rx = new RegExp(`(^|\\n)\\s*${escRe(lbl)}\\s*:?`,"gi");
61
+ for(let m; (m = rx.exec(text)); ){
62
+ const pos = m.index + (m[1] ? m[1].length : 0);
63
+ starts.add(pos);
64
+ }
65
+ });
66
+ return Array.from(starts).sort((a,b)=>a-b);
67
  }
68
 
69
  function splitTickets(raw){
70
  const text = normalizeText(raw);
71
  if(!text) return [];
72
+
73
+ let starts = findStartsByLabels(text, LABELS.problemType);
74
+ if(starts.length < 2){
75
+ const more = findStartsByLabels(text, [...LABELS.name, ...LABELS.time]);
76
+ starts = Array.from(new Set([...starts, ...more])).sort((a,b)=>a-b);
77
  }
78
+ starts = starts.filter((p,i,arr)=> i===0 || (p - arr[i-1]) >= MIN_SPLIT_SPAN);
79
+
 
80
  if(starts.length >= 2){
81
  const parts=[];
82
  for(let i=0;i<starts.length;i++){
83
  const s = starts[i];
84
+ const e = (i+1<starts.length) ? starts[i+1] : text.length;
85
  const slice = text.slice(s,e).trim();
86
  if(slice) parts.push(slice);
87
  }
88
+ if(parts.length) return parts;
89
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ const hard = text.split(HARD_SEP).map(p=>p.trim()).filter(Boolean);
92
+ if(hard.length >= 2) return hard;
93
 
94
+ // Fallback: split on repeated "نوع المشكلة" in-line without newlines
95
+ const inline = text.split(/(?=نوع المش(?:كلة|كله)\s*:?)/g).map(s=>s.trim()).filter(Boolean);
96
+ if(inline.length >= 2) return inline;
97
 
98
+ return [text];
99
+ }
 
 
 
 
100
 
101
+ /* ---------- Parse ---------- */
102
+
103
+ const BOUNDARY_RE = new RegExp(
104
+ "(?:" + ALL_LABELS_FLAT.map(escRe).join("|") + ")\\s*:",
105
+ "i"
106
+ );
107
+
108
+ function grabField(block, variants){
109
+ const labelAlt = variants.map(escRe).join("|");
110
+ const rx = new RegExp(
111
+ "(?:^|\\n)\\s*(?:"+labelAlt+")\\s*:\\s*([\\s\\S]*?)(?=\\n\\s*"+BOUNDARY_RE.source+"|$)",
112
+ "i"
113
+ );
114
+ const m = rx.exec(block);
115
+ if(!m) return "";
116
+ return m[1].trim();
117
+ }
118
+
119
+ function cleanVal(v){
120
+ if(!v) return "";
121
+ let s = v.replace(/\s{2,}/g," ").trim();
122
+ s = s.replace(/^[-–—]+/,'').trim();
123
+ s = s.replace(/^(?:\(|\[)?(غير محدد|N\/A|NA)(?:\)|\])?$/i,'');
124
+ return s;
125
+ }
126
+
127
+ function parseOne(block){
128
+ const t = normalizeText(block);
129
+ const ticket = {
130
+ problemType: cleanVal(grabField(t, LABELS.problemType)),
131
+ time: cleanVal(grabField(t, LABELS.time)),
132
+ name: cleanVal(grabField(t, LABELS.name)),
133
+ idn: cleanVal(grabField(t, LABELS.idn)),
134
+ device: cleanVal(grabField(t, LABELS.device)),
135
+ phone: cleanVal(grabField(t, LABELS.phone)),
136
+ region: cleanVal(grabField(t, LABELS.region)),
137
+ survey: cleanVal(grabField(t, LABELS.survey)),
138
+ };
139
 
140
+ // Lightweight normalization
141
+ ticket.idn = toEnglishDigits(ticket.idn).replace(/[^\d]/g,'');
142
+ ticket.device= toEnglishDigits(ticket.device).replace(/[^\d]/g,'');
143
+ ticket.phone = toEnglishDigits(ticket.phone).replace(/[^\d]/g,'').replace(/^0+(?=\d)/,'0');
 
 
144
 
145
+ // Accept tickets that have at least 3 strong signals (name/phone/id/time/survey/region)
146
+ const score = [
147
+ ticket.name, ticket.phone, ticket.idn, ticket.time, ticket.survey, ticket.region
148
+ ].filter(v=>v && v.length>=2).length;
149
 
150
+ // If it's just a comment like "نوع المشكلة: عدد العينات 0 ..." drop it
151
+ if(score < 3 && !ticket.problemType) return null;
152
 
153
+ return ticket;
154
  }
155
 
156
+ /* ---------- Render ---------- */
 
 
 
 
 
 
 
 
 
 
157
 
158
+ function render(){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  tbody.innerHTML = "";
160
+ const frag = document.createDocumentFragment();
161
+ for(const r of rows){
162
+ const tr = document.createElement("tr");
163
+
164
+ const cells = [
165
+ ["نوع المشكلة", r.problemType],
166
+ ["وقت حدوث المشكلة", r.time],
167
+ ["اسم صاحب المشكلة", r.name],
168
+ ["رقم الهوية", r.idn],
169
+ ["رقم الجهاز", r.device],
170
+ ["رقم الجوال", r.phone],
171
+ ["المنطقة", r.region],
172
+ ["المسح", r.survey],
173
+ ];
174
+
175
+ for(const [label, val] of cells){
176
+ const td = document.createElement("td");
177
+ td.setAttribute("data-label", label);
178
+ td.textContent = val || "";
179
  tr.appendChild(td);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
+ frag.appendChild(tr);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
+ tbody.appendChild(frag);
184
+ counter.textContent = `عدد التذاكر: ${rows.length}`;
185
  }
186
 
187
+ /* ---------- Actions ---------- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ analyze.addEventListener("click", ()=>{
190
+ const raw = inputEl.value;
191
+ rows = [];
192
+ const chunks = splitTickets(raw);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ for(const ch of chunks){
195
+ const t = parseOne(ch);
196
+ if(t) rows.push(t);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  }
 
198
 
199
+ render();
200
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ clearBtn.addEventListener("click", ()=>{
203
+ rows = [];
204
+ inputEl.value = "";
205
+ render();
206
+ });
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ /* ---------- Demo guard (no cached restore) ---------- */
209
+ window.addEventListener("load", ()=>{ rows=[]; render(); });