File size: 31,713 Bytes
673b4f0
 
 
 
 
 
 
 
 
 
 
87c3e98
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9195857
 
673b4f0
9195857
 
 
673b4f0
9195857
673b4f0
9195857
 
673b4f0
 
9195857
673b4f0
87c3e98
 
9195857
 
673b4f0
 
87c3e98
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87c3e98
673b4f0
 
 
d025295
db69a0c
 
 
 
4a52349
a6eccd4
4a52349
 
5fe2177
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fe2177
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
03d50ce
 
 
 
a6eccd4
673b4f0
4a52349
 
 
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a44c048
 
 
 
673b4f0
 
 
a6eccd4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db69a0c
a6eccd4
 
 
 
d025295
a6eccd4
 
 
 
 
db69a0c
a6eccd4
 
 
 
 
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d025295
 
673b4f0
 
 
 
 
 
 
 
 
db69a0c
673b4f0
 
 
db69a0c
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fe2177
673b4f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fe2177
 
 
 
a6eccd4
 
 
 
 
 
 
 
 
5fe2177
 
 
a6eccd4
5fe2177
 
a6eccd4
673b4f0
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
/* طبقة البيانات والفهرسة: تبقى كل المعالجة محلية داخل المتصفح. */
const FIELD_ALIASES = {
  commercialRegister: ["السجل التجاري", "رقم السجل التجاري", "رقم السجل", "السجل", "commercial register", "cr"],
  facilityName: ["اسم المنشأة", "إسم المنشأة", "المنشأة", "اسم المنشاه", "facility", "establishment name"],
  researcher: ["اسم الباحث", "اسم الباحث/ة", "الباحث", "researcher"],
  collectionStatus: ["حالة الاستيفاء", "حالة جمع البيانات", "collection status"],
  approvalStatus: ["حالة الاعتماد", "اعتماد", "الوصف", "الوصف.1", "approval"],
  surveyName: ["اسم المسح", "المسح", "survey", "survey name"],
};

const PASSCODE = "2030";
const app = { rows: [], byRegister: new Map(), entityGroups: new Map(), researchers: new Map(), surveys: new Map(), batch: [] };
const $ = (id) => document.getElementById(id);
const digits = { "٠":"0", "١":"1", "٢":"2", "٣":"3", "٤":"4", "٥":"5", "٦":"6", "٧":"7", "٨":"8", "٩":"9", "۰":"0", "۱":"1", "۲":"2", "۳":"3", "۴":"4", "۵":"5", "۶":"6", "۷":"7", "۸":"8", "۹":"9" };

function plain(value) { return String(value ?? "").replace(/[\u200E\u200F\u202A-\u202E\u00A0]/g, " ").replace(/\s+/g, " ").trim(); }
function normText(value) { return plain(value).toLowerCase().replace(/[إأآا]/g, "ا").replace(/ى/g, "ي").replace(/ة/g, "ه").replace(/[\u064B-\u065F\u0670]/g, ""); }
function normalizeDigits(value) { return plain(value).replace(/[٠-٩۰-۹]/g, (c) => digits[c]); }

function expandScientific(value) {
  const m = value.match(/^([+\-]?)(\d+)(?:\.(\d*))?[eE]([+\-]?\d+)$/);
  if (!m) return value;
  const sign = m[1] === "-" ? "-" : "";
  const whole = m[2];
  const fraction = m[3] || "";
  const power = Number(m[4]);
  const all = whole + fraction;
  const point = whole.length + power;
  if (!Number.isInteger(power) || point < 0 || point > 80) return value;
  if (point >= all.length) return sign + all + "0".repeat(point - all.length);
  if (point <= 0) return sign + "0." + "0".repeat(-point) + all;
  return sign + all.slice(0, point) + "." + all.slice(point);
}

function normalizeRegister(value) {
  let v = expandScientific(normalizeDigits(value));
  if (!v || /^(nan|null|undefined)$/i.test(v)) return "";
  if (/^\d+\.0+$/.test(v)) v = v.replace(/\.0+$/, "");
  if (/^[\d\s,،._\-/]+$/.test(v)) return v.replace(/[\s,،._\-/]/g, "");
  return v;
}

function getField(raw, aliases) {
  const entries = Object.entries(raw || {});
  for (const alias of aliases) {
    const hit = entries.find(([key]) => normText(key) === normText(alias));
    if (hit && plain(hit[1])) return plain(hit[1]);
  }
  return "";
}

function getStatusClass(value) {
  const v = normText(value);
  if (/اعطت كامل|استيفاء ذاتي|مكتمل|معتمد/.test(v) && !/غير/.test(v)) return "ok";
  if (/رفض|لم يعثر|مغلق|تعذر|غير مكتمل|ناقص|خارج نطاق/.test(v)) return "danger";
  if (/يطلب التواصل|بعض البيانات|جديد|اخرى|تحولت/.test(v)) return "warn";
  return "";
}

function entityKey(row, id) { return row.commercialRegister ? `cr:${row.commercialRegister}` : `row:${id}`; }
function dataPath() { return (typeof SURVEY_DATA_CONFIG !== "undefined" && SURVEY_DATA_CONFIG.excelPath) || "./data/تقاطع.xlsx"; }
function rowHasData(raw) { return Object.entries(raw || {}).some(([key, value]) => !String(key).startsWith("__") && plain(value)); }

async function readWorkbookRows() {
  const path = dataPath();
  if (typeof XLSX === "undefined") throw new Error("تعذر تحميل مكتبة قراءة Excel.");
  const response = await fetch(path, { cache: "no-store" });
  if (!response.ok) throw new Error(`تعذر العثور على ملف بيانات المسوح. المسار: ${path}`);
  const workbook = XLSX.read(await response.arrayBuffer(), { type: "array", cellText: true, cellDates: false });
  const rows = [];
  workbook.SheetNames.forEach((sheetName) => {
    const sheetRows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: "", raw: false, blankrows: false });
    sheetRows.filter(rowHasData).forEach((raw) => rows.push({ ...raw, "اسم المسح": sheetName, "__sheet": sheetName }));
  });
  return { rows, path };
}

async function loadData() {
  const source = await readWorkbookRows();
  if (!source.rows.length) throw new Error(`تعذر تحميل بيانات المسوح من الملف: ${source.path}`);
  app.rows = source.rows.map((raw, index) => {
    const commercialRegister = normalizeRegister(getField(raw, FIELD_ALIASES.commercialRegister));
    return {
      id: index,
      raw,
      commercialRegister,
      sourceSheet: raw.__sheet || getField(raw, FIELD_ALIASES.surveyName),
      facilityName: getField(raw, FIELD_ALIASES.facilityName),
      researcher: getField(raw, FIELD_ALIASES.researcher),
      collectionStatus: getField(raw, FIELD_ALIASES.collectionStatus),
      approvalStatus: getField(raw, FIELD_ALIASES.approvalStatus),
      surveyName: getField(raw, FIELD_ALIASES.surveyName) || raw.__sheet || "مسح غير معروف",
    };
  });

  app.rows.forEach((row) => {
    const key = entityKey(row, row.id);
    if (!app.entityGroups.has(key)) app.entityGroups.set(key, { key, commercialRegister: row.commercialRegister, rows: [] });
    app.entityGroups.get(key).rows.push(row);

    if (row.commercialRegister) {
      if (!app.byRegister.has(row.commercialRegister)) app.byRegister.set(row.commercialRegister, []);
      app.byRegister.get(row.commercialRegister).push(row);
    }

    const researcherKey = normText(row.researcher);
    if (researcherKey) {
      if (!app.researchers.has(researcherKey)) app.researchers.set(researcherKey, { key: researcherKey, name: row.researcher, rows: [] });
      app.researchers.get(researcherKey).rows.push(row);
    }

    if (!app.surveys.has(row.surveyName)) app.surveys.set(row.surveyName, []);
    app.surveys.get(row.surveyName).push(row);
  });
}

function escapeHtml(v) { return String(v ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;"); }
function td(label, content) { return `<td data-label="${escapeHtml(label)}">${content}</td>`; }
function stackedList(items) {
  const clean = items.map((item) => plain(item)).filter(Boolean);
  return clean.length ? `<div class="stacked-list">${clean.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}</div>` : "-";
}
function metric(value, label, target = "", description = "") {
  const action = target ? ` role="button" tabindex="0" data-dashboard-detail="${target}"` : "";
  const note = description ? `<small>${escapeHtml(description)}</small>` : "";
  return `<div class="metric${target ? " metric-action" : ""}"${action}><b>${value}</b><span>${label}</span>${note}</div>`;
}

function countBy(rows, picker) {
  return rows.reduce((map, row) => {
    const value = picker(row) || "غير محدد";
    map.set(value, (map.get(value) || 0) + 1);
    return map;
  }, new Map());
}

function sharedCount(rows) {
  return new Set(rows.filter((row) => row.commercialRegister && new Set(app.byRegister.get(row.commercialRegister).map((z) => z.surveyName)).size > 1).map((row) => row.commercialRegister)).size;
}

function groupInfo(group) {
  const rows = group.rows;
  const names = [...new Set(rows.map((row) => row.facilityName).filter(Boolean))];
  const surveys = [...new Set(rows.map((row) => row.surveyName))];
  const researchers = [...new Set(rows.map((row) => row.researcher).filter(Boolean))];
  return { ...group, name: names[0] || "-", names, surveys, researchers, multiSurvey: surveys.length > 1 };
}

function groupForRows(rows) {
  const groups = new Map();
  rows.forEach((row) => {
    const key = entityKey(row, row.id);
    if (!groups.has(key)) groups.set(key, { key, commercialRegister: row.commercialRegister, rows: [] });
    groups.get(key).rows.push(row);
  });
  return [...groups.values()].map(groupInfo);
}

function unlockApp() {
  const gate = $("loginGate");
  if (!gate || gate.classList.contains("hidden")) return;
  gate.classList.add("hidden");
  setTimeout(() => gate.remove(), 260);
  setTimeout(() => document.querySelector("[data-view='search']")?.focus(), 300);
}

function setupLoginGate() {
  const input = $("passcodeInput");
  if (!input) return;
  input.focus();
  $("loginForm")?.addEventListener("submit", (event) => {
    event.preventDefault();
    if (input.value === PASSCODE) unlockApp();
  });
  input.addEventListener("input", () => {
    input.value = normalizeDigits(input.value).replace(/\D/g, "").slice(0, 4);
    if (input.value === PASSCODE) unlockApp();
  });
  input.addEventListener("keydown", (event) => {
    if (event.key === "Enter" && input.value === PASSCODE) unlockApp();
  });
}

function showView(name) {
  document.querySelectorAll(".view").forEach((view) => view.classList.toggle("active", view.id === name));
  document.querySelectorAll(".nav-link").forEach((button) => button.classList.toggle("active", button.dataset.view === name));
  window.scrollTo({ top: 0, behavior: "smooth" });
}

function toast(message) {
  const el = $("toast");
  el.textContent = message;
  el.classList.add("show");
  clearTimeout(toast.timer);
  toast.timer = setTimeout(() => el.classList.remove("show"), 3200);
}

function renderMetrics() {
  const intersectionGroups = [...app.byRegister.entries()]
    .map(([register, rows]) => groupInfo({ key: `cr:${register}`, commercialRegister: register, rows }))
    .filter((group) => group.multiSurvey);
  const shared = intersectionGroups.length;
  const statusDifferenceGroups = intersectionGroups.filter((group) => new Set(group.rows.map((row) => plain(row.collectionStatus)).filter(Boolean)).size > 1);
  $("metrics").innerHTML = [
    metric(shared.toLocaleString("ar-SA"), "عدد التقاطعات", "intersections", "سجلات ظهرت في أكثر من مسح"),
    metric(statusDifferenceGroups.length.toLocaleString("ar-SA"), "اختلاف حالات الاستيفاء", "statuses", "اختلاف حالة المنشأة بين المسوح"),
    metric(shared.toLocaleString("ar-SA"), "المنشآت المتقاطعة", "establishments", "منشآت مشتركة قابلة للمراجعة"),
  ].join("");
}

function renderEntity(group) {
  const g = groupInfo(group);
  const title = g.names.length > 1 ? `${escapeHtml(g.name)} <span class="badge warn">أسماء متعددة</span>` : escapeHtml(g.name);
  const note = g.multiSurvey ? `<span class="badge warn">موجودة في أكثر من مسح</span>` : `<span class="badge ok">موجودة في مسح واحد</span>`;
  const occurrences = [...g.rows].sort((a, b) => a.surveyName.localeCompare(b.surveyName, "ar")).map((row) => `<div class="occurrence">
    <div><span>المسح</span>${escapeHtml(row.surveyName)}</div>
    <div><span>الباحث</span>${escapeHtml(row.researcher || "-")}</div>
    <div><span>حالة الاستيفاء</span><i class="badge ${getStatusClass(row.collectionStatus)}">${escapeHtml(row.collectionStatus || "-")}</i></div>
  </div>`).join("");

  return `<article class="entity-card">
    <div class="entity-top">
      <div><h3>${title}</h3><p>السجل التجاري: <b>${escapeHtml(g.commercialRegister || "غير متاح في المصدر")}</b></p></div>
      <div class="badges"><span class="badge">${g.surveys.length} مسح</span><span class="badge">${g.rows.length} ظهور</span><span class="badge">${g.researchers.length} باحث</span>${note}</div>
    </div>
    <div class="occurrences">
      <div class="occurrence occurrence-head"><div>المسح</div><div>الباحث</div><div>حالة الاستيفاء</div></div>
      ${occurrences}
    </div>
  </article>`;
}

function intersectionGroups() {
  return [...app.byRegister.entries()]
    .map(([register, rows]) => groupInfo({ key: `cr:${register}`, commercialRegister: register, rows }))
    .filter((group) => group.multiSurvey)
    .sort((a, b) => b.surveys.length - a.surveys.length || a.name.localeCompare(b.name, "ar"));
}

function dashboardDetailHeader(title, subtitle) {
  return `<div class="dashboard-detail-head"><div><h2>${escapeHtml(title)}</h2><p>${escapeHtml(subtitle)}</p></div><button class="btn secondary dashboard-close" type="button">إخفاء</button></div>`;
}

function renderDashboardDetail(type) {
  const panel = $("dashboardDetails");
  if (!panel) return;
  const groups = intersectionGroups();
  const sharedRows = groups.flatMap((group) => group.rows);
  panel.className = "dashboard-details card";

  if (type === "statuses") {
    const statusDiffs = groups.filter((group) => new Set(group.rows.map((row) => plain(row.collectionStatus)).filter(Boolean)).size > 1);
    panel.innerHTML = `${dashboardDetailHeader("اختلاف حالات الاستيفاء بين المسوح", "منشآت متقاطعة تختلف حالة الاستيفاء لها من مسح إلى آخر.")}
      <div class="table-wrap"><table><thead><tr><th>السجل التجاري</th><th>اسم المنشأة</th><th>الحالات حسب المسح</th><th>عدد الحالات</th></tr></thead><tbody>${statusDiffs.slice(0, 50).map((group) => {
        const statuses = group.rows
          .sort((a, b) => a.surveyName.localeCompare(b.surveyName, "ar"))
          .map((row) => `${row.surveyName}: ${row.collectionStatus || "-"}`);
        const count = new Set(group.rows.map((row) => plain(row.collectionStatus)).filter(Boolean)).size;
        return `<tr>${td("السجل التجاري", `<button class="row-button" data-entity="${escapeHtml(group.key)}">${escapeHtml(group.commercialRegister)}</button>`)}${td("اسم المنشأة", escapeHtml(group.name))}${td("الحالات حسب المسح", stackedList(statuses))}${td("عدد الحالات", count)}</tr>`;
      }).join("")}</tbody></table></div>`;
  } else if (type === "researchers") {
    const researchers = [...countBy(sharedRows, (row) => row.researcher).entries()].sort((a, b) => b[1] - a[1]).slice(0, 20);
    panel.innerHTML = `${dashboardDetailHeader("الباحثون الذين لديهم تقاطعات", "أعلى الباحثين ظهورًا ضمن المنشآت المتقاطعة بين المسوح.")}
      <div class="table-wrap compact-table"><table><thead><tr><th>الباحث</th><th>عدد الظهور في التقاطعات</th></tr></thead><tbody>${researchers.map(([name, count]) => `<tr>${td("الباحث", escapeHtml(name))}${td("عدد الظهور في التقاطعات", count.toLocaleString("ar-SA"))}</tr>`).join("")}</tbody></table></div>`;
  } else {
    const filtered = groups;
    const title = type === "establishments" ? "المنشآت المتقاطعة" : "تفاصيل التقاطعات";
    const subtitle = "السجلات التجارية التي ظهرت في أكثر من مسح.";
    panel.innerHTML = `${dashboardDetailHeader(title, subtitle)}
      <div class="table-wrap"><table><thead><tr><th>السجل التجاري</th><th>اسم المنشأة</th><th>عدد المسوح</th><th>المسوح</th><th>الباحثون</th></tr></thead><tbody>${filtered.slice(0, 50).map((group) => `<tr>${td("السجل التجاري", `<button class="row-button" data-entity="${escapeHtml(group.key)}">${escapeHtml(group.commercialRegister)}</button>`)}${td("اسم المنشأة", escapeHtml(group.name))}${td("عدد المسوح", group.surveys.length)}${td("المسوح", stackedList(group.surveys))}${td("الباحثون", stackedList(group.researchers))}</tr>`).join("")}</tbody></table></div>`;
  }

  panel.scrollIntoView({ behavior: "smooth", block: "start" });
}

function search() {
  const input = $("q").value;
  const q = normText(input);
  const reg = normalizeRegister(input);
  if (!q) {
    $("searchResults").className = "results empty-state";
    $("searchResults").textContent = "ابدأ بالكتابة لعرض النتائج مباشرة.";
    $("searchMeta").textContent = "البحث يعمل تلقائيًا أثناء الكتابة.";
    return;
  }

  let rows = [];
  let mode = "";
  if (reg && app.byRegister.has(reg)) {
    rows = app.byRegister.get(reg);
    mode = "سجل تجاري";
  } else {
    rows = app.rows.filter((row) => normText(row.facilityName).includes(q) || normText(row.researcher).includes(q));
    mode = "اسم منشأة أو باحث";
  }

  const groups = groupForRows(rows);
  $("searchMeta").textContent = groups.length ? `تم العثور على ${groups.length} نتيجة حسب ${mode}.` : "لم يتم العثور على نتائج مطابقة.";
  $("searchResults").className = "results";
  $("searchResults").innerHTML = groups.length ? groups.slice(0, 100).map(renderEntity).join("") : "<div class='empty-state'>لم يتم العثور على نتائج مطابقة.</div>";
  if (groups.length > 100) toast("تم عرض أول 100 نتيجة حفاظًا على سرعة الصفحة.");
}

function parseBatch(text) {
  const rawTokens = String(text || "").split(/[\s,،;؛\t\n\r]+/).map(plain).filter(Boolean);
  const values = [];
  const seen = new Set();
  let invalid = 0;
  let duplicates = 0;

  rawTokens.forEach((token) => {
    const register = normalizeRegister(token);
    if (!/^\d+$/.test(register)) {
      invalid += 1;
      return;
    }
    if (seen.has(register)) {
      duplicates += 1;
      return;
    }
    seen.add(register);
    values.push(register);
  });

  return { values, duplicates, invalid, entered: rawTokens.length };
}

function updateBatchHint() {
  const parsed = parseBatch($("batchInput").value);
  if (!parsed.entered) {
    $("batchHint").textContent = "لم تُدخل أي سجلات بعد.";
    return;
  }
  const ignored = [];
  if (parsed.duplicates) ignored.push(`${parsed.duplicates} مكرر`);
  if (parsed.invalid) ignored.push(`${parsed.invalid} يحتوي أحرفًا أو رموزًا`);
  $("batchHint").textContent = `تم التعرف على ${parsed.values.length} سجل رقمي${ignored.length ? `، وتم تجاهل ${ignored.join(" و")}.` : "."}`;
}

function batchCheck() {
  const parsed = parseBatch($("batchInput").value);
  if (!parsed.values.length) {
    toast("أدخل أرقام سجلات فقط، بدون أحرف أو رموز.");
    return;
  }

  app.batch = parsed.values.map((register, order) => {
    const rows = app.byRegister.get(register) || [];
    const group = rows.length ? groupInfo({ key: `cr:${register}`, commercialRegister: register, rows }) : null;
    return { order, register, group };
  });

  const found = app.batch.filter((item) => item.group);
  const multi = found.filter((item) => item.group.surveys.length > 1);
  $("batchSummary").innerHTML = [
    metric(app.batch.length, "إجمالي السجلات"),
    metric(found.length, "الموجودة"),
    metric(app.batch.length - found.length, "غير الموجودة"),
    metric(found.length - multi.length, "في مسح واحد"),
    metric(multi.length, "في عدة مسوح"),
  ].join("");
  $("batchMeta").textContent = `تم فحص ${app.batch.length} سجل رقمي بالترتيب الذي أُدخلت به.`;
  $("batchResults").className = "results";
  $("batchResults").innerHTML = renderBatchTable(app.batch);
  $("btnExport").disabled = false;
}

function renderBatchTable(items) {
  return `<div class="table-wrap"><table><thead><tr><th>رقم السجل التجاري</th><th>اسم المنشأة</th><th>مرات الظهور</th><th>الباحثون</th><th>حالة المطابقة</th></tr></thead><tbody>${items.map((item) => {
    const g = item.group;
    if (!g) return `<tr>${td("رقم السجل التجاري", escapeHtml(item.register))}${td("اسم المنشأة", "-")}${td("مرات الظهور", "0")}${td("الباحثون", "-")}${td("حالة المطابقة", `<span class="badge danger">غير موجود</span>`)}</tr>`;
    return `<tr>${td("رقم السجل التجاري", `<button class="row-button" data-entity="${escapeHtml(g.key)}">${escapeHtml(item.register)}</button>`)}${td("اسم المنشأة", escapeHtml(g.name))}${td("مرات الظهور", g.rows.length)}${td("الباحثون", g.researchers.length)}${td("حالة المطابقة", `<span class="badge ${g.multiSurvey ? "warn" : "ok"}">${g.multiSurvey ? "موجود في عدة مسوح" : "موجود"}</span>`)}</tr>`;
  }).join("")}</tbody></table></div>`;
}

function renderIntersections() {
  const groups = [...app.byRegister.entries()].map(([register, rows]) => groupInfo({ key: `cr:${register}`, commercialRegister: register, rows })).filter((group) => group.multiSurvey).sort((a, b) => b.surveys.length - a.surveys.length);
  const two = groups.filter((group) => group.surveys.length === 2).length;
  const three = groups.filter((group) => group.surveys.length >= 3).length;
  $("intersectionMetrics").innerHTML = [metric(groups.length, "منشآت مشتركة"), metric(two, "في مسحين"), metric(three, "في 3 مسوح أو أكثر"), metric(app.byRegister.size, "سجلات قابلة للمطابقة"), metric(app.surveys.size, "إجمالي المسوح")].join("");
  $("intersectionResults").className = "results";
  $("intersectionResults").innerHTML = groups.length ? `<div class="table-wrap"><table><thead><tr><th>السجل التجاري</th><th>اسم المنشأة</th><th>عدد المسوح</th><th>المسوح</th><th>الباحثون</th></tr></thead><tbody>${groups.slice(0, 200).map((group) => `<tr>${td("السجل التجاري", `<button class="row-button" data-entity="${escapeHtml(group.key)}">${escapeHtml(group.commercialRegister)}</button>`)}${td("اسم المنشأة", escapeHtml(group.name))}${td("عدد المسوح", group.surveys.length)}${td("المسوح", stackedList(group.surveys))}${td("الباحثون", stackedList(group.researchers))}</tr>`).join("")}</tbody></table></div>` : "<div class='empty-state'>لا توجد تقاطعات قابلة للحساب.</div>";
}

function renderCompactEntities(groups) {
  return `<div class="table-wrap compact-table"><table><thead><tr><th>السجل التجاري</th><th>اسم المنشأة</th><th>المسوح</th><th>الباحثون</th></tr></thead><tbody>${groups.map((group) => `<tr>${td("السجل التجاري", `<button class="row-button" data-entity="${escapeHtml(group.key)}">${escapeHtml(group.commercialRegister || "-")}</button>`)}${td("اسم المنشأة", escapeHtml(group.name))}${td("المسوح", stackedList(group.surveys))}${td("الباحثون", stackedList(group.researchers))}</tr>`).join("")}</tbody></table></div>`;
}

function renderResearchers() {
  const q = normText($("researcherFilter").value);
  const entries = [...app.researchers.values()].filter((item) => normText(item.name).includes(q)).sort((a, b) => b.rows.length - a.rows.length).slice(0, 80);
  $("researcherResults").innerHTML = entries.length ? entries.map((researcher) => {
    const surveys = countBy(researcher.rows, (row) => row.surveyName);
    const statuses = countBy(researcher.rows, (row) => row.collectionStatus);
    const sampleGroups = groupForRows(researcher.rows).slice(0, 10);
    return `<details class="card detail-card researcher-card">
      <summary><span><strong>${escapeHtml(researcher.name)}</strong><small>${researcher.rows.length} منشأة/سجل · ${surveys.size} مسح · ${sharedCount(researcher.rows)} منشأة متقاطعة</small></span><i>عرض التفاصيل</i></summary>
      <div class="detail-body">
        <div class="mini-grid">${[...surveys.entries()].map(([name, count]) => metric(count, name)).join("")}</div>
        <h4>حالات الاستيفاء</h4>
        <div class="badges status-cloud">${[...statuses.entries()].map(([status, count]) => `<span class="badge ${getStatusClass(status)}">${escapeHtml(status)} · ${count}</span>`).join("")}</div>
        <h4>نماذج من السجلات</h4>
        ${renderCompactEntities(sampleGroups)}
      </div>
    </details>`;
  }).join("") : "<div class='empty-state'>لا يوجد باحث مطابق.</div>";
}

function renderSurveys() {
  $("surveyResults").innerHTML = [...app.surveys.entries()].map(([name, rows]) => {
    const researchers = countBy(rows, (row) => row.researcher);
    const statuses = countBy(rows, (row) => row.collectionStatus);
    const shared = sharedCount(rows);
    const sampleGroups = groupForRows(rows.filter((row) => row.commercialRegister && new Set(app.byRegister.get(row.commercialRegister).map((z) => z.surveyName)).size > 1)).slice(0, 8);
    return `<details class="card detail-card survey-card">
      <summary><span><strong>${escapeHtml(name)}</strong><small>${rows.length.toLocaleString("ar-SA")} سجل · ${researchers.size} باحث · ${shared} منشأة متقاطعة</small></span><i>عرض التفاصيل</i></summary>
      <div class="detail-body">
        <div class="mini-grid">${metric(rows.length.toLocaleString("ar-SA"), "إجمالي السجلات")}${metric(researchers.size, "عدد الباحثين")}${metric(shared, "منشآت متقاطعة")}</div>
        <h4>حالات الاستيفاء في المسح</h4>
        <div class="badges status-cloud">${[...statuses.entries()].map(([status, count]) => `<span class="badge ${getStatusClass(status)}">${escapeHtml(status)} · ${count}</span>`).join("")}</div>
        <h4>أعلى الباحثين حسب عدد السجلات</h4>
        <div class="badges status-cloud">${[...researchers.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([researcher, count]) => `<span class="badge">${escapeHtml(researcher)} · ${count}</span>`).join("")}</div>
        <h4>نماذج من التقاطعات</h4>
        ${sampleGroups.length ? renderCompactEntities(sampleGroups) : "<div class='empty-state small'>لا توجد تقاطعات في هذا المسح.</div>"}
      </div>
    </details>`;
  }).join("");
}

function escapeXml(v) { return String(v ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
function xmlCell(value, style = "") { return `<Cell${style ? ` ss:StyleID="${style}"` : ""}><Data ss:Type="String">${escapeXml(value)}</Data></Cell>`; }

function exportBatch() {
  if (!app.batch.length) {
    toast("لا توجد نتائج لتصديرها.");
    return;
  }
  const max = Math.max(0, ...app.batch.map((item) => item.group?.rows.length || 0));
  const headers = ["رقم السجل التجاري", "اسم المنشأة", "عدد مرات الظهور", "حالة المطابقة"];
  for (let i = 1; i <= max; i += 1) headers.push(`الباحث ${i}`, `المسح ${i}`, `حالة الاستيفاء ${i}`);
  const data = app.batch.map((item) => {
    const g = item.group;
    const values = [item.register, g?.name || "", g?.rows.length || 0, g ? (g.multiSurvey ? "موجود في عدة مسوح" : "موجود") : "غير موجود"];
    const occurrences = g ? [...g.rows].sort((a, b) => a.surveyName.localeCompare(b.surveyName, "ar")) : [];
    for (let i = 0; i < max; i += 1) {
      const row = occurrences[i];
      values.push(row?.researcher || "", row?.surveyName || "", row?.collectionStatus || "");
    }
    return values;
  });
  const xml = `<?xml version="1.0" encoding="UTF-8"?><?mso-application progid="Excel.Sheet"?><Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel"><Styles><Style ss:ID="h"><Font ss:Bold="1" ss:Color="#FFFFFF"/><Interior ss:Color="#226D68" ss:Pattern="Solid"/><Alignment ss:Horizontal="Center" ss:Vertical="Center"/></Style></Styles><Worksheet ss:Name="نتائج الفحص" ss:RightToLeft="1"><Table><Row>${headers.map((h) => xmlCell(h, "h")).join("")}</Row>${data.map((row) => `<Row>${row.map((v) => xmlCell(v)).join("")}</Row>`).join("")}</Table><AutoFilter x:Range="R1C1:R${data.length + 1}C${headers.length}" xmlns="urn:schemas-microsoft-com:office:excel"/><WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel"><FreezePanes/><FrozenNoSplit/><SplitHorizontal>1</SplitHorizontal><TopRowBottomPane>1</TopRowBottomPane></WorksheetOptions></Worksheet></Workbook>`;
  const blob = new Blob(["\uFEFF" + xml], { type: "application/vnd.ms-excel;charset=utf-8" });
  const a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = "نتائج_فحص_السجلات.xls";
  a.click();
  URL.revokeObjectURL(a.href);
  toast("تم إنشاء ملف Excel بنجاح.");
}

function openEntity(key) {
  const group = app.entityGroups.get(key);
  if (!group) return;
  showView("search");
  $("q").value = group.commercialRegister || group.rows[0]?.facilityName || "";
  $("searchResults").className = "results";
  $("searchResults").innerHTML = renderEntity(group);
  $("searchMeta").textContent = "تفاصيل السجل المحدد.";
}

function debounce(fn, wait = 180) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

async function init() {
  setupLoginGate();
  try {
    await loadData();
    renderMetrics();
    renderIntersections();
    renderResearchers();
    renderSurveys();
  } catch (err) {
    console.error(err);
    document.querySelector("main").innerHTML = "<div class='empty-state'>تعذر العثور على ملف بيانات المسوح.</div>";
    return;
  }

  const debouncedSearch = debounce(search);
  document.querySelectorAll(".nav-link").forEach((button) => button.addEventListener("click", () => showView(button.dataset.view)));
  $("q").addEventListener("input", debouncedSearch);
  $("q").addEventListener("keydown", (event) => { if (event.key === "Enter") search(); });
  $("btnClear").addEventListener("click", () => {
    $("q").value = "";
    search();
    $("q").focus();
  });
  $("batchInput").addEventListener("input", updateBatchHint);
  $("btnBatch").addEventListener("click", batchCheck);
  $("btnBatchClear").addEventListener("click", () => {
    $("batchInput").value = "";
    app.batch = [];
    updateBatchHint();
    $("batchSummary").innerHTML = "";
    $("batchResults").className = "results empty-state";
    $("batchResults").textContent = "الصق قائمة السجلات ثم اختر «فحص السجلات».";
    $("btnExport").disabled = true;
  });
  $("btnExport").addEventListener("click", exportBatch);
  $("researcherFilter").addEventListener("input", renderResearchers);
  document.addEventListener("click", (event) => {
    const button = event.target.closest("[data-entity]");
    if (button) {
      openEntity(button.dataset.entity);
      return;
    }
    const closeDashboard = event.target.closest(".dashboard-close");
    if (closeDashboard) {
      const panel = $("dashboardDetails");
      panel.className = "dashboard-details empty-state";
      panel.textContent = "اختر إحدى البطاقات لعرض التفاصيل هنا.";
      return;
    }
    const dashboardCard = event.target.closest("[data-dashboard-detail]");
    if (dashboardCard) renderDashboardDetail(dashboardCard.dataset.dashboardDetail);
  });
  document.addEventListener("keydown", (event) => {
    if (event.key !== "Enter" && event.key !== " ") return;
    const dashboardCard = event.target.closest("[data-dashboard-detail]");
    if (!dashboardCard) return;
    event.preventDefault();
    renderDashboardDetail(dashboardCard.dataset.dashboardDetail);
  });
}

init();