stat2025 commited on
Commit
acf7905
·
verified ·
1 Parent(s): 7d7eed7

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +62 -74
app.js CHANGED
@@ -1,14 +1,10 @@
1
- /* app.js — v10.2: fix over-splitting & hard clear */
2
 
3
- /* ======== أعمدة الجدول ======== */
4
  const EXPORT_COLUMNS = [
5
- "التصنيف",
6
- "نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
7
- "رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة",
8
- "اسم الدعم الفني","الحالة"
9
  ];
10
 
11
- /* ======== مرادفات الحقول ======== */
12
  const FIELD_ALIASES = {
13
  "نوع المشكلة": ["نوع المشكله","نوع المشكلة","المشكلة"],
14
  "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
@@ -20,7 +16,6 @@ const FIELD_ALIASES = {
20
  "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
21
  };
22
 
23
- /* ======== قواعد التصنيف ======== */
24
  const CLASS_RULES = {
25
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
26
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
@@ -42,9 +37,8 @@ const CLASS_PRIORITY = [
42
  "النظام المكتبي","تناقل البيانات","استفسار",
43
  ];
44
 
45
- /* ======== أدوات نص وتطبيع ======== */
46
  const TICKET_SEP = /\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n/;
47
- const MIN_SPLIT_SPAN = 80; // أصغر مسافة بين بدايتين لنعتبرها تذكرة جديدة
48
 
49
  const arabicDigitsMap = {"٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
50
  function normalizeText(s){
@@ -55,15 +49,10 @@ function normalizeText(s){
55
  .replace(/[ــ]+/g,"")
56
  .trim();
57
  }
58
- function lettersOnly(ar){
59
- return (ar||"").replace(/[^A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]/g,"").replace(/\s{2,}/g," ").trim();
60
- }
61
- function alnumAr(s){
62
- return (s||"").replace(/[^0-9A-Za-z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s\-\._/]/g,"").replace(/\s{2,}/g," ").trim();
63
- }
64
  function digitsOnly(s){ return (s||"").replace(/\D+/g,""); }
65
 
66
- /* وقت مثل "7:10 ص" أو "7 صباحا" أو "7مساء" */
67
  function normalizeTimeLoose(val){
68
  const t = normalizeText(val);
69
  let m = t.match(/(\d{1,2})[:٫\.\-:](\d{2})\s*(ص|صباح|صباحا|am|م|مساء|pm)?/i);
@@ -109,42 +98,23 @@ function parseDateTime(raw){
109
  return t;
110
  }
111
 
112
- /* ======== مساعدة: إيجاد بدايات حسب مجموعة تسميات ======== */
113
  function findStartsByLabels(text, labels){
114
  const lblRe = labels.map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join("|");
115
- const re = new RegExp(
116
- `(^|\\n)\\s*(?:[-–—\\*•]+|\\d+[\\)\\.]\\s*)?\\s*(?:${lblRe})(?:\\s*[::]|\\s+)`,
117
- "gi"
118
- );
119
- const idxs = [];
120
- let m;
121
- while((m = re.exec(text))){
122
- idxs.push(m.index + (m[1] ? m[1].length : 0));
123
- }
124
  return idxs;
125
  }
126
 
127
- /* ======== تقسيم التذاكر (إصلاح الجذر) ========
128
- السياسة:
129
- 1) لو فيه فواصل واضحة => نستخدمها.
130
- 2) لو تكررت "نوع المشكلة" مرتين فأكثر => نقسم عليها فقط (أساس التقسيم).
131
- 3) وإلا لا نقسم على الحقول الأخرى حتى لا تتفتت التذكرة الواحدة.
132
- 4) أخيرًا: أسطر فارغة إن وجدت. غير ذلك: كله تذكرة واحدة. */
133
  function splitTickets(raw){
134
  const text = normalizeText(raw);
135
  if(!text) return [];
136
-
137
- // (1) فواصل معروفة
138
  if(TICKET_SEP.test(text)){
139
  return text.split(TICKET_SEP).map(p=>p.trim()).filter(Boolean);
140
  }
141
-
142
- // (2) تقسيم على "نوع المشكلة" فقط
143
  const starts = findStartsByLabels(text, ["نوع المشكلة","نوع المشكله"])
144
  .sort((a,b)=>a-b)
145
- // تجاهل بدايات قريبة جدًا (حماية)
146
  .filter((pos,i,arr)=> i===0 || (pos - arr[i-1]) >= MIN_SPLIT_SPAN);
147
-
148
  if(starts.length >= 2){
149
  const parts=[];
150
  for(let i=0;i<starts.length;i++){
@@ -155,16 +125,11 @@ function splitTickets(raw){
155
  }
156
  return parts;
157
  }
158
-
159
- // (4) fallback: أسطر فارغة => كتل
160
  const blocks = text.split(/\n\s*\n+/).map(p=>p.trim()).filter(Boolean);
161
  if(blocks.length>1) return blocks;
162
-
163
- // خلاف ذلك: تذكرة واحدة
164
  return [text];
165
  }
166
 
167
- /* ======== استخراج الحقول ======== */
168
  function findAfterLabel(text, labels){
169
  const hay = "\n" + normalizeText(text) + "\n";
170
  for(const rawLbl of labels){
@@ -222,7 +187,6 @@ function extractFields(ticketText){
222
  return out;
223
  }
224
 
225
- /* ======== التصنيف ======== */
226
  function classifyTicket(text, fields){
227
  const hay = normalizeText(`${text}\n${fields?.["نوع المشكلة"]||""}`).toLowerCase();
228
  for(const label of CLASS_PRIORITY){
@@ -235,7 +199,6 @@ function classifyTicket(text, fields){
235
  return "استفسار";
236
  }
237
 
238
- /* ======== تطبيق المنطقة / الاسم ======== */
239
  function parseTicketsWithExtras(raw, agentName, defaultRegion){
240
  const regionChosen = (defaultRegion || "").toString();
241
  return splitTickets(raw||"").map(t => {
@@ -258,7 +221,6 @@ function parseTicketsWithExtras(raw, agentName, defaultRegion){
258
  });
259
  }
260
 
261
- /* ======== بناء/قراءة الجدول ======== */
262
  function buildTable(rows){
263
  const theadRow = document.getElementById("theadRow");
264
  const tbody = document.getElementById("tbody");
@@ -280,6 +242,7 @@ function buildTable(rows){
280
  tbody.appendChild(tr);
281
  });
282
  }
 
283
  function readTable(){
284
  const tbody = document.getElementById("tbody");
285
  const rows = [];
@@ -291,7 +254,6 @@ function readTable(){
291
  return rows;
292
  }
293
 
294
- /* ======== شارة + أزرار + تحقق ======== */
295
  function updateBadge(n){
296
  const b = document.getElementById("countBadge");
297
  b.textContent = n; b.hidden = (n===0);
@@ -309,14 +271,14 @@ function validateCells(){
309
  const td=tr.children[idxPhone];
310
  const raw=(td.textContent||"").trim();
311
  const digits = raw.replace(/\D/g,"");
312
- const invalid = !!raw && digits.length < 9; // أقل من 9 = خطأ
313
  td.classList.toggle("invalid", invalid);
314
  }
315
  if(idxID>=0){
316
  const td=tr.children[idxID];
317
  const raw=(td.textContent||"").trim();
318
  const digits = raw.replace(/\D/g,"");
319
- const invalid = !!raw && digits.length !== 10; // غير 10 = خطأ
320
  td.classList.toggle("invalid", invalid);
321
  }
322
  });
@@ -328,7 +290,6 @@ document.addEventListener("input",(e)=>{
328
  }
329
  });
330
 
331
- /* ======== Toast ======== */
332
  function toast(msg){
333
  const t = document.getElementById("toast");
334
  t.textContent = msg; t.hidden = false;
@@ -336,7 +297,6 @@ function toast(msg){
336
  setTimeout(()=>{ t.hidden = true; }, 2000);
337
  }
338
 
339
- /* ======== ExcelJS (كما سابقًا) ======== */
340
  async function exportExcel(){
341
  const rows = readTable();
342
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
@@ -425,7 +385,6 @@ async function exportExcel(){
425
  toast("تم تنزيل الملف بتنسيق القالب.");
426
  }
427
 
428
- /* ======== نسخ إلى الحافظة (TSV) ======== */
429
  async function copyToClipboardTSV(){
430
  const rows = readTable();
431
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
@@ -449,7 +408,6 @@ async function copyToClipboardTSV(){
449
  }
450
  }
451
 
452
- /* ======== مثال ======== */
453
  const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
454
  وقت حدوث المشكلة: 21/8/2025 7 صباحا
455
  اسم صاحب المشكلة : نوف الناصر
@@ -459,11 +417,10 @@ const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاست
459
  اسم المسح: الخبر 2025
460
  اسم المنطقة: الشرقية`;
461
 
462
- /* ======== تخزين محلي ======== */
463
- const STATE_KEY = "ticketParserState_v10_2";
464
  const ALL_STATE_KEYS = [
465
  "ticketParserState_v8","ticketParserState_v9","ticketParserState_v10",
466
- "ticketParserState_v10_1","ticketParserState_v10_2"
467
  ];
468
 
469
  function ensureColumns(rows, agentName, defaultRegion){
@@ -507,7 +464,6 @@ function loadState(){
507
  }catch{ return false; }
508
  }
509
 
510
- /* ======== مسح الكل (يمسح كل المفاتيح القديمة والجديدة) ======== */
511
  function clearAll(){
512
  const rawEl = document.getElementById("raw");
513
  const tbody = document.getElementById("tbody");
@@ -520,36 +476,68 @@ function clearAll(){
520
  if(agentEl) agentEl.value = "";
521
  if(regionEl) regionEl.value = "";
522
  updateBadge(0); setButtonsEnabled(false);
523
- try{
524
- ALL_STATE_KEYS.forEach(k=>localStorage.removeItem(k));
525
- }catch{}
526
  toast("تم مسح كل البيانات والتخزين.");
527
  }
528
 
529
- /* ======== لصق مُنظَّم (يمكن الضغط عدة مرات — يضيف ولا يس��بدل) ======== */
530
  function normalizeForPaste(text){
531
  const norm = normalizeText(text||"");
532
  const parts = splitTickets(norm);
533
  return parts.length ? parts.join("\n\n🔴🔴🔴\n") : norm;
534
  }
 
535
  async function smartPasteInto(el){
536
  try{
537
  const txt = await navigator.clipboard.readText();
538
- const formatted = normalizeForPaste(txt || "");
539
- if(!formatted){ toast("الحافظة فارغة."); return; }
540
- if(el.value && el.value.trim()){
541
- el.value = el.value.trimEnd() + "\n\n🔴🔴🔴\n" + formatted;
542
- }else{
543
- el.value = formatted;
 
 
 
 
544
  }
545
- saveState();
546
- 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");
@@ -568,14 +556,14 @@ function init(){
568
 
569
  parseBtn.addEventListener("click", ()=>{
570
  const raw = (rawEl.value || "").trim();
571
- if(!raw){ toast("فضلاً الصق/ي تذاكر أولاً (أو استخدمي «إدراج المثال»)."); return; }
572
  const agent = agentEl.value || "";
573
  const defRegion = regionEl.value || "";
574
  const rows = parseTicketsWithExtras(raw, agent, defRegion);
575
  buildTable(rows); validateCells();
576
  updateBadge(rows.length); setButtonsEnabled(rows.length>0);
577
  saveState();
578
- toast(defRegion ? `تم استخراج ${rows.length} وتطبيق المنطقة: ${defRegion}` : `تم استخراج ${rows.length} تذكرة.`);
579
  });
580
 
581
  exportBtn.addEventListener("click", exportExcel);
 
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
  "وقت حدوث المشكلة": ["وقت حدوث المشكله","وقت حدوث المشكلة","وقت المشكلة","وقت حدوث"],
 
16
  "المنطقة": ["المنطقة","المنطقه","اسم المنطقة","المدينة","المحافظة","منطقة"]
17
  };
18
 
 
19
  const CLASS_RULES = {
20
  "استفسار": ["استفسار","سؤال","استعلام","معلومة","استفسارات"],
21
  "إضافة أجهزة": ["اضافة جهاز","إضافة أجهزة","اضافة اجهزة","تركيب جهاز","جهاز جديد","تسجيل جهاز","ربط جهاز","اضافة ماسح","إضافة ماسح"],
 
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){
 
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);
 
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++){
 
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){
 
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){
 
199
  return "استفسار";
200
  }
201
 
 
202
  function parseTicketsWithExtras(raw, agentName, defaultRegion){
203
  const regionChosen = (defaultRegion || "").toString();
204
  return splitTickets(raw||"").map(t => {
 
221
  });
222
  }
223
 
 
224
  function buildTable(rows){
225
  const theadRow = document.getElementById("theadRow");
226
  const tbody = document.getElementById("tbody");
 
242
  tbody.appendChild(tr);
243
  });
244
  }
245
+
246
  function readTable(){
247
  const tbody = document.getElementById("tbody");
248
  const rows = [];
 
254
  return rows;
255
  }
256
 
 
257
  function updateBadge(n){
258
  const b = document.getElementById("countBadge");
259
  b.textContent = n; b.hidden = (n===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
  });
 
290
  }
291
  });
292
 
 
293
  function toast(msg){
294
  const t = document.getElementById("toast");
295
  t.textContent = msg; t.hidden = false;
 
297
  setTimeout(()=>{ t.hidden = true; }, 2000);
298
  }
299
 
 
300
  async function exportExcel(){
301
  const rows = readTable();
302
  if(!rows.length){ toast("لا يوجد بيانات لتصديرها."); return; }
 
385
  toast("تم تنزيل الملف بتنسيق القالب.");
386
  }
387
 
 
388
  async function copyToClipboardTSV(){
389
  const rows = readTable();
390
  if(!rows.length){ toast("لا يوجد بيانات لنسخها."); return; }
 
408
  }
409
  }
410
 
 
411
  const SAMPLE = `نوع المشكلة : لا استطيع اكمال الاستمارة بسبب تعليق
412
  وقت حدوث المشكلة: 21/8/2025 7 صباحا
413
  اسم صاحب المشكلة : نوف الناصر
 
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){
 
464
  }catch{ return false; }
465
  }
466
 
 
467
  function clearAll(){
468
  const rawEl = document.getElementById("raw");
469
  const tbody = document.getElementById("tbody");
 
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");
 
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);