stat2025 commited on
Commit
625802c
·
verified ·
1 Parent(s): 375805c

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +209 -110
app.js CHANGED
@@ -1,5 +1,6 @@
1
- /* app.js — v8: Clear ALSO removes agent & region */
2
- /* ========= أعمدة الجدول ========= */
 
3
  const EXPORT_COLUMNS = [
4
  "التصنيف",
5
  "نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
@@ -7,7 +8,7 @@ const EXPORT_COLUMNS = [
7
  "اسم الدعم الفني","الحالة"
8
  ];
9
 
10
- /* ========= مرادفات الحقول ========= */
11
  const FIELD_ALIASES = {
12
  "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
13
  "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
@@ -19,7 +20,7 @@ const FIELD_ALIASES = {
19
  "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
20
  };
21
 
22
- /* ========= قواعد التصنيف ========= */
23
  const CLASS_RULES = {
24
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
25
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
@@ -41,7 +42,7 @@ const CLASS_PRIORITY = [
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){
@@ -59,15 +60,29 @@ function alnumAr(s){
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
  }
72
  function normalizeDate(v){
73
  v=(v||"").trim();
@@ -81,31 +96,19 @@ function normalizeDate(v){
81
  let y=+m[1], mo=+m[2], d=+m[3];
82
  return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
83
  }
84
- const months = {
85
- "يناير":1,"فبراير":2,"مارس":3,"أبريل":4,"ابريل":4,"مايو":5,"يونيو":6,"يوليو":7,"أغسطس":8,"اغسطس":8,"سبتمبر":9,"أكتوبر":10,"اكتوبر":10,"نوفمبر":11,"ديسمبر":12,
86
- "كانون الثاني":1,"شباط":2,"آذار":3,"نيسان":4,"أيار":5,"حزيران":6,"تموز":7,"آب":8,"أيلول":9,"تشرين الأول":10,"تشرين الثاني":11,"كانون الأول":12
87
- };
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){
@@ -118,16 +121,43 @@ function findAfterLabel(text, labels){
118
  return "";
119
  }
120
 
121
- /* تقسيم التذاكر */
 
 
122
  function splitTickets(raw){
123
- raw = normalizeText(raw);
124
- if(!raw) return [];
125
- let parts = raw.split(TICKET_SEP);
126
- if(parts.length===1){ parts = raw.split(/\n\s*\n+/).filter(p=>p.trim()); }
127
- return parts.map(p=>p.trim()).filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  }
129
 
130
- /* استخراج بالأنواع المطلوبة */
131
  function extractFields(ticketText){
132
  const text = normalizeText(ticketText);
133
  const out = {
@@ -135,19 +165,15 @@ function extractFields(ticketText){
135
  "رقم الهوية":"", "رقم الجهاز":"", "رقم الجوال":"", "المسح":"", "المنطقة":""
136
  };
137
 
138
- // نوع المشكلة: حروف/أرقام/معًا
139
  let v = findAfterLabel(text, FIELD_ALIASES["نوع المشكلة"]);
140
  if(v) out["نوع المشكلة"] = alnumAr(v);
141
 
142
- // وقت حدوث المشكلة: تاريخ أو وقت أو كلاهما
143
  v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
144
  if(v) out["وقت حدوث المشكلة"] = parseDateTime(v);
145
 
146
- // اسم صاحب المشكلة: كما هو
147
  v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
148
  if(v) out["اسم صاحب المشكلة"] = v;
149
 
150
- // رقم الهوية: أرقام فقط (التحقق 10 أرقام لاحقًا)
151
  v = findAfterLabel(text, FIELD_ALIASES["رقم الهوية"]);
152
  if(v) out["رقم الهوية"] = digitsOnly(v);
153
  if(!out["رقم الهوية"]){
@@ -155,7 +181,6 @@ function extractFields(ticketText){
155
  if(m) out["رقم الهوية"] = m[1];
156
  }
157
 
158
- // رقم الجهاز: أرقام فقط
159
  v = findAfterLabel(text, FIELD_ALIASES["رقم الجهاز"]);
160
  if(v) out["رقم الجهاز"] = digitsOnly(v);
161
  if(!out["رقم الجهاز"]){
@@ -163,7 +188,6 @@ function extractFields(ticketText){
163
  if(m) out["رقم الجهاز"] = m[1];
164
  }
165
 
166
- // رقم الجوال: أرقام فقط
167
  v = findAfterLabel(text, FIELD_ALIASES["رقم الجوال"]);
168
  if(v) out["رقم الجوال"] = digitsOnly(v);
169
  if(!out["رقم الجوال"]){
@@ -171,11 +195,9 @@ function extractFields(ticketText){
171
  if(m) out["رقم الجوال"] = m[1];
172
  }
173
 
174
- // اسم المسح: حروف/أرقام/معًا
175
  v = findAfterLabel(text, FIELD_ALIASES["المسح"]);
176
  if(v) out["المسح"] = alnumAr(v);
177
 
178
- // اسم المنطقة: إن لم تُختر من القائمة لاحقًا نأخذها حروف فقط
179
  v = findAfterLabel(text, FIELD_ALIASES["المنطقة"]);
180
  if(v) out["المنطقة"] = lettersOnly(v);
181
 
@@ -202,6 +224,7 @@ function parseTicketsWithExtras(raw, agentName, defaultRegion){
202
  const f = extractFields(t);
203
  const cls = classifyTicket(t, f);
204
  const region = regionChosen ? regionChosen : (f["المنطقة"] || "");
 
205
  return {
206
  "التصنيف": cls,
207
  "نوع المشكلة": f["نوع المشكلة"] || "",
@@ -218,7 +241,7 @@ function parseTicketsWithExtras(raw, agentName, defaultRegion){
218
  });
219
  }
220
 
221
- /* ========= بناء الجدول/قراءة الجدول ========= */
222
  function buildTable(rows){
223
  const theadRow = document.getElementById("theadRow");
224
  const tbody = document.getElementById("tbody");
@@ -251,7 +274,7 @@ function readTable(){
251
  return rows;
252
  }
253
 
254
- /* ========= شارة العدّاد + تمكين الأزرار + التحقق ========= */
255
  function updateBadge(n){
256
  const b = document.getElementById("countBadge");
257
  b.textContent = n; b.hidden = (n===0);
@@ -265,20 +288,18 @@ function validateCells(){
265
  const idxPhone = EXPORT_COLUMNS.indexOf("رقم الجوال");
266
  const idxID = EXPORT_COLUMNS.indexOf("رقم الهوية");
267
  [...tbody.rows].forEach(tr=>{
268
- // الجوال: خطأ فقط إذا أقل من 9 أرقام (إن كانت الخانة غير فارغة)
269
  if(idxPhone>=0){
270
  const td=tr.children[idxPhone];
271
  const raw=(td.textContent||"").trim();
272
  const digits = raw.replace(/\D/g,"");
273
- const invalid = !!raw && digits.length < 9;
274
  td.classList.toggle("invalid", invalid);
275
  }
276
- // الهوية: خطأ فقط إذا ليست 10 أرقام بالضبط (إن كانت الخانة غير فارغة)
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
  });
@@ -290,7 +311,7 @@ document.addEventListener("input",(e)=>{
290
  }
291
  });
292
 
293
- /* ========= Toast ========= */
294
  function toast(msg){
295
  const t = document.getElementById("toast");
296
  t.textContent = msg; t.hidden = false;
@@ -298,41 +319,82 @@ function toast(msg){
298
  setTimeout(()=>{ t.hidden = true; }, 2000);
299
  }
300
 
301
- /* ========= تصدير Excel (حفظ الأصفار كنص للأعمدة الرقمية) ========= */
302
  async function exportExcel(){
303
  const rows = readTable();
304
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
305
 
306
- const aoa = [EXPORT_COLUMNS, ...rows.map(r=>EXPORT_COLUMNS.map(c=>r[c]||""))];
307
- const ws = XLSX.utils.aoa_to_sheet(aoa);
308
- ws["!rtl"] = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
- const textCols = new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
311
- const nRows = rows.length + 1;
312
- EXPORT_COLUMNS.forEach((colName, colIdx)=>{
313
- if(!textCols.has(colName)) return;
314
- for(let r = 1; r < nRows; r++){
315
- const addr = XLSX.utils.encode_cell({ c: colIdx, r });
316
- const cell = ws[addr];
317
- if(cell){
318
- cell.t = "s"; cell.z = "@";
319
- if(typeof cell.v !== "string") cell.v = String(cell.v ?? "");
320
- if(typeof cell.w !== "string") cell.w = cell.v;
321
- }else{
322
- ws[addr] = { t:"s", v:"", z:"@" };
323
- }
324
- }
 
 
 
 
325
  });
326
 
327
- const wb = XLSX.utils.book_new();
328
- XLSX.utils.book_append_sheet(wb, ws, "التذاكر");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
- const ts = new Date().toISOString().replace(/\D/g,'').slice(0,14);
331
  const base = (document.getElementById("fname").value || "Ticket").trim() || "Ticket";
332
  const filename = `${base}_${ts}.xlsx`;
333
 
334
- const wbArray = XLSX.write(wb, { bookType: "xlsx", type: "array" });
335
- const blob = new Blob([wbArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
336
  const file = new File([blob], filename, { type: blob.type });
337
 
338
  if (navigator.canShare && navigator.canShare({ files: [file] })) {
@@ -343,14 +405,13 @@ async function exportExcel(){
343
  const a = document.createElement("a"); a.href = url; a.download = filename;
344
  document.body.appendChild(a); a.click(); a.remove();
345
  setTimeout(()=>URL.revokeObjectURL(url), 1000);
346
- toast("تم تنزيل الملف.");
347
  }
348
 
349
- /* ========= نسخ إلى الحافظة (TSV) ========= */
350
  async function copyToClipboardTSV(){
351
  const rows = readTable();
352
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
353
-
354
  const textCols = new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
355
  const header = EXPORT_COLUMNS.join("\t");
356
  const body = rows.map(r =>
@@ -362,10 +423,8 @@ async function copyToClipboardTSV(){
362
  ).join("\r\n");
363
  const tsv = "\uFEFF" + header + "\r\n" + body;
364
 
365
- try{
366
- await navigator.clipboard.writeText(tsv);
367
- toast("تم النسخ — الصق/ي مباشرة في Excel.");
368
- }catch(e){
369
  const ta = document.createElement("textarea");
370
  ta.value = tsv; document.body.appendChild(ta);
371
  ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
@@ -373,19 +432,36 @@ async function copyToClipboardTSV(){
373
  }
374
  }
375
 
376
- /* ========= مثال ========= */
377
  const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
378
- وقت حدوث المشكلة: 21/8/2025 10:35 ص
379
  اسم صاحب المشكلة : نوف الناصر
380
  رقم الهوية: 1234567890
381
  رقم الجهاز: 01234
382
  رقم الجوال: 0558174717
383
  اسم المسح: الخبر 2025
384
- اسم المنطقة: الشرقية`;
385
-
386
- /* ========= تخزين الحالة ========= */
387
- const STATE_KEY = "ticketParserState_v8";
388
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  function ensureColumns(rows, agentName, defaultRegion){
390
  if(!Array.isArray(rows)) return rows||[];
391
  return rows.map(r=>{
@@ -396,11 +472,10 @@ function ensureColumns(rows, agentName, defaultRegion){
396
  }
397
  if(!("اسم الدعم الفني" in out)) out["اسم الدعم الفني"] = agentName || out["اسم الدعم الفني"] || "";
398
  if(!("الحالة" in out) || !out["الحالة"]) out["الحالة"] = "تم الحل";
399
- if(defaultRegion) out["المنطقة"] = defaultRegion; // طبّق المختارة على الجميع
400
  return out;
401
  });
402
  }
403
-
404
  function saveState(){
405
  try{
406
  const raw = document.getElementById("raw")?.value || "";
@@ -411,7 +486,6 @@ function saveState(){
411
  localStorage.setItem(STATE_KEY, JSON.stringify({ raw, fname, agent, region, rows }));
412
  }catch{}
413
  }
414
-
415
  function loadState(){
416
  try{
417
  const s = localStorage.getItem(STATE_KEY);
@@ -429,44 +503,69 @@ function loadState(){
429
  }catch{ return false; }
430
  }
431
 
432
- /* ========= مسح كل شيء بما فيه اسم الدعم والمنطقة ========= */
433
  function clearAll(){
434
- // نظّف الواجهة كاملة
435
  const rawEl = document.getElementById("raw");
436
  const tbody = document.getElementById("tbody");
437
  const fnameEl = document.getElementById("fname");
438
  const agentEl = document.getElementById("agentName");
439
  const regionEl= document.getElementById("regionDefault");
440
-
441
  if(rawEl) rawEl.value = "";
442
  if(tbody) tbody.innerHTML = "";
443
  if(fnameEl) fnameEl.value = "Ticket";
444
  if(agentEl) agentEl.value = "";
445
  if(regionEl) regionEl.value = "";
446
-
447
- // عطّل الأزرار واصفر العداد
448
  updateBadge(0); setButtonsEnabled(false);
449
-
450
- // امسح التخزين كليًا
451
  try{ localStorage.removeItem(STATE_KEY); }catch{}
 
 
452
 
453
- toast("تم مسح كل البيانات بما فيها اسم الدعم والمنطقة.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  }
455
 
456
- /* ========= تهيئة ========= */
457
  function init(){
458
- const parseBtn = document.getElementById("btn-parse");
459
- const exportBtn = document.getElementById("btn-export");
460
- const copyBtn = document.getElementById("btn-copy");
461
- const clearBtn = document.getElementById("btn-clear");
462
- const sampleBtn = document.getElementById("btn-sample");
463
- const rawEl = document.getElementById("raw");
464
- const fnameEl = document.getElementById("fname");
465
- const agentEl = document.getElementById("agentName");
466
- const regionEl = document.getElementById("regionDefault");
 
467
 
468
  loadState();
469
 
 
 
470
  parseBtn.addEventListener("click", ()=>{
471
  const raw = (rawEl.value || SAMPLE);
472
  const agent = agentEl.value || "";
 
1
+ /* app.js — v9: smarter split, time parser, Smart Paste, keep all prior features */
2
+
3
+ /* ======== أعمدة الجدول (نفسها) ======== */
4
  const EXPORT_COLUMNS = [
5
  "التصنيف",
6
  "نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
 
8
  "اسم الدعم الفني","الحالة"
9
  ];
10
 
11
+ /* ======== مرادفات الحقول ======== */
12
  const FIELD_ALIASES = {
13
  "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
14
  "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
 
20
  "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
21
  };
22
 
23
+ /* ======== قواعد التصنيف ======== */
24
  const CLASS_RULES = {
25
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
26
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
 
42
  "النظام المكتبي","تناقل البيانات","استفسار",
43
  ];
44
 
45
+ /* ======== أدوات نص وتطبيع ======== */
46
  const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
47
  const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
48
  function normalizeText(s){
 
60
  }
61
  function digitsOnly(s){ return (s||"").replace(/\D+/g,""); }
62
 
63
+ /* وقت بدون دقايق مثل "7 صباحا" / "٧ مساء" */
64
+ function normalizeTimeLoose(val){
65
+ const t = normalizeText(val);
66
+ // HH:MM (مع أو بدون am/pm / ص/م)
67
+ let m = t.match(/(\d{1,2})[:٫\.\-:](\d{2})\s*(ص|صباح|صباحا|am|م|مساء|pm)?/i);
68
+ if(m){
69
+ let h = parseInt(m[1],10), mn = m[2], ampm = (m[3]||"").toLowerCase();
70
+ if(ampm){
71
+ if((/م|مساء|pm/).test(ampm) && h<12) h+=12;
72
+ if((/ص|صباح|صباحا|am/).test(ampm) && h===12) h=0;
73
+ }
74
+ return `${String(h).padStart(2,"0")}:${mn}`;
75
  }
76
+ // HH فقط مع صباح/مساء
77
+ m = t.match(/(\d{1,2})\s*(ص|صباح|صباحا|am|م|مساء|pm)/i);
78
+ if(m){
79
+ let h = parseInt(m[1],10);
80
+ const ampm = (m[2]||"").toLowerCase();
81
+ if((/م|مساء|pm/).test(ampm) && h<12) h+=12;
82
+ if((/ص|صباح|صباحا|am/).test(ampm) && h===12) h=0;
83
+ return `${String(h).padStart(2,"0")}:00`;
84
+ }
85
+ return "";
86
  }
87
  function normalizeDate(v){
88
  v=(v||"").trim();
 
96
  let y=+m[1], mo=+m[2], d=+m[3];
97
  return `${y.toString().padStart(4,"0")}-${String(mo).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
98
  }
 
 
 
 
 
 
 
 
 
 
 
99
  return v;
100
  }
101
  function parseDateTime(raw){
102
  const t = normalizeText(raw);
103
  const d = normalizeDate(t);
104
+ const hhmmLoose = normalizeTimeLoose(t);
105
+ if(d && /^\d{4}-\d{2}-\d{2}$/.test(d) && hhmmLoose) return `${d} ${hhmmLoose}`;
 
106
  if(d && /^\d{4}-\d{2}-\d{2}$/.test(d)) return d;
107
+ if(hhmmLoose) return hhmmLoose;
108
  return t;
109
  }
110
 
111
+ /* ابحث قيمة بعد مسافة أو بعد نقطتين */
112
  function findAfterLabel(text, labels){
113
  const hay = "\n" + normalizeText(text) + "\n";
114
  for(const rawLbl of labels){
 
121
  return "";
122
  }
123
 
124
+ /* تقسيم تذاكر ذكي:
125
+ - إن وجد فواصل معروفة → نستخدمها
126
+ - وإلا نكتشف كل سطر يبدأ بـ "نوع المشكلة" كحد فاصل */
127
  function splitTickets(raw){
128
+ raw = (raw || "").replace(/\r\n/g,"\n");
129
+ const normalized = normalizeText(raw);
130
+ if(!normalized) return [];
131
+
132
+ // 1) فواصل معروفة
133
+ if(TICKET_SEP.test(normalized)){
134
+ return normalized.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
135
+ }
136
+
137
+ // 2) اكتشاف بدايات "نوع المشكلة"
138
+ const starts = [];
139
+ const re = /(^|\n)\s*(?:نوع المشكلة|نوع المشكله)\s*[::]/g;
140
+ let m;
141
+ while((m = re.exec(normalized))){
142
+ const pos = m.index + (m[1] ? m[1].length : 0);
143
+ starts.push(pos);
144
+ }
145
+ if(starts.length >= 2){
146
+ const parts = [];
147
+ for(let i=0;i<starts.length;i++){
148
+ const s = starts[i];
149
+ const e = i+1<starts.length ? starts[i+1] : normalized.length;
150
+ const slice = normalized.slice(s,e).trim();
151
+ if(slice) parts.push(slice);
152
+ }
153
+ return parts;
154
+ }
155
+
156
+ // 3) fallback: أسطر فارغة
157
+ return normalized.split(/\n\s*\n+/).map(p=>p.trim()).filter(Boolean);
158
  }
159
 
160
+ /* استخراج الحقول بالأنواع المطلوبة */
161
  function extractFields(ticketText){
162
  const text = normalizeText(ticketText);
163
  const out = {
 
165
  "رقم الهوية":"", "رقم الجهاز":"", "رقم الجوال":"", "المسح":"", "المنطقة":""
166
  };
167
 
 
168
  let v = findAfterLabel(text, FIELD_ALIASES["نوع المشكلة"]);
169
  if(v) out["نوع المشكلة"] = alnumAr(v);
170
 
 
171
  v = findAfterLabel(text, FIELD_ALIASES["وقت حدوث المشكلة"]);
172
  if(v) out["وقت حدوث المشكلة"] = parseDateTime(v);
173
 
 
174
  v = findAfterLabel(text, FIELD_ALIASES["اسم صاحب المشكلة"]);
175
  if(v) out["اسم صاحب المشكلة"] = v;
176
 
 
177
  v = findAfterLabel(text, FIELD_ALIASES["رقم الهوية"]);
178
  if(v) out["رقم الهوية"] = digitsOnly(v);
179
  if(!out["رقم الهوية"]){
 
181
  if(m) out["رقم الهوية"] = m[1];
182
  }
183
 
 
184
  v = findAfterLabel(text, FIELD_ALIASES["رقم الجهاز"]);
185
  if(v) out["رقم الجهاز"] = digitsOnly(v);
186
  if(!out["رقم الجهاز"]){
 
188
  if(m) out["رقم الجهاز"] = m[1];
189
  }
190
 
 
191
  v = findAfterLabel(text, FIELD_ALIASES["رقم الجوال"]);
192
  if(v) out["رقم الجوال"] = digitsOnly(v);
193
  if(!out["رقم الجوال"]){
 
195
  if(m) out["رقم الجوال"] = m[1];
196
  }
197
 
 
198
  v = findAfterLabel(text, FIELD_ALIASES["المسح"]);
199
  if(v) out["المسح"] = alnumAr(v);
200
 
 
201
  v = findAfterLabel(text, FIELD_ALIASES["المنطقة"]);
202
  if(v) out["المنطقة"] = lettersOnly(v);
203
 
 
224
  const f = extractFields(t);
225
  const cls = classifyTicket(t, f);
226
  const region = regionChosen ? regionChosen : (f["المنطقة"] || "");
227
+
228
  return {
229
  "التصنيف": cls,
230
  "نوع المشكلة": f["نوع المشكلة"] || "",
 
241
  });
242
  }
243
 
244
+ /* ======== جدول ======== */
245
  function buildTable(rows){
246
  const theadRow = document.getElementById("theadRow");
247
  const tbody = document.getElementById("tbody");
 
274
  return rows;
275
  }
276
 
277
+ /* ======== شارة + أزرار + تحقق ======== */
278
  function updateBadge(n){
279
  const b = document.getElementById("countBadge");
280
  b.textContent = n; b.hidden = (n===0);
 
288
  const idxPhone = EXPORT_COLUMNS.indexOf("رقم الجوال");
289
  const idxID = EXPORT_COLUMNS.indexOf("رقم الهوية");
290
  [...tbody.rows].forEach(tr=>{
 
291
  if(idxPhone>=0){
292
  const td=tr.children[idxPhone];
293
  const raw=(td.textContent||"").trim();
294
  const digits = raw.replace(/\D/g,"");
295
+ const invalid = !!raw && digits.length < 9; // أقل من 9 = خطأ
296
  td.classList.toggle("invalid", invalid);
297
  }
 
298
  if(idxID>=0){
299
  const td=tr.children[idxID];
300
  const raw=(td.textContent||"").trim();
301
  const digits = raw.replace(/\D/g,"");
302
+ const invalid = !!raw && digits.length !== 10; // غير 10 = خطأ
303
  td.classList.toggle("invalid", invalid);
304
  }
305
  });
 
311
  }
312
  });
313
 
314
+ /* ======== Toast ======== */
315
  function toast(msg){
316
  const t = document.getElementById("toast");
317
  t.textContent = msg; t.hidden = false;
 
319
  setTimeout(()=>{ t.hidden = true; }, 2000);
320
  }
321
 
322
+ /* ======== تصدير Excel (ExcelJS بالتنسيق) ======== */
323
  async function exportExcel(){
324
  const rows = readTable();
325
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
326
 
327
+ const TEMPLATE_HEADERS = [
328
+ "نوع المشكلة","وصف المشكلة","المنطقة","اسم المسح","اسم المشغل",
329
+ "رقم الجوال","رقم الهوية ID","رقم الجهاز","تاريخ اليوم بالميلادي","الحالة","اسم الدعم الفني",
330
+ ];
331
+ const mapRow = (r)=>{
332
+ const today = new Date();
333
+ const yyyy=today.getFullYear(), mm=String(today.getMonth()+1).padStart(2,"0"), dd=String(today.getDate()).padStart(2,"0");
334
+ const todayStr = `${yyyy}-${mm}-${dd}`;
335
+ return {
336
+ "نوع المشكلة": r["نوع المشكلة"]||"",
337
+ "وصف المشكلة": r["التصنيف"] || r["نوع المشكلة"] || "",
338
+ "المنطقة": r["المنطقة"]||"",
339
+ "اسم المسح": r["المسح"]||"",
340
+ "اسم المشغل": r["اسم صاحب المشكلة"]||"",
341
+ "رقم الجوال": (r["رقم الجوال"]||"").toString(),
342
+ "رقم الهوية ID":(r["رقم الهوية"]||"").toString(),
343
+ "رقم الجهاز": (r["رقم الجهاز"]||"").toString(),
344
+ "تاريخ اليوم بالميلادي": todayStr,
345
+ "الحالة": r["الحالة"]||"تم الحل",
346
+ "اسم الدعم الفني": r["اسم الدعم الفني"]||"",
347
+ };
348
+ };
349
 
350
+ const wb = new ExcelJS.Workbook();
351
+ const ws = wb.addWorksheet("التذاكر", { views: [{ rightToLeft: true }] });
352
+
353
+ const colWidths = [18,26,16,18,20,18,18,18,22,14,18];
354
+ TEMPLATE_HEADERS.forEach((h,i)=> ws.getColumn(i+1).width = colWidths[i]||18);
355
+
356
+ ws.addRow(TEMPLATE_HEADERS);
357
+ const headerRow = ws.getRow(1);
358
+ headerRow.height = 24;
359
+ headerRow.eachCell((cell)=>{
360
+ cell.font = { bold:true, color:{argb:"FFFFFFFF"} };
361
+ cell.alignment = { horizontal:"center", vertical:"middle" };
362
+ cell.fill = { type:"pattern", pattern:"solid", fgColor:{argb:"FF4137A8"} };
363
+ cell.border = {
364
+ top:{style:"thin",color:{argb:"FFCDD2E1"}},
365
+ bottom:{style:"thin",color:{argb:"FFCDD2E1"}},
366
+ left:{style:"thin",color:{argb:"FFE5E7EB"}},
367
+ right:{style:"thin",color:{argb:"FFE5E7EB"}},
368
+ };
369
  });
370
 
371
+ const toTextCols = new Set(["رقم الجوال","رقم الهوية ID","رقم الجهاز"]);
372
+ const rawRows = readTable();
373
+ rawRows.forEach((r,idx)=>{
374
+ const m = mapRow(r);
375
+ const vals = TEMPLATE_HEADERS.map(h => (m[h] ?? ""));
376
+ const row = ws.addRow(vals);
377
+ row.alignment = { horizontal:"center", vertical:"middle" };
378
+ const even = (idx % 2) === 1;
379
+ row.eachCell((cell, colNumber)=>{
380
+ cell.border = {
381
+ top:{style:"thin",color:{argb:"FFE5E7EB"}},
382
+ bottom:{style:"thin",color:{argb:"FFE5E7EB"}},
383
+ left:{style:"thin",color:{argb:"FFE5E7EB"}},
384
+ right:{style:"thin",color:{argb:"FFE5E7EB"}},
385
+ };
386
+ if(even) cell.fill = { type:"pattern", pattern:"solid", fgColor:{argb:"FFF5F8FF"} };
387
+ const header = TEMPLATE_HEADERS[colNumber-1];
388
+ if(toTextCols.has(header)) cell.value = String(cell.value ?? "");
389
+ });
390
+ });
391
 
392
+ const ts = new Date().toISOString().replace(/\D/g,"").slice(0,14);
393
  const base = (document.getElementById("fname").value || "Ticket").trim() || "Ticket";
394
  const filename = `${base}_${ts}.xlsx`;
395
 
396
+ const buffer = await wb.xlsx.writeBuffer();
397
+ const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
398
  const file = new File([blob], filename, { type: blob.type });
399
 
400
  if (navigator.canShare && navigator.canShare({ files: [file] })) {
 
405
  const a = document.createElement("a"); a.href = url; a.download = filename;
406
  document.body.appendChild(a); a.click(); a.remove();
407
  setTimeout(()=>URL.revokeObjectURL(url), 1000);
408
+ toast("تم تنزيل الملف بتنسيق القالب.");
409
  }
410
 
411
+ /* ======== نسخ إلى الحافظة (TSV) ======== */
412
  async function copyToClipboardTSV(){
413
  const rows = readTable();
414
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
 
415
  const textCols = new Set(["رقم الهوية","رقم الجهاز","رقم الجوال"]);
416
  const header = EXPORT_COLUMNS.join("\t");
417
  const body = rows.map(r =>
 
423
  ).join("\r\n");
424
  const tsv = "\uFEFF" + header + "\r\n" + body;
425
 
426
+ try{ await navigator.clipboard.writeText(tsv); toast("تم النسخ — الصق/ي مباشرة في Excel."); }
427
+ catch(e){
 
 
428
  const ta = document.createElement("textarea");
429
  ta.value = tsv; document.body.appendChild(ta);
430
  ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
 
432
  }
433
  }
434
 
435
+ /* ======== مثال ======== */
436
  const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
437
+ وقت حدوث المشكلة: 21/8/2025 7 صباحا
438
  اسم صاحب المشكلة : نوف الناصر
439
  رقم الهوية: 1234567890
440
  رقم الجهاز: 01234
441
  رقم الجوال: 0558174717
442
  اسم المسح: الخبر 2025
443
+ اسم المنطقة: الشرقية
444
+
445
+ نوع المشكلة: مشكله بالدخول (حدث خطأ في التطبيق)
446
+ وقت حدوث المشكله:5:00 م
447
+ اسم صاحب المشكله: شيماء عبدالرحمن
448
+ رقم الهوية:1075808053
449
+ رقم الجهاز: 01426
450
+ رقم الجوال:0562974417
451
+ المنطقه: الخبر
452
+ المسح: تحديث الاسعار
453
+
454
+ نوع المشكلة: معلق على صفحه البدايه
455
+ وقت حدوث المشكله: 10:00
456
+ اسم صاحب المشكله: امال صالح العبدالعزيز
457
+ رقم الهوية: 1084881448
458
+ رقم الجهاز: 05337
459
+ رقم الجوال: 0582347326
460
+ المنطقه: الخبر
461
+ المسح:اسعار الجمله`;
462
+
463
+ /* ======== تخزين الحالة ======== */
464
+ const STATE_KEY = "ticketParserState_v9";
465
  function ensureColumns(rows, agentName, defaultRegion){
466
  if(!Array.isArray(rows)) return rows||[];
467
  return rows.map(r=>{
 
472
  }
473
  if(!("اسم الدعم الفني" in out)) out["اسم الدعم الفني"] = agentName || out["اسم الدعم الفني"] || "";
474
  if(!("الحالة" in out) || !out["الحالة"]) out["الحالة"] = "تم الحل";
475
+ if(defaultRegion) out["المنطقة"] = defaultRegion;
476
  return out;
477
  });
478
  }
 
479
  function saveState(){
480
  try{
481
  const raw = document.getElementById("raw")?.value || "";
 
486
  localStorage.setItem(STATE_KEY, JSON.stringify({ raw, fname, agent, region, rows }));
487
  }catch{}
488
  }
 
489
  function loadState(){
490
  try{
491
  const s = localStorage.getItem(STATE_KEY);
 
503
  }catch{ return false; }
504
  }
505
 
506
+ /* مسح الكل (مسح التخزين كذلك) */
507
  function clearAll(){
 
508
  const rawEl = document.getElementById("raw");
509
  const tbody = document.getElementById("tbody");
510
  const fnameEl = document.getElementById("fname");
511
  const agentEl = document.getElementById("agentName");
512
  const regionEl= document.getElementById("regionDefault");
 
513
  if(rawEl) rawEl.value = "";
514
  if(tbody) tbody.innerHTML = "";
515
  if(fnameEl) fnameEl.value = "Ticket";
516
  if(agentEl) agentEl.value = "";
517
  if(regionEl) regionEl.value = "";
 
 
518
  updateBadge(0); setButtonsEnabled(false);
 
 
519
  try{ localStorage.removeItem(STATE_KEY); }catch{}
520
+ toast("تم مسح كل البيانات.");
521
+ }
522
 
523
+ /* لصق مُنظَّم: يضيف فواصل بين كل تذكرة تلقائيًا */
524
+ function normalizeForPaste(text){
525
+ const src = (text||"").replace(/\r\n/g,"\n");
526
+ const norm = normalizeText(src);
527
+ const starts = [];
528
+ const re = /(^|\n)\s*(?:نوع المشكلة|نوع المشكله)\s*[::]/g;
529
+ let m; while((m=re.exec(norm))){ starts.push(m.index + (m[1] ? m[1].length : 0)); }
530
+ if(starts.length>=2){
531
+ const parts=[];
532
+ for(let i=0;i<starts.length;i++){
533
+ const s=starts[i], e=i+1<starts.length?starts[i+1]:norm.length;
534
+ const slice = norm.slice(s,e).trim();
535
+ if(slice) parts.push(slice);
536
+ }
537
+ return parts.join("\n\n🔴🔴🔴\n");
538
+ }
539
+ return norm;
540
+ }
541
+ async function smartPasteInto(el){
542
+ try{
543
+ const txt = await navigator.clipboard.readText();
544
+ const formatted = normalizeForPaste(txt || "");
545
+ if(formatted){ el.value = formatted; saveState(); toast("تم اللصق والتنظيم."); }
546
+ else { toast("الحافظة فارغة."); }
547
+ }catch{
548
+ toast("تعذّر قراءة الحافظة — الصق/ي يدويًا.");
549
+ }
550
  }
551
 
552
+ /* ======== تهيئة ======== */
553
  function init(){
554
+ const parseBtn = document.getElementById("btn-parse");
555
+ const exportBtn = document.getElementById("btn-export");
556
+ const copyBtn = document.getElementById("btn-copy");
557
+ const clearBtn = document.getElementById("btn-clear");
558
+ const sampleBtn = document.getElementById("btn-sample");
559
+ const smartPaste = document.getElementById("btn-smartpaste");
560
+ const rawEl = document.getElementById("raw");
561
+ const fnameEl = document.getElementById("fname");
562
+ const agentEl = document.getElementById("agentName");
563
+ const regionEl = document.getElementById("regionDefault");
564
 
565
  loadState();
566
 
567
+ smartPaste.addEventListener("click", ()=> smartPasteInto(rawEl));
568
+
569
  parseBtn.addEventListener("click", ()=>{
570
  const raw = (rawEl.value || SAMPLE);
571
  const agent = agentEl.value || "";