stat2025 commited on
Commit
36249d2
·
verified ·
1 Parent(s): 20b8e22

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +214 -75
app.js CHANGED
@@ -1,24 +1,24 @@
 
1
 
2
  const EXPORT_COLUMNS = [
3
  "التصنيف","نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
4
  "رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة","اسم الدعم الفني","الحالة"
5
  ];
6
 
 
7
  const FIELD_ALIASES = {
8
- "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
9
- "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث","وقت حدوث المشكله:"],
10
  "اسم صاحب المشكلة": ["اسم صاحب المشكله","اسم صاحب المشكلة","اسم صاحب البلاغ","الاسم"],
11
- "رقم الهوية": ["رقم الهويه","رقم الهوية","الهوية"],
12
  "رقم الجهاز": ["رقم الجهاز","الجهاز"],
13
- "رقم الجوال": ["رقم الجوال","الجوال","الهاتف"],
14
  "المسح": ["المسح","اسم المسح"],
15
  "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
16
  };
17
-
18
  const START_LABELS = Array.from(new Set(Object.values(FIELD_ALIASES).flat()));
19
- const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
20
- const MIN_SPLIT_SPAN = 40;
21
 
 
22
  const CLASS_RULES = {
23
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
24
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
@@ -40,64 +40,122 @@ const CLASS_PRIORITY = [
40
  "النظام المكتبي","تناقل البيانات","استفسار",
41
  ];
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");
@@ -105,7 +163,6 @@ function findStartsByLabels(text, labels){
105
  while((m = re.exec(text))){ idxs.push(m.index + (m[1]?m[1].length:0)); }
106
  return idxs;
107
  }
108
-
109
  function findAfterLabel(text, labels){
110
  const hay = "\n" + normalizeText(text) + "\n";
111
  for(const rawLbl of labels){
@@ -117,7 +174,7 @@ function findAfterLabel(text, labels){
117
  }
118
  return "";
119
  }
120
-
121
  function findBlockAfterLabel(text, labels, allLabels = START_LABELS){
122
  const hay = "\n" + normalizeText(text) + "\n";
123
  const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
@@ -131,10 +188,18 @@ function findBlockAfterLabel(text, labels, allLabels = START_LABELS){
131
  return m ? m[1].trim() : "";
132
  }
133
 
 
 
 
 
 
 
134
  function splitTickets(raw){
135
- const text = normalizeText(raw);
136
- if(!text) return [];
 
137
 
 
138
  const niu = findStartsByLabels(text, ["نوع المشكلة","نوع المشكله"]).sort((a,b)=>a-b);
139
  if(niu.length >= 2){
140
  const parts=[];
@@ -147,12 +212,12 @@ function splitTickets(raw){
147
  if(parts.length) return parts;
148
  }
149
 
150
-
151
  if(TICKET_SEP.test(text)){
152
  return text.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
153
  }
154
 
155
-
156
  const startsAll = findStartsByLabels(text, START_LABELS).sort((a,b)=>a-b);
157
  const filtered = startsAll.filter((pos,i,arr)=> i===0 || (pos - arr[i-1]) >= MIN_SPLIT_SPAN);
158
  if(filtered.length >= 2){
@@ -166,15 +231,16 @@ function splitTickets(raw){
166
  if(parts.length) return parts;
167
  }
168
 
169
-
170
  const blocks = text.split(/\n\s*\n+/).map(p=>p.trim()).filter(Boolean);
171
  if(blocks.length > 1) return blocks;
172
 
173
  return [text];
174
  }
175
 
 
176
  function extractFields(ticketText){
177
- const text = normalizeText(ticketText);
178
  const out = {
179
  "نوع المشكلة":"", "وقت حدوث المشكلة":"", "اسم صاحب المشكلة":"",
180
  "رقم الهوية":"", "رقم الجهاز":"", "رقم الجوال":"", "المسح":"", "المنطقة":""
@@ -184,7 +250,7 @@ function extractFields(ticketText){
184
  if(v) out["نوع المشكلة"] = normalizeText(v);
185
 
186
  v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
187
- if(v) out["وقت حدوث المشكلة"] = parseDateTime(v);
188
 
189
  v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
190
  if(v) out["اسم صاحب المشكلة"] = v;
@@ -219,6 +285,7 @@ function extractFields(ticketText){
219
  return out;
220
  }
221
 
 
222
  function classifyTicket(text, fields){
223
  const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
224
  for(const label of CLASS_PRIORITY){
@@ -230,7 +297,16 @@ function classifyTicket(text, fields){
230
  }
231
  return "استفسار";
232
  }
 
 
 
 
 
 
 
 
233
 
 
234
  function parseTicketsWithExtras(raw, agentName, defaultRegion){
235
  const regionChosen = (defaultRegion || "").toString();
236
  return splitTickets(raw||"").map(t => {
@@ -253,6 +329,7 @@ function parseTicketsWithExtras(raw, agentName, defaultRegion){
253
  });
254
  }
255
 
 
256
  function buildTable(rows){
257
  const theadRow = document.getElementById("theadRow");
258
  const tbody = document.getElementById("tbody");
@@ -267,25 +344,42 @@ function buildTable(rows){
267
  const tr=document.createElement("tr");
268
  EXPORT_COLUMNS.forEach(col=>{
269
  const td=document.createElement("td");
270
- td.contentEditable="true";
271
- td.textContent = r[col]||"";
 
 
 
 
 
 
 
 
272
  tr.appendChild(td);
273
  });
274
  tbody.appendChild(tr);
275
  });
276
  }
277
 
 
278
  function readTable(){
279
  const tbody = document.getElementById("tbody");
280
  const rows = [];
281
  [...tbody.querySelectorAll("tr")].forEach(tr=>{
282
  const obj={};
283
- [...tr.children].forEach((td,idx)=>{ obj[EXPORT_COLUMNS[idx]] = td.textContent.trim(); });
 
 
 
 
 
 
 
284
  rows.push(obj);
285
  });
286
  return rows;
287
  }
288
 
 
289
  function updateBadge(n){
290
  const b = document.getElementById("countBadge");
291
  b.textContent = n; b.hidden = (n===0);
@@ -329,6 +423,7 @@ function toast(msg){
329
  setTimeout(()=>{ t.hidden = true; }, 2000);
330
  }
331
 
 
332
  async function exportExcel(){
333
  const rows = readTable();
334
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
@@ -416,6 +511,7 @@ async function exportExcel(){
416
  toast("تم تنزيل الملف بتنسيق القالب.");
417
  }
418
 
 
419
  async function copyToClipboardTSV(){
420
  const rows = readTable();
421
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
@@ -439,21 +535,22 @@ async function copyToClipboardTSV(){
439
  }
440
  }
441
 
442
- /* مثال جاهز */
443
- const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
444
- وقت حدوث المشكلة: 21/8/2025 7 صباحا
445
- اسم صاحب المشكلة : نوف الناصر
446
  رقم الهوية: 1234567890
447
  رقم الجهاز: 01234
448
  رقم الجوال: 0558174717
449
- اسم المسح: الخبر 2025
450
- اسم المنطقة: الشرقية`;
451
 
452
- const STATE_KEY = "ticketParserState_v10_6";
 
453
  const ALL_STATE_KEYS = [
454
  "ticketParserState_v8","ticketParserState_v9","ticketParserState_v10",
455
  "ticketParserState_v10_1","ticketParserState_v10_2","ticketParserState_v10_3",
456
- "ticketParserState_v10_5","ticketParserState_v10_6"
457
  ];
458
 
459
  function ensureColumns(rows, agentName, defaultRegion){
@@ -476,17 +573,18 @@ function saveState(){
476
  const agent = document.getElementById("agentName")?.value || "";
477
  const region= document.getElementById("regionDefault")?.value || "";
478
  const rows = readTable();
479
- localStorage.setItem(STATE_KEY, JSON.stringify({ raw, agent, region, rows }));
480
  }catch{}
481
  }
482
  function loadState(){
483
  try{
484
  const s = localStorage.getItem(STATE_KEY);
485
  if(!s) return false;
486
- let { raw, agent, region, rows } = JSON.parse(s);
487
  if(typeof raw === "string"){ const el=document.getElementById("raw"); if(el) el.value = raw; }
488
  if(typeof agent === "string"){ const el=document.getElementById("agentName"); if(el) el.value = agent; }
489
  if(typeof region === "string"){ const el=document.getElementById("regionDefault"); if(el) el.value = region; }
 
490
  rows = ensureColumns(rows, agent, region);
491
  if(Array.isArray(rows) && rows.length){
492
  buildTable(rows); validateCells(); updateBadge(rows.length); setButtonsEnabled(true);
@@ -495,32 +593,53 @@ function loadState(){
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
- try{ ALL_STATE_KEYS.forEach(k=>localStorage.removeItem(k)); }catch{}
509
- toast("تم مسح كل البيانات والتخزين.");
510
  }
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  function init(){
513
  const parseBtn = document.getElementById("btn-parse");
514
  const exportBtn = document.getElementById("btn-export");
515
  const copyBtn = document.getElementById("btn-copy");
516
  const clearBtn = document.getElementById("btn-clear");
517
  const sampleBtn = document.getElementById("btn-sample");
 
 
 
518
  const rawEl = document.getElementById("raw");
519
  const agentEl = document.getElementById("agentName");
520
  const regionEl = document.getElementById("regionDefault");
521
 
522
  loadState();
523
 
 
 
524
  parseBtn.addEventListener("click", ()=>{
525
  const raw = (rawEl.value || "").trim();
526
  if(!raw){ toast("فضلاً الصق/ي تذاكر أولاً."); return; }
@@ -535,13 +654,33 @@ function init(){
535
 
536
  exportBtn.addEventListener("click", exportExcel);
537
  copyBtn.addEventListener("click", copyToClipboardTSV);
538
- clearBtn.addEventListener("click", clearAll);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  sampleBtn.addEventListener("click", ()=>{
540
- rawEl.value = SAMPLE;
 
541
  saveState();
542
  toast("تم إدراج المثال.");
543
  });
544
 
 
 
 
 
 
545
  rawEl.addEventListener("input", saveState);
546
  agentEl.addEventListener("input", saveState);
547
  regionEl.addEventListener("change", saveState);
@@ -551,7 +690,7 @@ function init(){
551
  if(ctrl && e.key === "Enter"){ e.preventDefault(); parseBtn.click(); }
552
  else if(ctrl && e.key.toLowerCase() === "e"){ e.preventDefault(); exportBtn.click(); }
553
  else if(ctrl && e.shiftKey && e.key.toLowerCase() === "c"){ e.preventDefault(); copyBtn.click(); }
554
- else if(e.key === "Escape"){ e.preventDefault(); clearAll(); }
555
  });
556
 
557
  setButtonsEnabled(!!document.getElementById("tbody")?.children.length);
 
1
+ /* v10.7 — تنظيف + تصحيح مسميات + شارات تصنيف + دمج مكررات + وضع داكن + تاريخ فقط (يدعم تحويل الهجري) */
2
 
3
  const EXPORT_COLUMNS = [
4
  "التصنيف","نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
5
  "رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة","اسم الدعم الفني","الحالة"
6
  ];
7
 
8
+ /* ------- قاموس الحقول والمرادفات ------- */
9
  const FIELD_ALIASES = {
10
+ "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة","نوع-المشكلة","نوع المشكلة"],
11
+ "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث","وقت حدوث المشكله:","وقت حدوث المشكله :"],
12
  "اسم صاحب المشكلة": ["اسم صاحب المشكله","اسم صاحب المشكلة","اسم صاحب البلاغ","الاسم"],
13
+ "رقم الهوية": ["رقم الهويه","رقم الهوية","الهوية","هوية"],
14
  "رقم الجهاز": ["رقم الجهاز","الجهاز"],
15
+ "رقم الجوال": ["رقم الجوال","الجوال","الهاتف","جوال"],
16
  "المسح": ["المسح","اسم المسح"],
17
  "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
18
  };
 
19
  const START_LABELS = Array.from(new Set(Object.values(FIELD_ALIASES).flat()));
 
 
20
 
21
+ /* ------- تصنيف ------- */
22
  const CLASS_RULES = {
23
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
24
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
 
40
  "النظام المكتبي","تناقل البيانات","استفسار",
41
  ];
42
 
43
+ /* ------- إعدادات عامة ------- */
44
+ const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
45
+ const MIN_SPLIT_SPAN = 40;
46
  const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
47
+
48
+ /* ------- أدوات نص ------- */
49
  function normalizeText(s){
50
  if(typeof s!=="string") return "";
51
  return s.replace(/\r\n/g,"\n")
52
  .replace(/[\u200f\u200e\u202a-\u202e\u2066-\u2069\u00a0]/g," ")
53
  .replace(/[٠-٩]/g, d => arabicDigitsMap[d] )
54
  .replace(/[ــ]+/g,"")
55
+ .replace(/[ \t]+\n/g,"\n")
56
+ .replace(/\n{3,}/g,"\n\n")
57
  .trim();
58
  }
59
  function lettersOnly(ar){ return (ar||"").replace(/[^A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]/g,"").replace(/\s{2,}/g," ").trim(); }
60
  function alnumAr(s){ return (s||"").replace(/[^0-9A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s\-\._/]/g,"").replace(/\s{2,}/g," ").trim(); }
61
  function digitsOnly(s){ return (s||"").replace(/\D+/g,""); }
62
 
63
+ /* ------- تصحيح شائع للمسميات (اقتراح 7) ------- */
64
+ const LABEL_FIXES = [
65
+ [/نوع\s*المشكله/gi, "نوع المشكلة"],
66
+ [/وقت\s*حدوث\s*المشكله/gi, "وقت حدوث المشكلة"],
67
+ [/اسم\s*صاحب\s*المشكله/gi, "اسم صاحب المشكلة"],
68
+ [/رقم\s*الهويه/gi, "رقم الهوية"],
69
+ [/المنطقه/gi, "المنطقة"],
70
+ [/اسم\s*المنطقة/gi, "المنطقة"],
71
+ [/اسم\s*المسح/gi, "المسح"],
72
+ [/الهاتف/gi, "رقم الجوال"],
73
+ [/جوال/gi, "رقم الجوال"],
74
+ ];
75
+ function fixLabels(s){
76
+ let t = s;
77
+ LABEL_FIXES.forEach(([re, rep])=> t = t.replace(re, rep));
78
+ return t;
79
+ }
80
+
81
+ /* ------- تواريخ: نُخرج "تاريخ فقط" + تحويل هجري -> ميلادي (اقتراح 5) ------- */
82
+
83
+ // أسماء الشهور الهجرية
84
+ const H_MONTHS = ["محرم","صفر","ربيع الأول","ربيع الاول","ربيع الآخر","ربيع الاخر","جمادى الأولى","جمادى الاولى","جمادى الآخرة","جمادى الاخرة","رجب","شعبان","رمضان","شوال","ذو القعدة","ذو القعده","ذو الحجة","ذو الحجه"];
85
+ function monthIndexHijri(name){
86
+ const i = H_MONTHS.findIndex(m => new RegExp("^"+m+"$", "i").test(name.trim()));
87
+ if(i<0) return -1;
88
+ // تحويل البدائل لرقم 1..12
89
+ 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};
90
+ return map[i] || -1;
91
+ }
92
+
93
+ // خوارزمية التحويل (Islamic Civil Calendar) — تقريب جيد
94
+ function hijriToGregorian(hy, hm, hd){
95
+ // hm: 1..12
96
+ // تحويل إلى يوم جولياني تقريبي
97
+ const jd = Math.floor((11*hy + 3)/30) + 354*hy + 30*hm - Math.floor((hm-1)/2) + hd + 1948440 - 385;
98
+ // تحويل JD إلى ميلادي
99
+ let l = jd + 68569;
100
+ let n = Math.floor(4*l/146097); l = l - Math.floor((146097*n + 3)/4);
101
+ let i = Math.floor(4000*(l+1)/1461001); l = l - Math.floor(1461*i/4) + 31;
102
+ let j = Math.floor(80*l/2447); const d = l - Math.floor(2447*j/80);
103
+ l = Math.floor(j/11); const m = j + 2 - 12*l; const y = 100*(n-49) + i + l;
104
+ return [y,m,d]; // [yyyy, mm, dd]
105
+ }
106
+
107
+ function detectHijriDate(str){
108
+ const t = normalizeText(str);
109
+ // صيغ بأسماء الشهور الهجرية: 10 رمضان 1446 هـ
110
+ let m = t.match(/(\d{1,2})\s+([^\s]+)\s+(\d{3,4})\s*(هـ|ه|هجري)?/i);
111
  if(m){
112
+ const d = +m[1];
113
+ const hm = monthIndexHijri(m[2]);
114
+ const y = +m[3];
115
+ if(hm>=1 && hm<=12) return {hy:y, hm:hm, hd:d};
 
116
  }
117
+ // صيغ رقمية مع هـ: 10/09/1446 هـ
118
+ m = t.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{3,4})\s*(هـ|ه|هجري)/i);
119
+ if(m){ return {hy:+m[3], hm:+m[2], hd:+m[1]}; }
120
+ return null;
121
  }
122
+
123
+ function normalizeDateOnly(raw){
124
+ const t = normalizeText(raw);
125
+
126
+ // لو كان "وقت فقط" (مثل 5:00 م أو 11:13) نُعيد فارغ
127
+ if(/(^|\s)(\d{1,2})([:٫\.:\-]\d{2})\s*(ص|صباح|صباحا|am|م|مساء|pm)?($|\s)/i.test(t)
128
+ && !/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/.test(t)
129
+ && !/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/.test(t)
130
+ && !detectHijriDate(t)
131
+ ){
132
+ return ""; // وقت فقط
133
+ }
134
+
135
+ // هجري بالأسماء/هـ
136
+ const hj = detectHijriDate(t);
137
+ if(hj){
138
+ const [gy,gm,gd] = hijriToGregorian(hj.hy, hj.hm, hj.hd);
139
+ return `${gy.toString().padStart(4,"0")}-${String(gm).padStart(2,"0")}-${String(gd).padStart(2,"0")}`;
140
+ }
141
+
142
+ // Gregorian: dd/mm/yyyy أو yyyy-mm-dd (+ تجاهل الوقت)
143
+ let m = t.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/);
144
  if(m){
145
  let d=+m[1], mo=+m[2], y=+m[3]; if(y<100) y+=2000;
146
  return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).toString().padStart(2,"0")}`;
147
  }
148
+ m = t.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
149
  if(m){
150
  let y=+m[1], mo=+m[2], d=+m[3];
151
  return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).toString().padStart(2,"0")}`;
152
  }
153
+
154
+ // لو لم نتعرف على تاريخ — نُعيد النص كما هو (قد يكون وصف)
 
 
 
 
 
 
 
155
  return t;
156
  }
157
 
158
+ /* ------- أدوات البحث عن الحقول ------- */
159
  function findStartsByLabels(text, labels){
160
  const lblRe = labels.map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
161
  const re = new RegExp(`(^|\\n)\\s*(?:[-–—\\*•]+|\\d+[\\)\\.]\\s*)?\\s*(?:${lblRe})(?:\\s*[::]|\\s+)`,"gi");
 
163
  while((m = re.exec(text))){ idxs.push(m.index + (m[1]?m[1].length:0)); }
164
  return idxs;
165
  }
 
166
  function findAfterLabel(text, labels){
167
  const hay = "\n" + normalizeText(text) + "\n";
168
  for(const rawLbl of labels){
 
174
  }
175
  return "";
176
  }
177
+ // يلتقط نصًا متعدد الأسطر حتى أول ليبل تالٍ
178
  function findBlockAfterLabel(text, labels, allLabels = START_LABELS){
179
  const hay = "\n" + normalizeText(text) + "\n";
180
  const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
 
188
  return m ? m[1].trim() : "";
189
  }
190
 
191
+ /* ------- التقسيم -------
192
+ 1) تكرار "نوع المشكلة" (لتذاكر متلاصقة بلا مسافة)
193
+ 2) فواصل 🔴/خطوط
194
+ 3) بدايات الحقول (span)
195
+ 4) فواصل فقرات
196
+ --------------------------------*/
197
  function splitTickets(raw){
198
+ const text0 = normalizeText(fixLabels(raw));
199
+ if(!text0) return [];
200
+ const text = text0;
201
 
202
+ // (1)
203
  const niu = findStartsByLabels(text, ["نوع المشكلة","نوع المشكله"]).sort((a,b)=>a-b);
204
  if(niu.length >= 2){
205
  const parts=[];
 
212
  if(parts.length) return parts;
213
  }
214
 
215
+ // (2)
216
  if(TICKET_SEP.test(text)){
217
  return text.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
218
  }
219
 
220
+ // (3)
221
  const startsAll = findStartsByLabels(text, START_LABELS).sort((a,b)=>a-b);
222
  const filtered = startsAll.filter((pos,i,arr)=> i===0 || (pos - arr[i-1]) >= MIN_SPLIT_SPAN);
223
  if(filtered.length >= 2){
 
231
  if(parts.length) return parts;
232
  }
233
 
234
+ // (4)
235
  const blocks = text.split(/\n\s*\n+/).map(p=>p.trim()).filter(Boolean);
236
  if(blocks.length > 1) return blocks;
237
 
238
  return [text];
239
  }
240
 
241
+ /* ------- استخراج ------- */
242
  function extractFields(ticketText){
243
+ const text = normalizeText(fixLabels(ticketText));
244
  const out = {
245
  "نوع المشكلة":"", "وقت حدوث المشكلة":"", "اسم صاحب المشكلة":"",
246
  "رقم الهوية":"", "رقم الجهاز":"", "رقم الجوال":"", "المسح":"", "المنطقة":""
 
250
  if(v) out["نوع المشكلة"] = normalizeText(v);
251
 
252
  v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
253
+ if(v) out["وقت حدوث المشكلة"] = normalizeDateOnly(v); // تاريخ فقط
254
 
255
  v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
256
  if(v) out["اسم صاحب المشكلة"] = v;
 
285
  return out;
286
  }
287
 
288
+ /* ------- تصنيف + شارة ------- */
289
  function classifyTicket(text, fields){
290
  const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
291
  for(const label of CLASS_PRIORITY){
 
297
  }
298
  return "استفسار";
299
  }
300
+ function catClass(label){
301
+ if(/تسجيل دخول/.test(label)) return "login";
302
+ if(/الشبكة/.test(label)) return "network";
303
+ if(/الاستمارة/.test(label)) return "form";
304
+ if(/النسخة|تحديث/.test(label)) return "update";
305
+ if(/أجهزة/.test(label)) return "device";
306
+ return "default";
307
+ }
308
 
309
+ /* ------- بايبلاين ------- */
310
  function parseTicketsWithExtras(raw, agentName, defaultRegion){
311
  const regionChosen = (defaultRegion || "").toString();
312
  return splitTickets(raw||"").map(t => {
 
329
  });
330
  }
331
 
332
+ /* ------- بناء الجدول + الشارات (اقتراح 2) ------- */
333
  function buildTable(rows){
334
  const theadRow = document.getElementById("theadRow");
335
  const tbody = document.getElementById("tbody");
 
344
  const tr=document.createElement("tr");
345
  EXPORT_COLUMNS.forEach(col=>{
346
  const td=document.createElement("td");
347
+ if(col==="التصنيف"){
348
+ const span=document.createElement("span");
349
+ const type = catClass(r[col]||"");
350
+ span.className = `cat ${type}`;
351
+ span.textContent = r[col] || "";
352
+ td.appendChild(span);
353
+ }else{
354
+ td.contentEditable="true";
355
+ td.textContent = r[col]||"";
356
+ }
357
  tr.appendChild(td);
358
  });
359
  tbody.appendChild(tr);
360
  });
361
  }
362
 
363
+ /* ------- قراءة الجدول ------- */
364
  function readTable(){
365
  const tbody = document.getElementById("tbody");
366
  const rows = [];
367
  [...tbody.querySelectorAll("tr")].forEach(tr=>{
368
  const obj={};
369
+ [...tr.children].forEach((td,idx)=>{
370
+ const col = EXPORT_COLUMNS[idx];
371
+ if(col==="التصنيف"){
372
+ obj[col] = td.textContent.trim();
373
+ }else{
374
+ obj[col] = td.textContent.trim();
375
+ }
376
+ });
377
  rows.push(obj);
378
  });
379
  return rows;
380
  }
381
 
382
+ /* ------- أدوات الواجهة ------- */
383
  function updateBadge(n){
384
  const b = document.getElementById("countBadge");
385
  b.textContent = n; b.hidden = (n===0);
 
423
  setTimeout(()=>{ t.hidden = true; }, 2000);
424
  }
425
 
426
+ /* ------- تصدير ------- */
427
  async function exportExcel(){
428
  const rows = readTable();
429
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
 
511
  toast("تم تنزيل الملف بتنسيق القالب.");
512
  }
513
 
514
+ /* ------- نسخ إلى الحافظة ------- */
515
  async function copyToClipboardTSV(){
516
  const rows = readTable();
517
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
 
535
  }
536
  }
537
 
538
+ /* ------- مثال جاهز (صُمّم كتذكرة واحدة بلا انقسام) ------- */
539
+ const SAMPLE = `نوع المشكلة: لا استطيع اكمال الاستمارة بسبب تعليق عند الحفظ
540
+ وقت حدوث المشكلة: 1446/09/10 هـ
541
+ اسم صاحب المشكلة: نوف الناصر
542
  رقم الهوية: 1234567890
543
  رقم الجهاز: 01234
544
  رقم الجوال: 0558174717
545
+ المسح: الخبر 2025
546
+ المنطقة: الشرقية`;
547
 
548
+ /* ------- حالة التخزين ------- */
549
+ const STATE_KEY = "ticketParserState_v10_7";
550
  const ALL_STATE_KEYS = [
551
  "ticketParserState_v8","ticketParserState_v9","ticketParserState_v10",
552
  "ticketParserState_v10_1","ticketParserState_v10_2","ticketParserState_v10_3",
553
+ "ticketParserState_v10_5","ticketParserState_v10_6","ticketParserState_v10_7"
554
  ];
555
 
556
  function ensureColumns(rows, agentName, defaultRegion){
 
573
  const agent = document.getElementById("agentName")?.value || "";
574
  const region= document.getElementById("regionDefault")?.value || "";
575
  const rows = readTable();
576
+ localStorage.setItem(STATE_KEY, JSON.stringify({ raw, agent, region, rows, theme:document.body.classList.contains('dark')?'dark':'light' }));
577
  }catch{}
578
  }
579
  function loadState(){
580
  try{
581
  const s = localStorage.getItem(STATE_KEY);
582
  if(!s) return false;
583
+ let { raw, agent, region, rows, theme } = JSON.parse(s);
584
  if(typeof raw === "string"){ const el=document.getElementById("raw"); if(el) el.value = raw; }
585
  if(typeof agent === "string"){ const el=document.getElementById("agentName"); if(el) el.value = agent; }
586
  if(typeof region === "string"){ const el=document.getElementById("regionDefault"); if(el) el.value = region; }
587
+ if(theme === 'dark') document.body.classList.add('dark');
588
  rows = ensureColumns(rows, agent, region);
589
  if(Array.isArray(rows) && rows.length){
590
  buildTable(rows); validateCells(); updateBadge(rows.length); setButtonsEnabled(true);
 
593
  }catch{ return false; }
594
  }
595
 
596
+ /* ------- تنظيف (اقتراح 1) ------- */
597
+ function cleanRaw(){
598
+ const el = document.getElementById("raw");
599
+ const before = el.value || "";
600
+ const cleaned = normalizeText(fixLabels(before));
601
+ el.value = cleaned;
602
+ saveState();
603
+ toast("تم تنظيف النص وتصحيح المسميات.");
 
 
 
 
604
  }
605
 
606
+ /* ------- دمج مكررات (اقتراح 8) ------- */
607
+ function mergeDuplicates(){
608
+ const rows = readTable();
609
+ if(!rows.length){ toast("لا يوجد بيانات لدمجها."); return; }
610
+ const map = new Map();
611
+ rows.forEach(r=>{
612
+ const key = [
613
+ r["رقم الهوية"]||"", r["رقم الجهاز"]||"",
614
+ r["رقم الجوال"]||"", r["وقت حدوث المشكلة"]||"",
615
+ (r["نوع المشكلة"]||"").slice(0,40)
616
+ ].join("|");
617
+ if(!map.has(key)) map.set(key, r);
618
+ });
619
+ const merged = [...map.values()];
620
+ buildTable(merged); validateCells(); updateBadge(merged.length); setButtonsEnabled(merged.length>0);
621
+ saveState();
622
+ toast(`تم الدمج — عدد الصفوف الآن: ${merged.length}`);
623
+ }
624
+
625
+ /* ------- تهيئة ------- */
626
  function init(){
627
  const parseBtn = document.getElementById("btn-parse");
628
  const exportBtn = document.getElementById("btn-export");
629
  const copyBtn = document.getElementById("btn-copy");
630
  const clearBtn = document.getElementById("btn-clear");
631
  const sampleBtn = document.getElementById("btn-sample");
632
+ const mergeBtn = document.getElementById("btn-merge");
633
+ const cleanBtn = document.getElementById("btn-clean");
634
+ const themeBtn = document.getElementById("btn-theme");
635
  const rawEl = document.getElementById("raw");
636
  const agentEl = document.getElementById("agentName");
637
  const regionEl = document.getElementById("regionDefault");
638
 
639
  loadState();
640
 
641
+ cleanBtn.addEventListener("click", cleanRaw);
642
+
643
  parseBtn.addEventListener("click", ()=>{
644
  const raw = (rawEl.value || "").trim();
645
  if(!raw){ toast("فضلاً الصق/ي تذاكر أولاً."); return; }
 
654
 
655
  exportBtn.addEventListener("click", exportExcel);
656
  copyBtn.addEventListener("click", copyToClipboardTSV);
657
+ clearBtn.addEventListener("click", ()=>{
658
+ const rawEl = document.getElementById("raw");
659
+ const tbody = document.getElementById("tbody");
660
+ const agentEl = document.getElementById("agentName");
661
+ const regionEl= document.getElementById("regionDefault");
662
+ if(rawEl) rawEl.value = "";
663
+ if(tbody) tbody.innerHTML = "";
664
+ if(agentEl) agentEl.value = "";
665
+ if(regionEl) regionEl.value = "";
666
+ updateBadge(0); setButtonsEnabled(false);
667
+ try{ ALL_STATE_KEYS.forEach(k=>localStorage.removeItem(k)); }catch{}
668
+ toast("تم مسح كل البيانات والتخزين.");
669
+ });
670
+ mergeBtn.addEventListener("click", mergeDuplicates);
671
+
672
  sampleBtn.addEventListener("click", ()=>{
673
+ const raw = document.getElementById("raw");
674
+ raw.value = SAMPLE; // مصمم لتذكرة واحدة بلا فواصل زائدة
675
  saveState();
676
  toast("تم إدراج المثال.");
677
  });
678
 
679
+ themeBtn.addEventListener("click", ()=>{
680
+ document.body.classList.toggle("dark");
681
+ saveState();
682
+ });
683
+
684
  rawEl.addEventListener("input", saveState);
685
  agentEl.addEventListener("input", saveState);
686
  regionEl.addEventListener("change", saveState);
 
690
  if(ctrl && e.key === "Enter"){ e.preventDefault(); parseBtn.click(); }
691
  else if(ctrl && e.key.toLowerCase() === "e"){ e.preventDefault(); exportBtn.click(); }
692
  else if(ctrl && e.shiftKey && e.key.toLowerCase() === "c"){ e.preventDefault(); copyBtn.click(); }
693
+ else if(e.key === "Escape"){ e.preventDefault(); document.getElementById("btn-clear").click(); }
694
  });
695
 
696
  setButtonsEnabled(!!document.getElementById("tbody")?.children.length);