stat2025 commited on
Commit
8adf575
·
verified ·
1 Parent(s): 9daee61

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +123 -110
app.js CHANGED
@@ -1,13 +1,13 @@
1
  /* ========= منطق التحليل والتصدير والنسخ (Static فقط) ========= */
2
- /* تمت إضافة: عمود "اسم الدعم الفني" وعمود "الحالة" (تم الحل) وتعبئة منطقة افتراضية عند الفراغ */
3
  const EXPORT_COLUMNS = [
4
- "التصنيف", // ثابت من نسختك السابقة
5
  "نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
6
  "رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة",
7
- "اسم الدعم الفني", // جديد
8
- "الحالة" // جديد (في النهاية)
9
  ];
10
 
 
11
  const FIELD_ALIASES = {
12
  "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
13
  "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
@@ -16,14 +16,10 @@ const FIELD_ALIASES = {
16
  "رقم الجهاز": ["رقم الجهاز","الجهاز"],
17
  "رقم الجوال": ["رقم الجوال","الجوال","الهاتف"],
18
  "المسح": ["المسح","اسم المسح"],
19
- "المنطقة": ["المنطقة","المنطقه","المدينة","المحافظة","منطقة"],
20
  };
21
 
22
- const LABEL_SEP = "(?::|:)?\\s*";
23
- const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
24
- const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
25
-
26
- /* ===== قواعد التصنيف (كما تم سابقًا) ===== */
27
  const CLASS_RULES = {
28
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
29
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
@@ -45,7 +41,9 @@ const CLASS_PRIORITY = [
45
  "النظام المكتبي","تناقل البيانات","استفسار",
46
  ];
47
 
48
- /* ================= أساسيات النص ================= */
 
 
49
  function normalizeText(s){
50
  if(typeof s!=="string") return "";
51
  return s.replace(/[\u200f\u200e\u202a-\u202e\u2066-\u2069\u00a0]/g," ")
@@ -53,13 +51,21 @@ function normalizeText(s){
53
  .replace(/[ــ]+/g,"")
54
  .trim();
55
  }
 
 
 
 
 
 
 
 
56
  function normalizeTime(val){
57
- const m = (val||"").match(/(\d{1,2})[:٫\.\-:](\d{2})\s*(ص|م)?/i);
58
  if(!m) return (val||"").trim();
59
- let h = parseInt(m[1],10), mn = m[2], ampm = m[3];
60
  if(ampm){
61
- if(/م|pm/i.test(ampm) && h<12) h+=12;
62
- if(/ص|am/i.test(ampm) && h===12) h=0;
63
  }
64
  return `${String(h).padStart(2,"0")}:${mn}`;
65
  }
@@ -82,14 +88,39 @@ function normalizeDate(v){
82
  const rx = new RegExp(`(\\d{1,2})\\s+(${Object.keys(months).join("|")})\\s+(\\d{2,4})`,"i");
83
  m = v.match(rx);
84
  if(m){
85
- const d = +m[1]; const mo = months[m[2].toLowerCase()] || months[m[2]];
86
  let y = +m[3]; if(y<100) y+=2000;
87
  if(mo) return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
88
  }
89
  return v;
90
  }
 
 
 
 
 
 
 
 
 
 
91
 
92
- /* ================= تقسيم/استخراج الحقول ================= */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  function splitTickets(raw){
94
  raw = normalizeText(raw);
95
  if(!raw) return [];
@@ -97,67 +128,68 @@ function splitTickets(raw){
97
  if(parts.length===1){ parts = raw.split(/\n\s*\n+/).filter(p=>p.trim()); }
98
  return parts.map(p=>p.trim()).filter(Boolean);
99
  }
100
- function compileFieldPatterns(){
101
- const pats = {};
102
- for(const [canonical, labels] of Object.entries(FIELD_ALIASES)){
103
- const lbls = labels.map(l => l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
104
- pats[canonical] = [
105
- new RegExp(`(?:^|\\n)\\s*(?:${lbls})\\s*${LABEL_SEP}(.+)$`, "mi"),
106
- new RegExp(`(?:^|\\n)\\s*(?:${lbls})\\s*${LABEL_SEP}\\n\\s*(.+)`, "mi"),
107
- ];
108
- }
109
- return pats;
110
- }
111
- const FIELD_PATTERNS = compileFieldPatterns();
112
 
 
113
  function extractFields(ticketText){
114
- const data = {};
115
- for(const k of Object.keys(FIELD_ALIASES)) data[k]="";
116
  const text = normalizeText(ticketText);
 
 
 
 
117
 
118
- for(const [fname, patterns] of Object.entries(FIELD_PATTERNS)){
119
- for(const pat of patterns){
120
- const m = text.match(pat);
121
- if(m){
122
- let val = normalizeText(m[1]);
123
- if(fname==="وقت حدوث المشكلة") val = normalizeTime(val);
124
- if(!data[fname]) data[fname]=val;
125
- break;
126
- }
127
- }
128
- }
129
- if(!data["رقم الجهاز"]){
130
- const m = text.match(/(?:رقم\s*الجهاز|الجهاز)\D*([0-9][0-9\-\s]{2,})/i);
131
- if(m) data["رقم الجهاز"]=m[1].replace(/\D/g,"").slice(0,20);
132
- }
133
- if(!data["رقم الجوال"]){
134
- const m = text.match(/(05[0-9\-\s]{8,12})/);
135
- if(m) data["رقم الجوال"]=m[1].replace(/\D/g,"").slice(0,10);
136
- }
137
- if(!data["رقم الهوية"]){
138
- const m = text.match(/(1[0-9\-\s]{9,12})/);
139
- if(m) data["رقم الهوية"]=m[1].replace(/\D/g,"").slice(0,10);
140
  }
141
- if(!data["المسح"]){
142
- const m = text.match(/(?:اسم\s*المسح|المسح)\s*[::]?\s*(.+)/);
143
- if(m) data["المسح"]=normalizeText(m[1].split(/\r?\n/)[0]);
 
 
 
 
144
  }
145
- const dm = text.match(/(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}|\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2})/);
146
- if(dm){
147
- const date = normalizeDate(dm[1]);
148
- const tm = data["وقت حدوث المشكلة"] || "";
149
- data["وقت حدوث المشكلة"] = `${date} ${tm}`.trim();
 
 
150
  }
151
- return data;
 
 
 
 
 
 
 
 
 
152
  }
153
 
154
- /* ================= التصنيف ================= */
155
  function classifyTicket(text, fields){
156
  const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
157
  for(const label of CLASS_PRIORITY){
158
  const kws = CLASS_RULES[label] || [];
159
  for(const kw of kws){
160
- if(!kw) continue;
161
  const needle = normalizeText(kw).toLowerCase();
162
  if(needle && hay.includes(needle)) return label;
163
  }
@@ -165,14 +197,14 @@ function classifyTicket(text, fields){
165
  return "استفسار";
166
  }
167
 
168
- /* نبني الصفوف مع "التصنيف" + "اسم الدعم الفني" + "الحالة" + تعبئة المنطقة الافتراضية عند الفراغ */
169
  function parseTicketsWithExtras(raw, agentName, defaultRegion){
 
170
  return splitTickets(raw||"").map(t => {
171
  const f = extractFields(t);
172
  const cls = classifyTicket(t, f);
173
 
174
- // املأ المنطقة الافتراضية إذا كانت فارغة
175
- const region = f["المنطقة"] && f["المنطقة"].trim() ? f["المنطقة"] : (defaultRegion || "");
176
 
177
  return {
178
  "التصنيف": cls,
@@ -190,7 +222,7 @@ function parseTicketsWithExtras(raw, agentName, defaultRegion){
190
  });
191
  }
192
 
193
- /* ================= بناء الجدول/القراءة ================= */
194
  function buildTable(rows){
195
  const theadRow = document.getElementById("theadRow");
196
  const tbody = document.getElementById("tbody");
@@ -223,19 +255,15 @@ function readTable(){
223
  return rows;
224
  }
225
 
226
- /* شارة عدّاد داخل زر التحليل */
227
  function updateBadge(n){
228
  const b = document.getElementById("countBadge");
229
  b.textContent = n; b.hidden = (n===0);
230
  }
231
-
232
- /* تمكين/تعطيل الأزرار حسب وجود صفوف */
233
  function setButtonsEnabled(hasRows){
234
  document.getElementById("btn-export").disabled = !hasRows;
235
  document.getElementById("btn-copy").disabled = !hasRows;
236
  }
237
-
238
- /* إبراز الخلايا غير الصحيحة (هوية/جوال) */
239
  function validateCells(){
240
  const tbody=document.getElementById("tbody");
241
  const idxPhone = EXPORT_COLUMNS.indexOf("رقم الجوال");
@@ -254,7 +282,7 @@ function validateCells(){
254
  document.addEventListener("input",(e)=>{
255
  if(e.target && e.target.closest && e.target.closest("#tbody")){
256
  validateCells();
257
- saveState(); // حفظ عند التعديل
258
  }
259
  });
260
 
@@ -266,7 +294,7 @@ function toast(msg){
266
  setTimeout(()=>{ t.hidden = true; }, 2000);
267
  }
268
 
269
- /* ======== تصدير/مشاركة Excel مع حفظ الأصفار كنص (هوية/جهاز/جوال) ======== */
270
  async function exportExcel(){
271
  const rows = readTable();
272
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
@@ -306,7 +334,7 @@ async function exportExcel(){
306
 
307
  if (navigator.canShare && navigator.canShare({ files: [file] })) {
308
  try { await navigator.share({ files: [file], title: "ملف التذاكر" }); toast("تمت المشاركة/الحفظ."); return; }
309
- catch(e){ /* أغلق المشاركة */ }
310
  }
311
  const url = URL.createObjectURL(blob);
312
  const a = document.createElement("a"); a.href = url; a.download = filename;
@@ -315,7 +343,7 @@ async function exportExcel(){
315
  toast("تم تنزيل الملف.");
316
  }
317
 
318
- /* ======== نسخ إلى الحافظة (TSV) مع BOM وحفظ الأصفار قدر الإمكان ======== */
319
  async function copyToClipboardTSV(){
320
  const rows = readTable();
321
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
@@ -329,8 +357,8 @@ async function copyToClipboardTSV(){
329
  return v;
330
  }).join("\t")
331
  ).join("\r\n");
332
-
333
  const tsv = "\uFEFF" + header + "\r\n" + body;
 
334
  try{
335
  await navigator.clipboard.writeText(tsv);
336
  toast("تم النسخ — الصق/ي مباشرة في Excel.");
@@ -342,18 +370,18 @@ async function copyToClipboardTSV(){
342
  }
343
  }
344
 
345
- /* المثال المطلوب (تذكرة واحدة) */
346
  const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
347
- وقت حدوث المشكلة: 21/8/2025
348
  اسم صاحب المشكلة : نوف الناصر
349
  رقم الهوية: 1234567890
350
  رقم الجهاز: 01234
351
  رقم الجوال: 0558174717
352
- اسم المسح: الخبر
353
- المنطقة: الشرقية`;
354
 
355
- /* ================= حفظ واسترجاع الحالة ================= */
356
- const STATE_KEY = "ticketParserState_v3"; // نسخة جديدة لأننا أضفنا أعمدة وخيارات
357
  function ensureColumns(rows, agentName, defaultRegion){
358
  if(!Array.isArray(rows)) return rows||[];
359
  return rows.map(r=>{
@@ -364,7 +392,7 @@ function ensureColumns(rows, agentName, defaultRegion){
364
  }
365
  if(!("اسم الدعم الفني" in out)) out["اسم الدعم الفني"] = agentName || out["اسم الدعم الفني"] || "";
366
  if(!("الحالة" in out) || !out["الحالة"]) out["الحالة"] = "تم الحل";
367
- if((!out["المنطقة"] || !out["المنطقة"].trim()) && defaultRegion) out["المنطقة"] = defaultRegion;
368
  return out;
369
  });
370
  }
@@ -375,8 +403,7 @@ function saveState(){
375
  const agent = document.getElementById("agentName")?.value || "";
376
  const region= document.getElementById("regionDefault")?.value || "";
377
  const rows = readTable();
378
- const state = { raw, fname, agent, region, rows };
379
- localStorage.setItem(STATE_KEY, JSON.stringify(state));
380
  }catch{}
381
  }
382
  function loadState(){
@@ -390,29 +417,21 @@ function loadState(){
390
  if(typeof region === "string"){ const el=document.getElementById("regionDefault"); if(el) el.value = region; }
391
  rows = ensureColumns(rows, agent, region);
392
  if(Array.isArray(rows) && rows.length){
393
- buildTable(rows);
394
- validateCells();
395
- updateBadge(rows.length);
396
- setButtonsEnabled(true);
397
  }
398
  return true;
399
  }catch{ return false; }
400
  }
401
- function clearState(){
402
- try{ localStorage.removeItem(STATE_KEY); }catch{}
403
- }
404
-
405
- /* تنظيف واجهة فقط (يُستدعى من زر مسح) */
406
  function wipeUI(){
407
  const rawEl = document.getElementById("raw");
408
  const tbody = document.getElementById("tbody");
409
  if(rawEl) rawEl.value = "";
410
  if(tbody) tbody.innerHTML = "";
411
- updateBadge(0);
412
- setButtonsEnabled(false);
413
  }
414
 
415
- /* تهيئة وربط الأزرار + اختصارات */
416
  function init(){
417
  const parseBtn = document.getElementById("btn-parse");
418
  const exportBtn = document.getElementById("btn-export");
@@ -424,7 +443,6 @@ function init(){
424
  const agentEl = document.getElementById("agentName");
425
  const regionEl = document.getElementById("regionDefault");
426
 
427
- // استرجاع الحالة المخزنة
428
  loadState();
429
 
430
  parseBtn.addEventListener("click", ()=>{
@@ -432,22 +450,18 @@ function init(){
432
  const agent = agentEl.value || "";
433
  const defRegion = regionEl.value || "";
434
  const rows = parseTicketsWithExtras(raw, agent, defRegion);
435
- buildTable(rows);
436
- validateCells();
437
- updateBadge(rows.length);
438
- setButtonsEnabled(rows.length>0);
439
  saveState();
440
- toast(`تم استخراج ${rows.length} ${rows.length===1 ? "تذكرة" : "تذاكر"}.`);
 
441
  });
442
 
443
  exportBtn.addEventListener("click", exportExcel);
444
  copyBtn.addEventListener("click", copyToClipboardTSV);
445
 
446
  clearBtn.addEventListener("click", ()=>{
447
- clearState();
448
- wipeUI();
449
- // لا نمسح اسم الدعم والمنطقة الافتراضية لراحَتِك: فقط نمسح المدخلات والجدول
450
- toast("تم مسح كل البيانات.");
451
  });
452
 
453
  sampleBtn.addEventListener("click", ()=>{ rawEl.value = SAMPLE; saveState(); });
@@ -457,7 +471,6 @@ function init(){
457
  agentEl.addEventListener("input", saveState);
458
  regionEl.addEventListener("change", saveState);
459
 
460
- // اختصارات لوحة المفاتيح
461
  document.addEventListener("keydown", (e)=>{
462
  const ctrl = e.ctrlKey || e.metaKey;
463
  if(ctrl && e.key === "Enter"){ e.preventDefault(); parseBtn.click(); }
 
1
  /* ========= منطق التحليل والتصدير والنسخ (Static فقط) ========= */
2
+ /* أعمدة الجدول */
3
  const EXPORT_COLUMNS = [
4
+ "التصنيف",
5
  "نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
6
  "رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة",
7
+ "اسم الدعم الفني","الحالة"
 
8
  ];
9
 
10
+ /* المرادفات */
11
  const FIELD_ALIASES = {
12
  "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
13
  "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
 
16
  "رقم الجهاز": ["رقم الجهاز","الجهاز"],
17
  "رقم الجوال": ["رقم الجوال","الجوال","الهاتف"],
18
  "المسح": ["المسح","اسم المسح"],
19
+ "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
20
  };
21
 
22
+ /* تصنيف بالكلمات المفتاحية */
 
 
 
 
23
  const CLASS_RULES = {
24
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
25
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
 
41
  "النظام المكتبي","تناقل البيانات","استفسار",
42
  ];
43
 
44
+ /* أدوات نص */
45
+ const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
46
+ const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
47
  function normalizeText(s){
48
  if(typeof s!=="string") return "";
49
  return s.replace(/[\u200f\u200e\u202a-\u202e\u2066-\u2069\u00a0]/g," ")
 
51
  .replace(/[ــ]+/g,"")
52
  .trim();
53
  }
54
+ function lettersOnly(ar){
55
+ return (ar||"").replace(/[^A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]/g,"").replace(/\s{2,}/g," ").trim();
56
+ }
57
+ function alnumAr(s){
58
+ return (s||"").replace(/[^0-9A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s\-\._/]/g,"").replace(/\s{2,}/g," ").trim();
59
+ }
60
+ function digitsOnly(s){ return (s||"").replace(/\D+/g,""); }
61
+
62
  function normalizeTime(val){
63
+ const m = (val||"").match(/(\d{1,2})[:٫\.\-:](\d{2})\s*(ص|م|am|pm)?/i);
64
  if(!m) return (val||"").trim();
65
+ let h = parseInt(m[1],10), mn = m[2], ampm = (m[3]||"").toLowerCase();
66
  if(ampm){
67
+ if((/م|pm/).test(ampm) && h<12) h+=12;
68
+ if((/ص|am/).test(ampm) && h===12) h=0;
69
  }
70
  return `${String(h).padStart(2,"0")}:${mn}`;
71
  }
 
88
  const rx = new RegExp(`(\\d{1,2})\\s+(${Object.keys(months).join("|")})\\s+(\\d{2,4})`,"i");
89
  m = v.match(rx);
90
  if(m){
91
+ const d = +m[1]; const mo = months[(m[2]||"").toLowerCase()] || months[m[2]];
92
  let y = +m[3]; if(y<100) y+=2000;
93
  if(mo) return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
94
  }
95
  return v;
96
  }
97
+ function parseDateTime(raw){
98
+ const t = normalizeText(raw);
99
+ const d = normalizeDate(t);
100
+ const timeMatch = t.match(/(\d{1,2})[:٫\.\-:](\d{2})\s*(ص|م|am|pm)?/i);
101
+ const time = timeMatch ? normalizeTime(timeMatch[0]) : "";
102
+ if(d && /^\d{4}-\d{2}-\d{2}$/.test(d) && time) return `${d} ${time}`;
103
+ if(d && /^\d{4}-\d{2}-\d{2}$/.test(d)) return d;
104
+ if(time) return time;
105
+ return t; // إن لم نتعرّف صيغة
106
+ }
107
 
108
+ /* البحث عن قيمة بعد مسافة أو بعد نقطتين */
109
+ function findAfterLabel(text, labels){
110
+ const hay = "\n" + normalizeText(text) + "\n";
111
+ for(const rawLbl of labels){
112
+ const lbl = rawLbl.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
113
+ // 1) label : value
114
+ let m = hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s*[::]\\s*([^\\n]+)`, "i"));
115
+ if(m) return m[1].trim();
116
+ // 2) label value (بعد مسافة)
117
+ m = hay.match(new RegExp(`(?:^|\\n)\\s*${lbl}\\s+([^\\n]+)`, "i"));
118
+ if(m) return m[1].trim();
119
+ }
120
+ return "";
121
+ }
122
+
123
+ /* تقسيم التذاكر */
124
  function splitTickets(raw){
125
  raw = normalizeText(raw);
126
  if(!raw) return [];
 
128
  if(parts.length===1){ parts = raw.split(/\n\s*\n+/).filter(p=>p.trim()); }
129
  return parts.map(p=>p.trim()).filter(Boolean);
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ /* استخراج الحقول بدقّة الأنواع */
133
  function extractFields(ticketText){
 
 
134
  const text = normalizeText(ticketText);
135
+ const out = {
136
+ "نوع المشكلة":"", "وقت حدوث المشكلة":"", "اسم صاحب المشكلة":"",
137
+ "رقم الهوية":"", "رقم الجهاز":"", "رقم الجوال":"", "المسح":"", "المنطقة":""
138
+ };
139
 
140
+ // نوع المشكلة: حروف/أرقام/كلاهما
141
+ let v = findAfterLabel(text, FIELD_ALIASES["نوع المشكلة"]);
142
+ if(v) out["نوع المشكلة"] = alnumAr(v);
143
+
144
+ // وقت حدوث المشكلة: تاريخ أو وقت أو كلاهما
145
+ v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
146
+ if(v) out["وقت حدوث المشكلة"] = parseDateTime(v);
147
+
148
+ // اسم صاحب المشكلة: نتركه كما هو (نظيف)
149
+ v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
150
+ if(v) out["اسم صاحب المشكلة"] = v;
151
+
152
+ // رقم الهوية: أرقام فقط
153
+ v = findAfterLabel(text, FIELD_ALIASES["رقم الهوية"]);
154
+ if(v) out["رقم الهوية"] = digitsOnly(v);
155
+ if(!out["رقم الهوية"]){
156
+ const m = text.match(/(?:^|\D)(1\d{9})(?:\D|$)/);
157
+ if(m) out["رقم الهوية"] = m[1];
 
 
 
 
158
  }
159
+
160
+ // رقم الجهاز: أرقام فقط
161
+ v = findAfterLabel(text, FIELD_ALIASES["رقم الجهاز"]);
162
+ if(v) out["رقم الجهاز"] = digitsOnly(v);
163
+ if(!out["رقم الجهاز"]){
164
+ const m = text.match(/(?:^|\D)(\d{5,20})(?:\D|$)/); // احتياط
165
+ if(m) out["رقم الجهاز"] = m[1];
166
  }
167
+
168
+ // رقم الجوال: أرقام فقط
169
+ v = findAfterLabel(text, FIELD_ALIASES["رقم الجوال"]);
170
+ if(v) out["رقم الجوال"] = digitsOnly(v);
171
+ if(!out["رقم الجوال"]){
172
+ const m = text.match(/(?:^|\D)(05\d{8})(?:\D|$)/);
173
+ if(m) out["رقم الجوال"] = m[1];
174
  }
175
+
176
+ // اسم المسح: حروف/أرقام/كلاهما
177
+ v = findAfterLabel(text, FIELD_ALIASES["المسح"]);
178
+ if(v) out["المسح"] = alnumAr(v);
179
+
180
+ // اسم المنطقة: إن لم تُختر من القائمة لاحقًا نأخذها من التذكرة "حروف فقط"
181
+ v = findAfterLabel(text, FIELD_ALIASES["المنطقة"]);
182
+ if(v) out["المنطقة"] = lettersOnly(v);
183
+
184
+ return out;
185
  }
186
 
187
+ /* التصنيف */
188
  function classifyTicket(text, fields){
189
  const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
190
  for(const label of CLASS_PRIORITY){
191
  const kws = CLASS_RULES[label] || [];
192
  for(const kw of kws){
 
193
  const needle = normalizeText(kw).toLowerCase();
194
  if(needle && hay.includes(needle)) return label;
195
  }
 
197
  return "استفسار";
198
  }
199
 
200
+ /* المنطقة المختارة تُطبّق على الجميع، وإلا نستخدم ما في التذكرة (حروف فقط) */
201
  function parseTicketsWithExtras(raw, agentName, defaultRegion){
202
+ const regionChosen = (defaultRegion || "").toString(); // مثال: "4. الشرقية"
203
  return splitTickets(raw||"").map(t => {
204
  const f = extractFields(t);
205
  const cls = classifyTicket(t, f);
206
 
207
+ const region = regionChosen ? regionChosen : (f["المنطقة"] || "");
 
208
 
209
  return {
210
  "التصنيف": cls,
 
222
  });
223
  }
224
 
225
+ /* بناء الجدول/القراءة */
226
  function buildTable(rows){
227
  const theadRow = document.getElementById("theadRow");
228
  const tbody = document.getElementById("tbody");
 
255
  return rows;
256
  }
257
 
258
+ /* الشارة + الأزرار + التحقق */
259
  function updateBadge(n){
260
  const b = document.getElementById("countBadge");
261
  b.textContent = n; b.hidden = (n===0);
262
  }
 
 
263
  function setButtonsEnabled(hasRows){
264
  document.getElementById("btn-export").disabled = !hasRows;
265
  document.getElementById("btn-copy").disabled = !hasRows;
266
  }
 
 
267
  function validateCells(){
268
  const tbody=document.getElementById("tbody");
269
  const idxPhone = EXPORT_COLUMNS.indexOf("رقم الجوال");
 
282
  document.addEventListener("input",(e)=>{
283
  if(e.target && e.target.closest && e.target.closest("#tbody")){
284
  validateCells();
285
+ saveState();
286
  }
287
  });
288
 
 
294
  setTimeout(()=>{ t.hidden = true; }, 2000);
295
  }
296
 
297
+ /* تصدير Excel مع حفظ الأصفار كنص للأعمدة الرقمية */
298
  async function exportExcel(){
299
  const rows = readTable();
300
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
 
334
 
335
  if (navigator.canShare && navigator.canShare({ files: [file] })) {
336
  try { await navigator.share({ files: [file], title: "ملف التذاكر" }); toast("تمت المشاركة/الحفظ."); return; }
337
+ catch(e){}
338
  }
339
  const url = URL.createObjectURL(blob);
340
  const a = document.createElement("a"); a.href = url; a.download = filename;
 
343
  toast("تم تنزيل الملف.");
344
  }
345
 
346
+ /* نسخ إلى الحافظة (TSV) مع BOM */
347
  async function copyToClipboardTSV(){
348
  const rows = readTable();
349
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
 
357
  return v;
358
  }).join("\t")
359
  ).join("\r\n");
 
360
  const tsv = "\uFEFF" + header + "\r\n" + body;
361
+
362
  try{
363
  await navigator.clipboard.writeText(tsv);
364
  toast("تم النسخ — الصق/ي مباشرة في Excel.");
 
370
  }
371
  }
372
 
373
+ /* مثال */
374
  const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
375
+ وقت حدوث المشكلة: 21/8/2025 10:35 ص
376
  اسم صاحب المشكلة : نوف الناصر
377
  رقم الهوية: 1234567890
378
  رقم الجهاز: 01234
379
  رقم الجوال: 0558174717
380
+ اسم المسح: الخبر 2025
381
+ اسم المنطقة: الشرقية`;
382
 
383
+ /* حفظ الحالة */
384
+ const STATE_KEY = "ticketParserState_v4";
385
  function ensureColumns(rows, agentName, defaultRegion){
386
  if(!Array.isArray(rows)) return rows||[];
387
  return rows.map(r=>{
 
392
  }
393
  if(!("اسم الدعم الفني" in out)) out["اسم الدعم الفني"] = agentName || out["اسم الدعم الفني"] || "";
394
  if(!("الحالة" in out) || !out["الحالة"]) out["الحالة"] = "تم الحل";
395
+ if(defaultRegion) out["المنطقة"] = defaultRegion; // طبّق المختارة على الجميع
396
  return out;
397
  });
398
  }
 
403
  const agent = document.getElementById("agentName")?.value || "";
404
  const region= document.getElementById("regionDefault")?.value || "";
405
  const rows = readTable();
406
+ localStorage.setItem(STATE_KEY, JSON.stringify({ raw, fname, agent, region, rows }));
 
407
  }catch{}
408
  }
409
  function loadState(){
 
417
  if(typeof region === "string"){ const el=document.getElementById("regionDefault"); if(el) el.value = region; }
418
  rows = ensureColumns(rows, agent, region);
419
  if(Array.isArray(rows) && rows.length){
420
+ buildTable(rows); validateCells(); updateBadge(rows.length); setButtonsEnabled(true);
 
 
 
421
  }
422
  return true;
423
  }catch{ return false; }
424
  }
425
+ function clearState(){ try{ localStorage.removeItem(STATE_KEY); }catch{} }
 
 
 
 
426
  function wipeUI(){
427
  const rawEl = document.getElementById("raw");
428
  const tbody = document.getElementById("tbody");
429
  if(rawEl) rawEl.value = "";
430
  if(tbody) tbody.innerHTML = "";
431
+ updateBadge(0); setButtonsEnabled(false);
 
432
  }
433
 
434
+ /* تهيئة */
435
  function init(){
436
  const parseBtn = document.getElementById("btn-parse");
437
  const exportBtn = document.getElementById("btn-export");
 
443
  const agentEl = document.getElementById("agentName");
444
  const regionEl = document.getElementById("regionDefault");
445
 
 
446
  loadState();
447
 
448
  parseBtn.addEventListener("click", ()=>{
 
450
  const agent = agentEl.value || "";
451
  const defRegion = regionEl.value || "";
452
  const rows = parseTicketsWithExtras(raw, agent, defRegion);
453
+ buildTable(rows); validateCells();
454
+ updateBadge(rows.length); setButtonsEnabled(rows.length>0);
 
 
455
  saveState();
456
+ if(defRegion) toast(`تم استخراج ${rows.length} تذكرة وتطبيق المنطقة: ${defRegion}`);
457
+ else toast(`تم استخراج ${rows.length} تذكرة.`);
458
  });
459
 
460
  exportBtn.addEventListener("click", exportExcel);
461
  copyBtn.addEventListener("click", copyToClipboardTSV);
462
 
463
  clearBtn.addEventListener("click", ()=>{
464
+ clearState(); wipeUI(); toast("تم مسح كل البيانات.");
 
 
 
465
  });
466
 
467
  sampleBtn.addEventListener("click", ()=>{ rawEl.value = SAMPLE; saveState(); });
 
471
  agentEl.addEventListener("input", saveState);
472
  regionEl.addEventListener("change", saveState);
473
 
 
474
  document.addEventListener("keydown", (e)=>{
475
  const ctrl = e.ctrlKey || e.metaKey;
476
  if(ctrl && e.key === "Enter"){ e.preventDefault(); parseBtn.click(); }