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

Update app.js

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