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

Update app.js

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