stat2025 commited on
Commit
1f30211
·
verified ·
1 Parent(s): 35f6a40

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +13 -24
  2. app.js +431 -315
  3. data.js +0 -0
  4. generate-data.mjs +137 -56
  5. index.html +95 -61
  6. style.css +708 -1006
README.md CHANGED
@@ -1,41 +1,30 @@
1
  ---
2
- title: ICS
3
- emoji: "🏭"
4
  colorFrom: green
5
  colorTo: blue
6
  sdk: static
7
  pinned: false
8
  license: mit
9
- short_description: نظام استعلام المنشآت الصناعية
10
  ---
11
 
12
- # شاشة استعلام
13
 
14
- واجهة عربية ثابتة للاستعلام عن المنشآت حسب المنطقة والمدينة الصناعية، مع بحث مباشر باسم المنشأة أو السجل التجاري.
15
 
16
- ## المناطق والمدن
17
 
18
- - منطقة الدمام: الصناعية الأولى بالدمام، الصناعية الثانية بالدمام، الصناعية الثالثة بالدمام
19
- - منطقة الأحساء: المدينة صناعية الأولى بالأحساء
20
- - منطقة حفر الباطن: المدينة الصناعية بحفر الباطن
21
- - سبارك: مدينة الملك سلمان
22
-
23
- ## الإحداثيات والتواصل
24
-
25
- - إذا كانت الخانة تحتوي على إحداثيات رقمية، يظهر رابط مباشر إلى خرائط Google.
26
- - إذا كانت الخانة فارغة، لا يظهر حقل الإحداثيات.
27
- - إذا كانت الخانة تحتوي على نص تواصل ورقم جوال، تظهر باسم "إفادة مدن" مع أزرار نسخ الرقم وواتساب ونسخ رسالة مقترحة.
28
- - يتم تمييز الإحداثيات وإفادة مدن بصريًا بألوان مختلفة.
29
- - يحفظ النظام آخر منطقة ومدينة وبحث تم استخدامها على نفس المتصفح.
30
 
31
  ## تحديث البيانات
32
 
33
- رمز الدخول الافتراضي في النسخة الحالية هو `20302030`.
34
-
35
- لتحديث ملف البيانات المشفر:
36
-
37
  ```powershell
38
- node generate-data.mjs "path\to\input.xlsx" "data.js" "PASSWORD"
39
  ```
40
 
41
- لا ترفع ملف Excel الأصلي إلى المستودع؛ ارفع `data.js` بعد توليده فقط.
 
1
  ---
2
+ title: ICS2
3
+ emoji: "🗺️"
4
  colorFrom: green
5
  colorTo: blue
6
  sdk: static
7
  pinned: false
8
  license: mit
9
+ short_description: بوابة الباحث الميداني الشاملة
10
  ---
11
 
12
+ # بوابة الباحث الميداني
13
 
14
+ واجهة عربية متجاوبة تعرض لكل باحث عيناته وخريطته الشاملة وأدوات التواصل الخاصة بالمدن الصناعية.
15
 
16
+ ## آلية البيانات
17
 
18
+ - اسم المدينة يعتمد على عمود المدينة الخاص بالصفحة، ثم يرجع إلى عمود المدينة الأساسي عند غيابه.
19
+ - إحداثية مدن هي الموقع المعتمد أولًا.
20
+ - عند عدم توفر إحداثية مدن، تستخدم إحداثيات X وY الأساسية.
21
+ - تعرض إفادة مدن منفصلة عن الموقع، وتظهر العينات دون تفاصيل موقع في نهاية النتائج.
22
+ - البيانات مشفرة داخل `data.js`، ورمز الدخول الافتراضي هو `20302030`.
 
 
 
 
 
 
 
23
 
24
  ## تحديث البيانات
25
 
 
 
 
 
26
  ```powershell
27
+ node generate-data.mjs "path\to\شامل.xlsx" "data.js" "20302030"
28
  ```
29
 
30
+ لا ترفع ملف Excel إلى المستودع. ارفع ملفات الواجهة و`data.js` الناتج فقط.
app.js CHANGED
@@ -1,50 +1,39 @@
1
  "use strict";
2
 
3
- const loginView = document.getElementById("loginView");
4
- const dashboardView = document.getElementById("dashboardView");
5
- const loginForm = document.getElementById("loginForm");
6
- const passwordInput = document.getElementById("password");
7
- const loginButton = document.getElementById("loginButton");
8
- const loginError = document.getElementById("loginError");
9
- const togglePassword = document.getElementById("togglePassword");
10
- const logoutButton = document.getElementById("logoutButton");
11
- const regionSelect = document.getElementById("regionSelect");
12
- const regionButtons = document.getElementById("regionButtons");
13
- const citySelect = document.getElementById("citySelect");
14
- const searchInput = document.getElementById("searchInput");
15
- const clearFiltersButton = document.getElementById("clearFiltersButton");
16
- const emptyState = document.getElementById("emptyState");
17
- const resultsSection = document.getElementById("resultsSection");
18
- const resultsMeta = document.getElementById("resultsMeta");
19
- const resultsTitle = document.getElementById("resultsTitle");
20
- const samplesGrid = document.getElementById("samplesGrid");
21
- const noResults = document.getElementById("noResults");
22
- const loadMoreWrap = document.getElementById("loadMoreWrap");
23
- const loadMoreButton = document.getElementById("loadMoreButton");
24
- const loadMoreMeta = document.getElementById("loadMoreMeta");
25
- const toast = document.getElementById("toast");
26
-
27
  const PAGE_SIZE = 24;
28
- const STATE_KEY = "icsInquiryFilters";
29
- const CONTACT_MESSAGE = (row) =>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  [
31
- "السلام عليكم ورحمة الله وبركاته",
32
- "",
33
- "أستاذي الكريم،",
34
- "نأمل تزويدنا بموقع المنشأة التالية، وذلك لاستكمال بيانات الزيارة الميدانية:",
35
- "",
36
- `منشأة: ${row.establishmentName || "-"}`,
37
- `السجل التجاري: ${row.commercialRecord || "-"}`,
38
- `المدينة: ${row.city || "-"}`,
39
- "",
40
- "شاكرين لكم تعاونكم.",
41
- ].join("\n");
42
-
43
- let rows = [];
44
  let visibleRows = [];
45
  let renderedCount = 0;
46
  let toastTimer;
47
- let isRestoringState = false;
48
 
49
  function base64ToBytes(value) {
50
  const binary = atob(value);
@@ -71,10 +60,15 @@ async function deriveKey(password, salt) {
71
  async function decryptData(password) {
72
  const salt = base64ToBytes(ENCRYPTED_DATA.salt);
73
  const iv = base64ToBytes(ENCRYPTED_DATA.iv);
74
- const cipherText = base64ToBytes(ENCRYPTED_DATA.payload);
 
 
 
 
 
75
  const key = await deriveKey(password, salt);
76
- const plainBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipherText);
77
- return JSON.parse(new TextDecoder().decode(plainBuffer));
78
  }
79
 
80
  function normalize(value) {
@@ -95,24 +89,68 @@ function uniqueSorted(values) {
95
  );
96
  }
97
 
98
- function setOptions(select, values, placeholder) {
99
- select.replaceChildren(new Option(placeholder, ""));
100
- values.forEach((value) => select.add(new Option(value, value)));
 
 
 
 
 
101
  }
102
 
103
- function saveFilterState() {
104
- if (isRestoringState) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  localStorage.setItem(
106
  STATE_KEY,
107
  JSON.stringify({
108
- region: regionSelect.value,
109
- city: citySelect.value,
110
- search: searchInput.value,
 
 
111
  }),
112
  );
113
  }
114
 
115
- function getSavedFilterState() {
116
  try {
117
  return JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
118
  } catch {
@@ -120,348 +158,426 @@ function getSavedFilterState() {
120
  }
121
  }
122
 
123
- function updateRegionButtonState() {
124
- regionButtons.querySelectorAll(".region-choice").forEach((button) => {
125
- const isActive = button.dataset.region === regionSelect.value;
126
- button.classList.toggle("active", isActive);
127
- button.setAttribute("aria-checked", String(isActive));
128
  });
129
  }
130
 
131
- function setRegion(region, scrollToControls = false) {
132
- regionSelect.value = region;
133
- updateRegionButtonState();
134
- const cities = uniqueSorted(rows.filter((row) => row.region === region).map((row) => row.city));
135
- setOptions(citySelect, cities, "اختر المدينة الصناعية");
136
- citySelect.disabled = !region;
137
- citySelect.value = "";
138
- applyFilters();
139
- if (scrollToControls) {
140
- window.scrollTo({ top: document.querySelector(".controls-panel").offsetTop - 16, behavior: "smooth" });
141
- }
142
- }
143
-
144
- function showToast(message) {
145
- clearTimeout(toastTimer);
146
- toast.textContent = message;
147
- toast.classList.add("show");
148
- toastTimer = setTimeout(() => toast.classList.remove("show"), 2200);
 
 
 
149
  }
150
 
151
- async function copyText(text, successMessage) {
152
- try {
153
- await navigator.clipboard.writeText(text);
154
- } catch {
155
- const area = document.createElement("textarea");
156
- area.value = text;
157
- area.style.position = "fixed";
158
- area.style.opacity = "0";
159
- document.body.append(area);
160
- area.select();
161
- document.execCommand("copy");
162
- area.remove();
163
- }
164
- showToast(successMessage);
165
  }
166
 
167
- function phoneDigits(value) {
168
- const match = String(value ?? "").match(/(?:\+?966|0)?5\d{8}/);
169
- if (!match) return "";
170
- let digits = match[0].replace(/\D/g, "");
171
- if (digits.startsWith("966")) return digits;
172
- if (digits.startsWith("0")) return `966${digits.slice(1)}`;
173
- return `966${digits}`;
 
 
 
174
  }
175
 
176
- function localPhoneNumber(value) {
177
- const digits = phoneDigits(value);
178
- if (!digits) return "";
179
- return digits.startsWith("966") ? `0${digits.slice(3)}` : digits;
180
  }
181
 
182
- function isCoordinate(value) {
183
- return /^[-+]?\d{1,2}(?:\.\d+)?\s*,\s*[-+]?\d{1,3}(?:\.\d+)?$/.test(String(value ?? "").trim());
 
 
 
 
 
 
184
  }
185
 
186
- function hasLocationInfo(row) {
187
- return Boolean(String(row.coordinates ?? "").trim());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
 
190
- function sortRowsByLocationInfo(items) {
191
- return [...items].sort((a, b) => Number(hasLocationInfo(b)) - Number(hasLocationInfo(a)));
 
192
  }
193
 
194
- function createSvgIcon(path) {
195
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
196
- svg.setAttribute("viewBox", "0 0 24 24");
197
- svg.setAttribute("aria-hidden", "true");
198
- const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
199
- p.setAttribute("d", path);
200
- svg.append(p);
201
- return svg;
202
  }
203
 
204
- function createActionLink(label, href, iconPath) {
205
  const link = document.createElement("a");
206
- link.className = "action-link";
207
  link.href = href;
208
  link.target = "_blank";
209
  link.rel = "noopener";
210
- link.append(createSvgIcon(iconPath), document.createTextNode(label));
211
  return link;
212
  }
213
 
214
- function createDetail(label, value, extraClass = "") {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  const item = document.createElement("div");
216
  item.className = `detail-item ${extraClass}`.trim();
217
- const title = document.createElement("span");
218
- title.className = "detail-label";
219
- title.textContent = label;
220
- const text = document.createElement("span");
221
- text.className = "detail-value";
222
- text.textContent = String(value);
223
- item.append(title, text);
224
  return item;
225
  }
226
 
227
- function createCoordinatesBlock(row) {
228
- const value = String(row.coordinates ?? "").trim();
229
- if (!value) return null;
230
-
231
- const item = document.createElement("div");
232
- const hasCoordinates = isCoordinate(value);
233
- item.className = `detail-item coordinates-detail ${hasCoordinates ? "map-detail" : "statement-detail"}`;
234
- const label = document.createElement("span");
235
- label.className = "detail-label";
236
- label.textContent = hasCoordinates ? "الإحداثيات" : "إفادة مدن";
237
- item.append(label);
238
-
239
- if (hasCoordinates) {
240
- item.append(
241
- createActionLink(
242
- "اضغط هنا للذهاب بخرائط Google",
243
- `https://www.google.com/maps?q=${encodeURIComponent(value)}`,
244
- "M12 21s7-4.6 7-11a7 7 0 1 0-14 0c0 6.4 7 11 7 11Z M12 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
 
245
  ),
246
  );
247
- return item;
248
  }
249
 
250
- const phone = phoneDigits(value);
251
- const note = document.createElement("p");
252
- note.className = "contact-note";
253
- note.textContent = value;
254
- item.append(note);
255
-
256
- if (phone) {
257
- const message = CONTACT_MESSAGE(row);
258
- const actions = document.createElement("div");
259
- actions.className = "contact-actions";
260
- const copyPhoneButton = document.createElement("button");
261
- copyPhoneButton.type = "button";
262
- copyPhoneButton.className = "action-link";
263
- copyPhoneButton.append(
264
- createSvgIcon("M8 8h11v11H8z M5 16H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"),
265
- document.createTextNode("نسخ الرقم"),
266
- );
267
- copyPhoneButton.addEventListener("click", () =>
268
- copyText(localPhoneNumber(value), "تم نسخ الرقم"),
269
- );
270
- actions.append(
271
- copyPhoneButton,
272
- createActionLink("واتساب", `https://wa.me/${phone}?text=${encodeURIComponent(message)}`, "M20.5 11.5a8.5 8.5 0 0 1-12.6 7.4L3 20l1.2-4.7A8.5 8.5 0 1 1 20.5 11.5Z M8.6 8.7c.2 3.3 2.7 5.8 6 6.2"),
273
- );
274
- const copyButton = document.createElement("button");
275
- copyButton.type = "button";
276
- copyButton.className = "action-link";
277
- copyButton.append(createSvgIcon("M8 8h11v11H8z M5 16H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"), document.createTextNode("نسخ رسالة مقترحة"));
278
- copyButton.addEventListener("click", () => copyText(message, "تم نسخ الرسالة المقترحة"));
279
- actions.append(copyButton);
280
- item.append(actions);
281
  }
282
 
283
- return item;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  }
285
 
286
  function createCard(row, index) {
287
  const card = document.createElement("article");
288
  card.className = "sample-card";
289
-
290
  const header = document.createElement("div");
291
- header.className = "sample-card-header";
292
- const heading = document.createElement("div");
293
- const indexEl = document.createElement("span");
294
- indexEl.className = "sample-index";
295
- indexEl.textContent = `منشأة ${index + 1}`;
296
  const title = document.createElement("h3");
297
  title.textContent = row.establishmentName || "منشأة دون اسم";
298
- heading.append(indexEl, title);
299
  const badge = document.createElement("span");
300
- badge.className = "status-badge";
301
- badge.textContent = row.complianceStatus || "غير محدد";
302
- header.append(heading, badge);
303
 
304
  const details = document.createElement("div");
305
- details.className = "sample-details";
306
  [
307
- ["السجل التجاري", row.commercialRecord, "ltr-value wide-detail"],
308
- ["توضيح المدينة", row.cityClarification, "wide-detail"],
 
 
309
  ].forEach(([label, value, className]) => {
310
- if (String(value ?? "").trim()) details.append(createDetail(label, value, className));
311
  });
312
- const coordinatesBlock = createCoordinatesBlock(row);
313
- if (coordinatesBlock) details.append(coordinatesBlock);
314
 
315
- card.append(header, details);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  return card;
317
  }
318
 
319
- function renderVisibleRows(reset = false) {
320
  if (reset) {
321
  renderedCount = 0;
322
- samplesGrid.replaceChildren();
323
  }
324
  const fragment = document.createDocumentFragment();
325
  visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE).forEach((row, offset) => {
326
  fragment.append(createCard(row, renderedCount + offset));
327
  });
328
- samplesGrid.append(fragment);
329
  renderedCount += Math.min(PAGE_SIZE, visibleRows.length - renderedCount);
330
  const remaining = visibleRows.length - renderedCount;
331
- loadMoreWrap.hidden = remaining <= 0;
332
- loadMoreButton.textContent = `عرض المزيد (${Math.min(PAGE_SIZE, remaining)})`;
333
- loadMoreMeta.textContent = `تم عرض ${renderedCount} من أصل ${visibleRows.length} منشأة`;
334
  }
335
 
336
  function applyFilters() {
337
- const region = regionSelect.value;
338
- const city = citySelect.value;
339
- const query = normalize(searchInput.value);
340
-
341
- const scopedRows = rows.filter((row) => {
342
- const matchesRegion = !region || row.region === region;
343
- const matchesCity = !city || row.city === city;
344
- return matchesRegion && matchesCity;
 
 
 
 
 
 
 
 
345
  });
 
 
 
 
 
 
346
 
347
- visibleRows = sortRowsByLocationInfo(scopedRows.filter((row) => {
348
- if (!query) return Boolean(city || region);
349
- return (
350
- normalize(row.establishmentName).includes(query) ||
351
- normalize(row.commercialRecord).includes(query) ||
352
- normalize(row.cityClarification).includes(query)
353
- );
354
- }));
355
-
356
- const shouldShow = visibleRows.length > 0 || city || region || query;
357
- emptyState.hidden = shouldShow;
358
- resultsSection.hidden = !shouldShow;
359
- noResults.hidden = visibleRows.length !== 0;
360
- resultsTitle.textContent = city || region || "نتائج البحث";
361
- resultsMeta.textContent = `${visibleRows.length} من أصل ${scopedRows.length} منشأة`;
362
- renderVisibleRows(true);
363
- saveFilterState();
364
- }
365
-
366
- function initializeDashboard(data) {
367
- rows = data.rows || [];
368
- const regions = data.regions || uniqueSorted(rows.map((row) => row.region));
369
- setOptions(regionSelect, regions, "اختر المنطقة");
370
- const regionCounts = new Map();
371
- rows.forEach((row) => regionCounts.set(row.region, (regionCounts.get(row.region) || 0) + 1));
372
- regionButtons.replaceChildren(
373
- ...regions.map((region) => {
374
- const button = document.createElement("button");
375
- button.type = "button";
376
- button.className = "region-choice";
377
- button.dataset.region = region;
378
- button.setAttribute("role", "radio");
379
- button.setAttribute("aria-checked", "false");
380
- const name = document.createElement("span");
381
- name.textContent = region;
382
- const count = document.createElement("strong");
383
- count.textContent = regionCounts.get(region) || 0;
384
- button.append(name, count);
385
- button.addEventListener("click", () => setRegion(region));
386
- return button;
387
- }),
388
- );
389
- setOptions(citySelect, [], "اختر المدينة الصناعية");
390
- citySelect.disabled = true;
391
- restoreFilterState();
392
- }
393
-
394
- function restoreFilterState() {
395
- const saved = getSavedFilterState();
396
- isRestoringState = true;
397
- searchInput.value = saved.search || "";
398
- if (saved.region && [...regionSelect.options].some((option) => option.value === saved.region)) {
399
- setRegion(saved.region);
400
- if (saved.city && [...citySelect.options].some((option) => option.value === saved.city)) {
401
- citySelect.value = saved.city;
402
- }
403
- } else {
404
- setRegion("");
405
- }
406
- isRestoringState = false;
407
- applyFilters();
408
  }
409
 
410
- function performLogout() {
411
- rows = [];
412
- visibleRows = [];
413
- searchInput.value = "";
414
- setRegion("");
415
- citySelect.value = "";
416
- dashboardView.hidden = true;
417
- loginView.hidden = false;
418
- passwordInput.focus();
 
 
419
  }
420
 
421
- loginForm.addEventListener("submit", async (event) => {
422
  event.preventDefault();
423
- const password = passwordInput.value.trim();
424
- if (!password) return;
425
- loginButton.disabled = true;
426
- loginButton.querySelector("span").textContent = "جاري التحقق...";
427
- loginError.textContent = "";
428
-
429
  try {
430
- const data = await decryptData(password);
431
- initializeDashboard(data);
432
- passwordInput.value = "";
433
- loginView.hidden = true;
434
- dashboardView.hidden = false;
435
  } catch {
436
- loginError.textContent = "رمز الدخول غير صحيح.";
437
- passwordInput.select();
438
  } finally {
439
- loginButton.disabled = false;
440
- loginButton.querySelector("span").textContent = "دخول";
441
  }
442
  });
443
 
444
- togglePassword.addEventListener("click", () => {
445
- const showing = passwordInput.type === "text";
446
- passwordInput.type = showing ? "password" : "text";
447
- togglePassword.setAttribute("aria-label", showing ? "إظهار رمز الدخول" : "إخفاء رمز الدخول");
448
  });
449
-
450
- logoutButton.addEventListener("click", performLogout);
451
-
452
- regionSelect.addEventListener("change", () => setRegion(regionSelect.value));
453
-
454
- citySelect.addEventListener("change", applyFilters);
455
- searchInput.addEventListener("input", applyFilters);
456
- loadMoreButton.addEventListener("click", () => renderVisibleRows());
457
- clearFiltersButton.addEventListener("click", () => {
458
- searchInput.value = "";
459
- localStorage.removeItem(STATE_KEY);
460
- setRegion("");
461
- showToast("تم مسح الاختيارات");
462
  });
463
-
464
- if (!window.crypto?.subtle || typeof ENCRYPTED_DATA === "undefined") {
465
- loginButton.disabled = true;
466
- loginError.textContent = "تعذر تشغيل التشفير في هذا المتصفح. استخدم متصفحًا حديثًا.";
467
- }
 
 
 
 
 
 
 
 
 
 
1
  "use strict";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const PAGE_SIZE = 24;
4
+ const STATE_KEY = "ics2ResearcherWorkspace";
5
+ const FORM_URL = "https://drive.google.com/file/d/1BEJ3qrqB3RMvhw4Z0i4dxZiwEreT9cJc/view?usp=sharing";
6
+
7
+ const REPRESENTATIVES = [
8
+ { match: ["الصناعية الأولى", "الصناعية الاولى"], city: "الدمام الأولى", name: "يوسف العباد", phone: "0501444214" },
9
+ { match: ["الصناعية الثانية"], city: "الدمام الثانية", name: "سالم بوسوده", phone: "0560503707" },
10
+ { match: ["الصناعية الثالثة"], city: "الدمام الثالثة", name: "إبراهيم الغامدي", phone: "0555309388" },
11
+ { match: ["الأحساء", "الاحساء", "العيون", "واحة مدن"], city: "الأحساء", name: "أحمد الخليفة", phone: "0506448053" },
12
+ ];
13
+
14
+ const ICONS = {
15
+ copy: "M8 8h11v11H8z M5 16H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1",
16
+ map: "M12 21s7-4.6 7-11a7 7 0 1 0-14 0c0 6.4 7 11 7 11Z M12 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
17
+ whatsapp: "M20.5 11.5a8.5 8.5 0 0 1-12.6 7.4L3 20l1.2-4.7A8.5 8.5 0 1 1 20.5 11.5Z M8.6 8.7c.2 3.3 2.7 5.8 6 6.2",
18
+ };
19
+
20
+ const elements = Object.fromEntries(
21
  [
22
+ "loginView", "loginForm", "password", "togglePassword", "loginButton", "loginError",
23
+ "researcherView", "researcherSearch", "researcherGrid", "dashboardView", "researcherName",
24
+ "switchResearcherButton", "logoutButton", "summaryStats", "researcherMapLink", "formLink",
25
+ "shareFormButton", "cityButtons", "searchInput", "statusSelect", "locationSelect",
26
+ "clearFiltersButton", "representativePanel", "resultsMeta", "resultsTitle", "samplesGrid",
27
+ "noResults", "loadMoreWrap", "loadMoreButton", "loadMoreMeta", "toast",
28
+ ].map((id) => [id, document.getElementById(id)]),
29
+ );
30
+
31
+ let payload = null;
32
+ let activeResearcher = null;
33
+ let researcherRows = [];
 
34
  let visibleRows = [];
35
  let renderedCount = 0;
36
  let toastTimer;
 
37
 
38
  function base64ToBytes(value) {
39
  const binary = atob(value);
 
60
  async function decryptData(password) {
61
  const salt = base64ToBytes(ENCRYPTED_DATA.salt);
62
  const iv = base64ToBytes(ENCRYPTED_DATA.iv);
63
+ const combined = base64ToBytes(ENCRYPTED_DATA.payload);
64
+ const cipherText = combined.slice(0, -16);
65
+ const authTag = combined.slice(-16);
66
+ const encrypted = new Uint8Array(cipherText.length + authTag.length);
67
+ encrypted.set(cipherText);
68
+ encrypted.set(authTag, cipherText.length);
69
  const key = await deriveKey(password, salt);
70
+ const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encrypted);
71
+ return JSON.parse(new TextDecoder().decode(plain));
72
  }
73
 
74
  function normalize(value) {
 
89
  );
90
  }
91
 
92
+ function createIcon(path) {
93
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
94
+ svg.setAttribute("viewBox", "0 0 24 24");
95
+ svg.setAttribute("aria-hidden", "true");
96
+ const element = document.createElementNS("http://www.w3.org/2000/svg", "path");
97
+ element.setAttribute("d", path);
98
+ svg.append(element);
99
+ return svg;
100
  }
101
 
102
+ function showToast(message) {
103
+ clearTimeout(toastTimer);
104
+ elements.toast.textContent = message;
105
+ elements.toast.classList.add("show");
106
+ toastTimer = setTimeout(() => elements.toast.classList.remove("show"), 2300);
107
+ }
108
+
109
+ async function copyText(text, successMessage) {
110
+ try {
111
+ await navigator.clipboard.writeText(text);
112
+ } catch {
113
+ const area = document.createElement("textarea");
114
+ area.value = text;
115
+ area.style.cssText = "position:fixed;opacity:0";
116
+ document.body.append(area);
117
+ area.select();
118
+ document.execCommand("copy");
119
+ area.remove();
120
+ }
121
+ showToast(successMessage);
122
+ }
123
+
124
+ function localPhone(value) {
125
+ const digits = String(value ?? "").replace(/\D/g, "");
126
+ if (digits.startsWith("966")) return `0${digits.slice(3)}`;
127
+ return digits;
128
+ }
129
+
130
+ function internationalPhone(value) {
131
+ const digits = localPhone(value);
132
+ return digits.startsWith("0") ? `966${digits.slice(1)}` : digits;
133
+ }
134
+
135
+ function locationRank(row) {
136
+ return { madon: 4, base: 3, statement: 2, none: 1 }[row.locationType] || 0;
137
+ }
138
+
139
+ function saveState() {
140
+ if (!activeResearcher) return;
141
  localStorage.setItem(
142
  STATE_KEY,
143
  JSON.stringify({
144
+ researcher: activeResearcher.name,
145
+ city: elements.cityButtons.querySelector(".city-choice.active")?.dataset.city || "",
146
+ search: elements.searchInput.value,
147
+ status: elements.statusSelect.value,
148
+ location: elements.locationSelect.value,
149
  }),
150
  );
151
  }
152
 
153
+ function savedState() {
154
  try {
155
  return JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
156
  } catch {
 
158
  }
159
  }
160
 
161
+ function showOnly(view) {
162
+ [elements.loginView, elements.researcherView, elements.dashboardView].forEach((item) => {
163
+ item.hidden = item !== view;
 
 
164
  });
165
  }
166
 
167
+ function renderResearchers() {
168
+ const query = normalize(elements.researcherSearch.value);
169
+ const fragment = document.createDocumentFragment();
170
+ payload.researchers
171
+ .filter((researcher) => !query || normalize(researcher.name).includes(query))
172
+ .forEach((researcher) => {
173
+ const button = document.createElement("button");
174
+ button.type = "button";
175
+ button.className = "researcher-choice";
176
+ const content = document.createElement("span");
177
+ const name = document.createElement("strong");
178
+ name.textContent = researcher.name;
179
+ const meta = document.createElement("small");
180
+ meta.textContent = `${researcher.count} عينة`;
181
+ content.append(name, meta);
182
+ const arrow = createIcon("m15 18-6-6 6-6");
183
+ button.append(content, arrow);
184
+ button.addEventListener("click", () => openResearcher(researcher));
185
+ fragment.append(button);
186
+ });
187
+ elements.researcherGrid.replaceChildren(fragment);
188
  }
189
 
190
+ function createStat(value, label, className) {
191
+ const item = document.createElement("div");
192
+ item.className = `stat-item ${className}`;
193
+ const strong = document.createElement("strong");
194
+ strong.textContent = value;
195
+ const span = document.createElement("span");
196
+ span.textContent = label;
197
+ item.append(strong, span);
198
+ return item;
 
 
 
 
 
199
  }
200
 
201
+ function renderSummary() {
202
+ const withMadon = researcherRows.filter((row) => row.locationType === "madon").length;
203
+ const withBase = researcherRows.filter((row) => row.locationType === "base").length;
204
+ const withoutLocation = researcherRows.filter((row) => row.locationType === "none").length;
205
+ elements.summaryStats.replaceChildren(
206
+ createStat(researcherRows.length, "إجمالي العينات", "total"),
207
+ createStat(withMadon, "إحداثية مدن", "madon"),
208
+ createStat(withBase, "إحداثية أساسية", "base"),
209
+ createStat(withoutLocation, "دون تفاصيل موقع", "missing"),
210
+ );
211
  }
212
 
213
+ function activeCity() {
214
+ return elements.cityButtons.querySelector(".city-choice.active")?.dataset.city || "";
 
 
215
  }
216
 
217
+ function setActiveCity(city) {
218
+ elements.cityButtons.querySelectorAll(".city-choice").forEach((button) => {
219
+ const active = button.dataset.city === city;
220
+ button.classList.toggle("active", active);
221
+ button.setAttribute("aria-checked", String(active));
222
+ });
223
+ updateRepresentative(city);
224
+ applyFilters();
225
  }
226
 
227
+ function renderCities(preferredCity = "") {
228
+ const counts = new Map();
229
+ researcherRows.forEach((row) => counts.set(row.city, (counts.get(row.city) || 0) + 1));
230
+ const cities = uniqueSorted([...counts.keys()]);
231
+ const allButton = document.createElement("button");
232
+ allButton.type = "button";
233
+ allButton.className = "city-choice";
234
+ allButton.dataset.city = "";
235
+ allButton.innerHTML = `<span>جميع المدن</span><strong>${researcherRows.length}</strong>`;
236
+ allButton.addEventListener("click", () => setActiveCity(""));
237
+ const buttons = [allButton];
238
+ cities.forEach((city) => {
239
+ const button = document.createElement("button");
240
+ button.type = "button";
241
+ button.className = "city-choice";
242
+ button.dataset.city = city;
243
+ const label = document.createElement("span");
244
+ label.textContent = city;
245
+ const count = document.createElement("strong");
246
+ count.textContent = counts.get(city);
247
+ button.append(label, count);
248
+ button.addEventListener("click", () => setActiveCity(city));
249
+ buttons.push(button);
250
+ });
251
+ elements.cityButtons.replaceChildren(...buttons);
252
+ const initialCity = cities.includes(preferredCity) ? preferredCity : "";
253
+ setActiveCity(initialCity);
254
  }
255
 
256
+ function findRepresentative(city) {
257
+ const normalizedCity = normalize(city);
258
+ return REPRESENTATIVES.find((item) => item.match.some((term) => normalizedCity.includes(normalize(term))));
259
  }
260
 
261
+ function actionButton(label, icon, onClick) {
262
+ const button = document.createElement("button");
263
+ button.type = "button";
264
+ button.className = "mini-action";
265
+ button.append(createIcon(icon), document.createTextNode(label));
266
+ button.addEventListener("click", onClick);
267
+ return button;
 
268
  }
269
 
270
+ function actionLink(label, href, icon) {
271
  const link = document.createElement("a");
272
+ link.className = "mini-action";
273
  link.href = href;
274
  link.target = "_blank";
275
  link.rel = "noopener";
276
+ link.append(createIcon(icon), document.createTextNode(label));
277
  return link;
278
  }
279
 
280
+ function representativeMessage(representative, row = null) {
281
+ const lines = [
282
+ "السلام عليكم ورحمة الله وبركاته",
283
+ "",
284
+ `الأستاذ ${representative.name} المحترم،`,
285
+ "نأمل تزويدنا بموقع المنشأة التالية، وذلك لاستكمال بيانات الزيارة الميدانية:",
286
+ ];
287
+ if (row) {
288
+ lines.push(
289
+ "",
290
+ `المنشأة: ${row.establishmentName || "-"}`,
291
+ `السجل التجاري: ${row.commercialRecord || "-"}`,
292
+ `المدينة: ${row.city || "-"}`,
293
+ );
294
+ }
295
+ lines.push("", "شاكرين لكم تعاونكم.");
296
+ return lines.join("\n");
297
+ }
298
+
299
+ function updateRepresentative(city) {
300
+ const cityRow = researcherRows.find((row) => row.city === city);
301
+ const representative = findRepresentative(cityRow?.representativeCity || city);
302
+ elements.representativePanel.hidden = !representative;
303
+ elements.representativePanel.replaceChildren();
304
+ if (!representative) return;
305
+
306
+ const copy = document.createElement("div");
307
+ copy.className = "representative-copy";
308
+ const icon = document.createElement("span");
309
+ icon.className = "representative-icon";
310
+ icon.append(createIcon("M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8M19 8v6M22 11h-6"));
311
+ const text = document.createElement("div");
312
+ const label = document.createElement("small");
313
+ label.textContent = `ممثل ${representative.city}`;
314
+ const name = document.createElement("strong");
315
+ name.textContent = representative.name;
316
+ const phone = document.createElement("span");
317
+ phone.textContent = representative.phone;
318
+ text.append(label, name, phone);
319
+ copy.append(icon, text);
320
+
321
+ const actions = document.createElement("div");
322
+ actions.className = "representative-actions";
323
+ actions.append(
324
+ actionButton("نسخ الرقم", ICONS.copy, () => copyText(representative.phone, "تم نسخ رقم الممثل")),
325
+ actionLink(
326
+ "واتساب",
327
+ `https://wa.me/${internationalPhone(representative.phone)}?text=${encodeURIComponent(representativeMessage(representative))}`,
328
+ ICONS.whatsapp,
329
+ ),
330
+ );
331
+ elements.representativePanel.append(copy, actions);
332
+ }
333
+
334
+ function setSelectOptions(select, values, placeholder) {
335
+ select.replaceChildren(new Option(placeholder, ""));
336
+ values.forEach((value) => select.add(new Option(value, value)));
337
+ }
338
+
339
+ function openResearcher(researcher) {
340
+ activeResearcher = researcher;
341
+ researcherRows = payload.rows
342
+ .filter((row) => row.researcher === researcher.name)
343
+ .sort((a, b) => locationRank(b) - locationRank(a));
344
+ elements.researcherName.textContent = researcher.name;
345
+ elements.researcherMapLink.href = researcher.mapUrl;
346
+ elements.formLink.href = FORM_URL;
347
+ elements.searchInput.value = "";
348
+ elements.locationSelect.value = "";
349
+ setSelectOptions(elements.statusSelect, uniqueSorted(researcherRows.map((row) => row.status)), "جميع الحالات");
350
+ renderSummary();
351
+ const state = savedState();
352
+ if (state.researcher === researcher.name) {
353
+ elements.searchInput.value = state.search || "";
354
+ elements.statusSelect.value = state.status || "";
355
+ elements.locationSelect.value = state.location || "";
356
+ }
357
+ renderCities(state.researcher === researcher.name ? state.city : "");
358
+ showOnly(elements.dashboardView);
359
+ window.scrollTo({ top: 0 });
360
+ saveState();
361
+ }
362
+
363
+ function detailItem(label, value, extraClass = "") {
364
  const item = document.createElement("div");
365
  item.className = `detail-item ${extraClass}`.trim();
366
+ const name = document.createElement("span");
367
+ name.className = "detail-label";
368
+ name.textContent = label;
369
+ const content = document.createElement("span");
370
+ content.className = "detail-value";
371
+ content.textContent = String(value);
372
+ item.append(name, content);
373
  return item;
374
  }
375
 
376
+ function createLocationBlock(row) {
377
+ if (row.locationType === "none" && !row.madonStatement && !row.madonNote) return null;
378
+ const block = document.createElement("div");
379
+ block.className = `location-block location-${row.locationType}`;
380
+
381
+ if (row.coordinates) {
382
+ const heading = document.createElement("div");
383
+ heading.className = "location-heading";
384
+ const label = document.createElement("span");
385
+ label.textContent = row.locationType === "madon" ? "إحداثية مدن" : "الإحداثية الأساسية";
386
+ const source = document.createElement("small");
387
+ source.textContent = row.locationType === "madon" ? "الموقع المحدّث" : "حسب بيانات X وY";
388
+ heading.append(label, source);
389
+ block.append(heading);
390
+ block.append(
391
+ actionLink(
392
+ "فتح الموقع في خرائط Google",
393
+ `https://www.google.com/maps?q=${encodeURIComponent(row.coordinates)}`,
394
+ ICONS.map,
395
  ),
396
  );
 
397
  }
398
 
399
+ if (row.madonStatement || row.madonNoteText) {
400
+ const statement = document.createElement("div");
401
+ statement.className = "madon-statement";
402
+ const title = document.createElement("span");
403
+ title.textContent = "إفادة مدن";
404
+ const text = document.createElement("p");
405
+ text.textContent = [row.madonStatement, row.madonNoteText].filter(Boolean).join(" - ");
406
+ statement.append(title, text);
407
+ block.append(statement);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  }
409
 
410
+ return block;
411
+ }
412
+
413
+ function sampleMessage(row) {
414
+ return [
415
+ "السلام عليكم ورحمة الله وبركاته",
416
+ "",
417
+ "أستاذي الكريم،",
418
+ "نأمل تزويدنا بموقع المنشأة التالية، وذلك لاستكمال بيانات الزيارة الميدانية:",
419
+ "",
420
+ `المنشأة: ${row.establishmentName || "-"}`,
421
+ `السجل التجاري: ${row.commercialRecord || "-"}`,
422
+ `المدينة: ${row.city || "-"}`,
423
+ "",
424
+ "شاكرين لكم تعاونكم.",
425
+ ].join("\n");
426
  }
427
 
428
  function createCard(row, index) {
429
  const card = document.createElement("article");
430
  card.className = "sample-card";
 
431
  const header = document.createElement("div");
432
+ header.className = "card-header";
433
+ const titleWrap = document.createElement("div");
434
+ const indexLabel = document.createElement("span");
435
+ indexLabel.className = "sample-index";
436
+ indexLabel.textContent = `عينة ${index + 1}`;
437
  const title = document.createElement("h3");
438
  title.textContent = row.establishmentName || "منشأة دون اسم";
439
+ titleWrap.append(indexLabel, title);
440
  const badge = document.createElement("span");
441
+ badge.className = `status-badge status-${normalize(row.status).includes("مغلق") ? "closed" : "default"}`;
442
+ badge.textContent = row.status || "غير محدد";
443
+ header.append(titleWrap, badge);
444
 
445
  const details = document.createElement("div");
446
+ details.className = "card-details";
447
  [
448
+ ["السجل التجاري", row.commercialRecord, "ltr-value"],
449
+ ["الرقم الموحد", row.unifiedNumber, "ltr-value"],
450
+ ["المدينة الصناعية", row.city, "wide"],
451
+ ["النشاط", row.activity, "wide"],
452
  ].forEach(([label, value, className]) => {
453
+ if (String(value ?? "").trim()) details.append(detailItem(label, value, className));
454
  });
455
+ const location = createLocationBlock(row);
456
+ if (location) details.append(location);
457
 
458
+ const footer = document.createElement("div");
459
+ footer.className = "card-actions";
460
+ footer.append(
461
+ actionButton("نسخ بيانات العينة", ICONS.copy, () => copyText(sampleMessage(row), "تم نسخ بيانات العينة")),
462
+ );
463
+ const representative = findRepresentative(row.representativeCity || row.city);
464
+ if (representative && !row.coordinates) {
465
+ const message = representativeMessage(representative, row);
466
+ footer.append(
467
+ actionLink(
468
+ `مراسلة ${representative.name.split(" ")[0]}`,
469
+ `https://wa.me/${internationalPhone(representative.phone)}?text=${encodeURIComponent(message)}`,
470
+ ICONS.whatsapp,
471
+ ),
472
+ );
473
+ }
474
+ card.append(header, details, footer);
475
  return card;
476
  }
477
 
478
+ function renderRows(reset = false) {
479
  if (reset) {
480
  renderedCount = 0;
481
+ elements.samplesGrid.replaceChildren();
482
  }
483
  const fragment = document.createDocumentFragment();
484
  visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE).forEach((row, offset) => {
485
  fragment.append(createCard(row, renderedCount + offset));
486
  });
487
+ elements.samplesGrid.append(fragment);
488
  renderedCount += Math.min(PAGE_SIZE, visibleRows.length - renderedCount);
489
  const remaining = visibleRows.length - renderedCount;
490
+ elements.loadMoreWrap.hidden = remaining <= 0;
491
+ elements.loadMoreMeta.textContent = remaining > 0 ? `متبقي ${remaining} عينة` : "";
 
492
  }
493
 
494
  function applyFilters() {
495
+ if (!activeResearcher) return;
496
+ const city = activeCity();
497
+ const query = normalize(elements.searchInput.value);
498
+ const status = elements.statusSelect.value;
499
+ const location = elements.locationSelect.value;
500
+ visibleRows = researcherRows.filter((row) => {
501
+ if (city && row.city !== city) return false;
502
+ if (status && row.status !== status) return false;
503
+ if (location && row.locationType !== location) return false;
504
+ if (query) {
505
+ const haystack = normalize(
506
+ [row.establishmentName, row.commercialRecord, row.unifiedNumber, row.activity].join(" "),
507
+ );
508
+ if (!haystack.includes(query)) return false;
509
+ }
510
+ return true;
511
  });
512
+ elements.resultsTitle.textContent = city || "جميع العينات";
513
+ elements.resultsMeta.textContent = `${visibleRows.length} من أصل ${researcherRows.length} عينة`;
514
+ elements.noResults.hidden = visibleRows.length > 0;
515
+ renderRows(true);
516
+ saveState();
517
+ }
518
 
519
+ function clearFilters() {
520
+ elements.searchInput.value = "";
521
+ elements.statusSelect.value = "";
522
+ elements.locationSelect.value = "";
523
+ setActiveCity("");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  }
525
 
526
+ function shareForm() {
527
+ const message = [
528
+ "السلام عليكم ورحمة الله وبركاته",
529
+ "",
530
+ "نرفق لكم استمارة المسح المطلوبة، ونأمل التكرم بتعبئتها.",
531
+ "",
532
+ FORM_URL,
533
+ "",
534
+ "شاكرين لكم تعاونكم.",
535
+ ].join("\n");
536
+ window.open(`https://wa.me/?text=${encodeURIComponent(message)}`, "_blank", "noopener");
537
  }
538
 
539
+ elements.loginForm.addEventListener("submit", async (event) => {
540
  event.preventDefault();
541
+ elements.loginError.textContent = "";
542
+ elements.loginButton.disabled = true;
543
+ elements.loginButton.querySelector("span").textContent = "جارٍ الدخول...";
 
 
 
544
  try {
545
+ payload = await decryptData(elements.password.value);
546
+ elements.password.value = "";
547
+ elements.researcherSearch.value = "";
548
+ renderResearchers();
549
+ showOnly(elements.researcherView);
550
  } catch {
551
+ elements.loginError.textContent = "رمز الدخول غير صحيح.";
 
552
  } finally {
553
+ elements.loginButton.disabled = false;
554
+ elements.loginButton.querySelector("span").textContent = "دخول";
555
  }
556
  });
557
 
558
+ elements.togglePassword.addEventListener("click", () => {
559
+ const visible = elements.password.type === "text";
560
+ elements.password.type = visible ? "password" : "text";
561
+ elements.togglePassword.setAttribute("aria-label", visible ? "إظهار رمز الدخول" : "إخفاء رمز الدخول");
562
  });
563
+ elements.researcherSearch.addEventListener("input", renderResearchers);
564
+ elements.switchResearcherButton.addEventListener("click", () => {
565
+ activeResearcher = null;
566
+ renderResearchers();
567
+ showOnly(elements.researcherView);
568
+ window.scrollTo({ top: 0 });
 
 
 
 
 
 
 
569
  });
570
+ elements.logoutButton.addEventListener("click", () => {
571
+ payload = null;
572
+ activeResearcher = null;
573
+ researcherRows = [];
574
+ visibleRows = [];
575
+ showOnly(elements.loginView);
576
+ elements.password.focus();
577
+ });
578
+ elements.searchInput.addEventListener("input", applyFilters);
579
+ elements.statusSelect.addEventListener("change", applyFilters);
580
+ elements.locationSelect.addEventListener("change", applyFilters);
581
+ elements.clearFiltersButton.addEventListener("click", clearFilters);
582
+ elements.loadMoreButton.addEventListener("click", () => renderRows(false));
583
+ elements.shareFormButton.addEventListener("click", shareForm);
data.js CHANGED
The diff for this file is too large to render. See raw diff
 
generate-data.mjs CHANGED
@@ -4,72 +4,148 @@ import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
4
 
5
  const [inputPath, outputPath, password = "20302030"] = process.argv.slice(2);
6
 
7
- const CITY_INFO = new Map([
8
- ["المدينة الصناعية الأولى بالدمام", { region: "منطقة الدمام", city: "الصناعية الأولى بالدمام" }],
9
- ["المدينة صناعية الثانية بالدمام", { region: "منطقة الدمام", city: "الصناعية الثانية بالدمام" }],
10
- ["المدينة صناعية الثالثة بالدمام", { region: "منطقة الدمام", city: "الصناعية الثالثة بالدمام" }],
11
- ["المدينة صناعية الأولى بالأحساء", { region: "منطقة الأحساء", city: "المدينة صناعية الأولى بالأحساء" }],
12
- ["المدينة الصناعية بحفر الباطن", { region: "منطقة حفر الباطن", city: "المدينة الصناعية بحفر الباطن" }],
13
- ['مدينة الملك سلمان بارك"', { region: "سبارك", city: "مدينة الملك سلمان" }],
14
- ]);
15
-
16
- const REGIONS = ["منطقة الدمام", "منطقة الأحساء", "منطقة حفر الباطن", "سبارك"];
17
-
18
- function normalizeHeader(value) {
19
- return String(value ?? "")
20
- .replace(/\s+/g, " ")
21
- .trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
- function getByHeader(row, headers, candidates) {
25
- for (const candidate of candidates) {
26
- const index = headers.findIndex((header) => normalizeHeader(header) === candidate);
27
- if (index !== -1) return row[index] ?? "";
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  return "";
30
  }
31
 
32
- function clean(value) {
33
- if (value === null || value === undefined) return "";
34
- return String(value).trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  if (!inputPath || !outputPath) {
38
  console.error("Usage: node generate-data.mjs <input.xlsx> <output.js> [password]");
39
  process.exitCode = 1;
40
  } else {
41
- const blob = await FileBlob.load(inputPath);
42
- const workbook = await SpreadsheetFile.importXlsx(blob);
 
43
  const rows = [];
44
 
45
- for (const sheet of workbook.worksheets.items) {
46
- const info = CITY_INFO.get(sheet.name);
47
- if (!info) continue;
48
- const { region, city } = info;
49
-
50
- const values = sheet.getUsedRange(true).values;
51
- if (!values?.length) continue;
52
- const headers = values[0].map(normalizeHeader);
53
-
54
- for (const sourceRow of values.slice(1)) {
55
- const establishmentName = clean(getByHeader(sourceRow, headers, ["إسم المنشأة", "اسم المنشأة"]));
56
- if (!establishmentName) continue;
57
- rows.push({
58
- region,
59
- city,
60
- establishmentName,
61
- commercialRecord: clean(getByHeader(sourceRow, headers, ["السجل التجاري من الاطار", "السجل التجاري"])),
62
- complianceStatus: clean(getByHeader(sourceRow, headers, ["حالة الاستيفاء"])),
63
- cityClarification: clean(getByHeader(sourceRow, headers, ["توضيح المدينة", "توضيح المدينة الصناعية"])),
64
- coordinates: clean(getByHeader(sourceRow, headers, ["الاحداثيات", "الإحداثية", "الإحداثيات"])),
65
- });
66
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
 
69
- const payload = JSON.stringify({
70
- version: 2,
 
 
 
 
 
 
 
 
 
71
  generatedAt: new Date().toISOString(),
72
- regions: REGIONS,
73
  rows,
74
  });
75
 
@@ -78,12 +154,10 @@ if (!inputPath || !outputPath) {
78
  const iv = crypto.randomBytes(12);
79
  const key = crypto.pbkdf2Sync(password, salt, iterations, 32, "sha256");
80
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
81
- const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
82
- const authTag = cipher.getAuthTag();
83
- const combined = Buffer.concat([encrypted, authTag]);
84
-
85
  const output = [
86
- "/* Generated encrypted data. Replace this file using generate-data.mjs when the workbook changes. */",
87
  "const ENCRYPTED_DATA = Object.freeze({",
88
  ` iterations: ${iterations},`,
89
  ` salt: "${salt.toString("base64")}",`,
@@ -94,5 +168,12 @@ if (!inputPath || !outputPath) {
94
  ].join("\n");
95
 
96
  await fs.writeFile(outputPath, output, "utf8");
97
- console.log(JSON.stringify({ rows: rows.length, regions: REGIONS.length, outputPath }));
 
 
 
 
 
 
 
98
  }
 
4
 
5
  const [inputPath, outputPath, password = "20302030"] = process.argv.slice(2);
6
 
7
+ const MAP_URLS = [
8
+ ["أماني مصطفى عبدالله الطيب", "https://stat2025-map.static.hf.space/Rahn/02.html"],
9
+ ["اسعد بن ماجد بن", "https://stat2025-map.static.hf.space/Rahn/03.html"],
10
+ ["اسماعيل خليفه السماعيل", "https://stat2025-map.static.hf.space/Rahn/04.html"],
11
+ ["ريم بنت محمد بن عبدالعزيز الملحم", "https://stat2025-map.static.hf.space/Rahn/05.html"],
12
+ ["زكي بن عيسى بن", "https://stat2025-map.static.hf.space/Rahn/06.html"],
13
+ [اره حسين بن عبدالهادي بوخمسين", "https://stat2025-map.static.hf.space/Rahn/07.html"],
14
+ ["ساره خالد سليمان المحيسن", "https://stat2025-map.static.hf.space/Rahn/08.html"],
15
+ ["طيبه فالح بن عبدالله الرويشد", "https://stat2025-map.static.hf.space/Rahn/09.html"],
16
+ ["عبدالعزيز فهد عبدالعزيز العبلان", "https://stat2025-map.static.hf.space/Rahn/10.html"],
17
+ ["علي جابر بن علي", "https://stat2025-map.static.hf.space/Rahn/11.html"],
18
+ ["عماد بن عيسى بن", "https://stat2025-map.static.hf.space/Rahn/12.html"],
19
+ ["عمران فخري بن عمران", "https://stat2025-map.static.hf.space/Rahn/13.html"],
20
+ ["فهد علي بن عبدالخالق الغامدي", "https://stat2025-map.static.hf.space/Rahn/14.html"],
21
+ ["فوز عائد نومان المطيري", "https://stat2025-map.static.hf.space/Rahn/15.html"],
22
+ ["قنوت محمد بن عبدالله آل حماد", "https://stat2025-map.static.hf.space/Rahn/16.html"],
23
+ ["لولوه بدر بن حمد الصياح", "https://stat2025-map.static.hf.space/Rahn/17.html"],
24
+ ["لين أحمد بن عبدالعزيز القصير", "https://stat2025-map.static.hf.space/Rahn/18.html"],
25
+ ["ماجد سعد ناصر السبيعي", "https://stat2025-map.static.hf.space/Rahn/19.html"],
26
+ ["مرتضى عبدالجليل بن عيسى", "https://stat2025-map.static.hf.space/Rahn/20.html"],
27
+ ["نبأ عادل بن عبدالكريم", "https://stat2025-map.static.hf.space/Rahn/21.html"],
28
+ ["نوف سعود بن سالم الخثعمي", "https://stat2025-map.static.hf.space/Rahn/22.html"],
29
+ ["نيللي حسين عبدالله الجعص", "https://stat2025-map.static.hf.space/Rahn/23.html"],
30
+ ["هبه عبدالعزيز", "https://stat2025-map.static.hf.space/Rahn/24.html"],
31
+ ];
32
+
33
+ function clean(value) {
34
+ if (value === null || value === undefined || value === 0) return "";
35
+ return String(value).replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
36
  }
37
 
38
+ function normalize(value) {
39
+ return clean(value)
40
+ .normalize("NFKD")
41
+ .replace(/[\u064B-\u065F\u0670]/g, "")
42
+ .replace(/[أإآ]/g, "ا")
43
+ .replace(/ى/g, "ي")
44
+ .replace(/ة/g, "ه")
45
+ .toLowerCase();
46
+ }
47
+
48
+ function numeric(value) {
49
+ const parsed = Number(value);
50
+ return Number.isFinite(parsed) ? parsed : null;
51
+ }
52
+
53
+ function coordinateFromText(value) {
54
+ const text = clean(value);
55
+ const match = text.match(/(-?\d{1,2}(?:\.\d+)?)\s*[,،]\s*(-?\d{1,3}(?:\.\d+)?)/);
56
+ if (!match) return "";
57
+ const first = Number(match[1]);
58
+ const second = Number(match[2]);
59
+ if (first >= 15 && first <= 35 && second >= 35 && second <= 60) return `${first}, ${second}`;
60
+ if (second >= 15 && second <= 35 && first >= 35 && first <= 60) return `${second}, ${first}`;
61
  return "";
62
  }
63
 
64
+ function canonicalCity(primary, fallback) {
65
+ const raw = clean(primary) || clean(fallback) || "غير محدد";
66
+ const city = normalize(raw);
67
+ if (city.includes("الصناعيه الثانيه") && city.includes("الدمام")) return "المدينة الصناعية الثانية بالدمام";
68
+ if ((city.includes("الصناعيه الاولي") || city === "الصناعيه الاولي") && !city.includes("الاحساء")) {
69
+ return "المدينة الصناعية الأولى بالدمام";
70
+ }
71
+ if (city.includes("الصناعيه الثالثه") && city.includes("الدمام")) return "المدينة الصناعية الثالثة بالدمام";
72
+ if (city.includes("الاحساء") || city === "العيون" || city.includes("واحه مدن")) return "المدينة الصناعية الأولى بالأحساء";
73
+ if (city.includes("حفر الباطن")) return "المدينة الصناعية بحفر الباطن";
74
+ if (city.includes("الملك سلمان") || city.includes("سكيكو") || city.includes("ارامكو")) return "مدينة الملك سلمان (سبارك)";
75
+ if (city.includes("الصناعيه الثانيه") && city.includes("الرياض")) return "المدينة الصناعية الثانية بالرياض";
76
+ return raw;
77
+ }
78
+
79
+ function mapUrlFor(researcher) {
80
+ const target = normalize(researcher);
81
+ const exact = MAP_URLS.find(([name]) => normalize(name) === target);
82
+ if (exact) return exact[1];
83
+ const partial = MAP_URLS.find(([name]) => target.includes(normalize(name)) || normalize(name).includes(target));
84
+ return partial?.[1] || "";
85
  }
86
 
87
  if (!inputPath || !outputPath) {
88
  console.error("Usage: node generate-data.mjs <input.xlsx> <output.js> [password]");
89
  process.exitCode = 1;
90
  } else {
91
+ const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(inputPath));
92
+ const sheet = workbook.worksheets.getItemAt(0);
93
+ const values = sheet.getUsedRange(true).values;
94
  const rows = [];
95
 
96
+ for (const source of values.slice(1)) {
97
+ const researcher = clean(source[0]);
98
+ const establishmentName = clean(source[1]) || clean(source[2]);
99
+ if (!researcher || !establishmentName) continue;
100
+
101
+ const madonStatement = clean(source[7]);
102
+ const madonNote = clean(source[8]);
103
+ const madonCoordinates = coordinateFromText(madonNote);
104
+ const x = numeric(source[9]);
105
+ const y = numeric(source[10]);
106
+ const baseCoordinates =
107
+ x !== null && y !== null && x >= 35 && x <= 60 && y >= 15 && y <= 35 ? `${y}, ${x}` : "";
108
+ const coordinates = madonCoordinates || baseCoordinates;
109
+ const locationType = madonCoordinates
110
+ ? "madon"
111
+ : baseCoordinates
112
+ ? "base"
113
+ : madonStatement || madonNote
114
+ ? "statement"
115
+ : "none";
116
+
117
+ rows.push({
118
+ researcher,
119
+ establishmentName,
120
+ contractNumber: clean(source[3]),
121
+ city: canonicalCity(source[5], source[4]),
122
+ sourceCity: clean(source[5]) || clean(source[4]),
123
+ representativeCity: canonicalCity("", source[4]),
124
+ status: clean(source[6]),
125
+ madonStatement,
126
+ madonNoteText: madonCoordinates ? "" : madonNote,
127
+ coordinates,
128
+ locationType,
129
+ commercialRecord: clean(source[11]),
130
+ unifiedNumber: clean(source[12]),
131
+ activityCode: clean(source[13]),
132
+ activity: clean(source[14]),
133
+ });
134
  }
135
 
136
+ const counts = new Map();
137
+ rows.forEach((row) => counts.set(row.researcher, (counts.get(row.researcher) || 0) + 1));
138
+ const researchers = [...counts.entries()]
139
+ .map(([name, count]) => ({ name, count, mapUrl: mapUrlFor(name) }))
140
+ .sort((a, b) => a.name.localeCompare(b.name, "ar", { sensitivity: "base" }));
141
+
142
+ const missingMaps = researchers.filter((item) => !item.mapUrl).map((item) => item.name);
143
+ if (missingMaps.length) throw new Error(`Missing map URLs: ${missingMaps.join(", ")}`);
144
+
145
+ const plainPayload = JSON.stringify({
146
+ version: 3,
147
  generatedAt: new Date().toISOString(),
148
+ researchers,
149
  rows,
150
  });
151
 
 
154
  const iv = crypto.randomBytes(12);
155
  const key = crypto.pbkdf2Sync(password, salt, iterations, 32, "sha256");
156
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
157
+ const encrypted = Buffer.concat([cipher.update(plainPayload, "utf8"), cipher.final()]);
158
+ const combined = Buffer.concat([encrypted, cipher.getAuthTag()]);
 
 
159
  const output = [
160
+ "/* Generated encrypted data. Rebuild this file from the source workbook. */",
161
  "const ENCRYPTED_DATA = Object.freeze({",
162
  ` iterations: ${iterations},`,
163
  ` salt: "${salt.toString("base64")}",`,
 
168
  ].join("\n");
169
 
170
  await fs.writeFile(outputPath, output, "utf8");
171
+ console.log(JSON.stringify({
172
+ rows: rows.length,
173
+ researchers: researchers.length,
174
+ madonCoordinates: rows.filter((row) => row.locationType === "madon").length,
175
+ baseCoordinates: rows.filter((row) => row.locationType === "base").length,
176
+ statements: rows.filter((row) => row.locationType === "statement").length,
177
+ noLocation: rows.filter((row) => row.locationType === "none").length,
178
+ }));
179
  }
index.html CHANGED
@@ -5,30 +5,23 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <meta name="theme-color" content="#4137A8" />
7
  <meta name="robots" content="noindex, nofollow" />
8
- <title>شاشة استعلام</title>
9
- <link rel="preconnect" href="https://fonts.googleapis.com" />
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
- <link
12
- href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap"
13
- rel="stylesheet"
14
- />
15
  <link rel="stylesheet" href="style.css" />
16
  </head>
17
  <body>
18
  <main>
19
- <section id="loginView" class="login-view">
20
- <form id="loginForm" class="login-card" autocomplete="off">
21
  <div class="brand-mark" aria-hidden="true">
22
  <svg viewBox="0 0 24 24"><path d="M4 20V8l8-4 8 4v12H4Z" /><path d="M8 20v-7h8v7" /></svg>
23
  </div>
24
- <p class="eyebrow">نظام الاستعلام</p>
25
- <h1>شاشة استعلام</h1>
26
- <p class="login-description">أدخل رمز الدخول للمتابعة.</p>
27
-
28
  <label for="password">رمز الدخول</label>
29
- <div class="password-field">
30
  <input id="password" type="password" inputmode="numeric" placeholder="رمز الدخول" required autofocus />
31
- <button id="togglePassword" type="button" class="icon-button" aria-label="إظهار رمز الدخول">
32
  <svg viewBox="0 0 24 24"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" /><circle cx="12" cy="12" r="2.8" /></svg>
33
  </button>
34
  </div>
@@ -40,6 +33,21 @@
40
  </form>
41
  </section>
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  <section id="dashboardView" class="dashboard-view" hidden>
44
  <header class="topbar">
45
  <div class="topbar-brand">
@@ -47,80 +55,106 @@
47
  <svg viewBox="0 0 24 24"><path d="M4 20V8l8-4 8 4v12H4Z" /><path d="M8 20v-7h8v7" /></svg>
48
  </div>
49
  <div>
50
- <p>نظام الاستعلام</p>
51
- <h1>شاشة استعلام</h1>
52
  </div>
53
  </div>
54
- <button id="logoutButton" class="secondary-button compact" type="button">
55
- <svg viewBox="0 0 24 24"><path d="M10 17l5-5-5-5M15 12H3M15 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4" /></svg>
56
- خروج
57
- </button>
 
 
 
 
 
58
  </header>
59
 
60
  <div class="dashboard-shell">
61
- <section class="summary-panel">
62
- <div class="summary-copy">
63
- <p class="eyebrow">لوحة الاستعلام</p>
64
- <h2>اختر المنطقة والمدينة الصناعية أو ابحث باسم المنشأة مباشرة</h2>
65
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </section>
67
 
68
  <section class="controls-panel">
 
 
 
 
69
  <div class="filters-grid">
70
- <div class="field-group region-group">
71
- <span class="field-label">المنطقة</span>
72
- <div id="regionButtons" class="region-buttons" role="radiogroup" aria-label="اختيار المنطقة"></div>
73
- <select id="regionSelect" class="region-select-hidden" aria-hidden="true" tabindex="-1">
74
- <option value="">اختر المنطقة</option>
75
- </select>
76
- </div>
77
-
78
- <div class="field-group">
79
- <label for="citySelect">المدينة الصناعية</label>
80
- <div class="select-wrap">
81
- <select id="citySelect" disabled><option value="">اختر المدينة الصناعية</option></select>
82
  <svg viewBox="0 0 24 24"><path d="m7 10 5 5 5-5" /></svg>
83
  </div>
84
- </div>
85
-
86
- <div class="field-group search-group">
87
- <label for="searchInput">اسم المنشأة</label>
88
- <div class="search-wrap">
89
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4-4" /></svg>
90
- <input id="searchInput" type="search" placeholder="ابحث باسم المنشأة أو السجل التجاري" />
 
 
 
 
 
91
  </div>
92
- </div>
93
- </div>
94
-
95
- <div class="filters-actions">
96
- <button id="clearFiltersButton" class="clear-filters-button" type="button">
97
  <svg viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12" /></svg>
98
- مسح الاختيارات
99
  </button>
100
  </div>
101
  </section>
102
 
103
- <section id="emptyState" class="empty-state">
104
- <h3>ابدأ بالاختيار أو البحث</h3>
105
- <p>عند اختيار مدينة صناعية ستظهر كل بياناتها بالأسفل، ويمكن تضييق النتائج بالبحث.</p>
106
- </section>
107
 
108
- <section id="resultsSection" class="results-section" hidden>
109
  <div class="results-header">
110
  <div>
111
  <p id="resultsMeta" class="results-meta"></p>
112
- <h2 id="resultsTitle">النتائج</h2>
113
  </div>
114
  </div>
115
  <div id="samplesGrid" class="samples-grid"></div>
 
 
 
 
116
  <div id="loadMoreWrap" class="load-more-wrap" hidden>
117
- <button id="loadMoreButton" class="secondary-button load-more-button" type="button">عرض المزيد</button>
118
  <p id="loadMoreMeta"></p>
119
  </div>
120
- <div id="noResults" class="no-results" hidden>
121
- <h3>لا توجد نتائج مطابقة</h3>
122
- <p>جرّب كتابة جزء من اسم المنشأة أو رقم السجل التجاري.</p>
123
- </div>
124
  </section>
125
  </div>
126
  </section>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <meta name="theme-color" content="#4137A8" />
7
  <meta name="robots" content="noindex, nofollow" />
8
+ <title>بوابة الباحث الميداني</title>
 
 
 
 
 
 
9
  <link rel="stylesheet" href="style.css" />
10
  </head>
11
  <body>
12
  <main>
13
+ <section id="loginView" class="center-view">
14
+ <form id="loginForm" class="access-panel" autocomplete="off">
15
  <div class="brand-mark" aria-hidden="true">
16
  <svg viewBox="0 0 24 24"><path d="M4 20V8l8-4 8 4v12H4Z" /><path d="M8 20v-7h8v7" /></svg>
17
  </div>
18
+ <p class="eyebrow">قياس الوضع الراهن</p>
19
+ <h1>بوابة الباحث الميداني</h1>
20
+ <p class="lead">أدخل رمز الدخول للوصول إلى العينات وأدوات العمل الميداني.</p>
 
21
  <label for="password">رمز الدخول</label>
22
+ <div class="input-shell">
23
  <input id="password" type="password" inputmode="numeric" placeholder="رمز الدخول" required autofocus />
24
+ <button id="togglePassword" class="icon-button" type="button" aria-label="إظهار رمز الدخول" title="إظهار رمز الدخول">
25
  <svg viewBox="0 0 24 24"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" /><circle cx="12" cy="12" r="2.8" /></svg>
26
  </button>
27
  </div>
 
33
  </form>
34
  </section>
35
 
36
+ <section id="researcherView" class="center-view" hidden>
37
+ <div class="researcher-panel">
38
+ <div class="panel-heading">
39
+ <p class="eyebrow">اختيار الملف</p>
40
+ <h1>اختر اسم الباحث</h1>
41
+ <p>سيتم عرض العينات والخريطة والأدوات الخاصة بالباحث المختار.</p>
42
+ </div>
43
+ <div class="search-shell">
44
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4-4" /></svg>
45
+ <input id="researcherSearch" type="search" placeholder="ابحث باسم الباحث" />
46
+ </div>
47
+ <div id="researcherGrid" class="researcher-grid"></div>
48
+ </div>
49
+ </section>
50
+
51
  <section id="dashboardView" class="dashboard-view" hidden>
52
  <header class="topbar">
53
  <div class="topbar-brand">
 
55
  <svg viewBox="0 0 24 24"><path d="M4 20V8l8-4 8 4v12H4Z" /><path d="M8 20v-7h8v7" /></svg>
56
  </div>
57
  <div>
58
+ <p>بوابة الباحث الميداني</p>
59
+ <h1 id="researcherName"></h1>
60
  </div>
61
  </div>
62
+ <div class="topbar-actions">
63
+ <button id="switchResearcherButton" class="secondary-button compact" type="button">
64
+ <svg viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8M19 8v6M22 11h-6" /></svg>
65
+ تغيير الباحث
66
+ </button>
67
+ <button id="logoutButton" class="icon-button topbar-icon" type="button" aria-label="تسجيل الخروج" title="تسجيل الخروج">
68
+ <svg viewBox="0 0 24 24"><path d="M10 17l5-5-5-5M15 12H3M15 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4" /></svg>
69
+ </button>
70
+ </div>
71
  </header>
72
 
73
  <div class="dashboard-shell">
74
+ <section class="workspace-header">
75
+ <div>
76
+ <p class="eyebrow">مساحة العمل</p>
77
+ <h2>عينات الباحث وأدوات الزيارة الميدانية</h2>
78
  </div>
79
+ <div id="summaryStats" class="summary-stats"></div>
80
+ </section>
81
+
82
+ <section class="quick-actions" aria-label="أدوات الباحث">
83
+ <a id="researcherMapLink" class="tool-action map-action" target="_blank" rel="noopener">
84
+ <span class="tool-icon"><svg viewBox="0 0 24 24"><path d="m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6Z M9 3v15M15 6v15" /></svg></span>
85
+ <span><strong>الخريطة الشاملة</strong><small>عرض جميع مواقع الباحث</small></span>
86
+ <svg class="action-arrow" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6" /></svg>
87
+ </a>
88
+ <a id="formLink" class="tool-action form-action" target="_blank" rel="noopener">
89
+ <span class="tool-icon"><svg viewBox="0 0 24 24"><path d="M6 2h9l4 4v16H6V2Z M14 2v5h5M9 12h7M9 16h7M9 8h2" /></svg></span>
90
+ <span><strong>استمارة المسح</strong><small>فتح الاستمارة عند طلب المنشأة</small></span>
91
+ <svg class="action-arrow" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6" /></svg>
92
+ </a>
93
+ <button id="shareFormButton" class="tool-action share-action" type="button">
94
+ <span class="tool-icon"><svg viewBox="0 0 24 24"><path d="M20.5 11.5a8.5 8.5 0 0 1-12.6 7.4L3 20l1.2-4.7A8.5 8.5 0 1 1 20.5 11.5Z M8.6 8.7c.2 3.3 2.7 5.8 6 6.2" /></svg></span>
95
+ <span><strong>إرسال الاستمارة</strong><small>مشاركة رسالة جاهزة عبر واتساب</small></span>
96
+ <svg class="action-arrow" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6" /></svg>
97
+ </button>
98
  </section>
99
 
100
  <section class="controls-panel">
101
+ <div class="city-section">
102
+ <span class="field-label">المدينة الصناعية</span>
103
+ <div id="cityButtons" class="city-buttons" role="radiogroup" aria-label="اختيار المدينة الصناعية"></div>
104
+ </div>
105
  <div class="filters-grid">
106
+ <label class="field-group search-group">
107
+ <span>البحث</span>
108
+ <div class="search-shell">
109
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4-4" /></svg>
110
+ <input id="searchInput" type="search" placeholder="اسم المنشأة أو السجل التجاري أو الرقم الموحد" />
111
+ </div>
112
+ </label>
113
+ <label class="field-group">
114
+ <span>حالة المنشأة</span>
115
+ <div class="select-shell">
116
+ <select id="statusSelect"><option value="">جميع الحالات</option></select>
 
117
  <svg viewBox="0 0 24 24"><path d="m7 10 5 5 5-5" /></svg>
118
  </div>
119
+ </label>
120
+ <label class="field-group">
121
+ <span>بيانات الموقع</span>
122
+ <div class="select-shell">
123
+ <select id="locationSelect">
124
+ <option value="">جميع العينات</option>
125
+ <option value="madon"حداثية مدن</option>
126
+ <option value="base">إحداثية أساسية</option>
127
+ <option value="statement">إفادة مدن فقط</option>
128
+ <option value="none">لا توجد تفاصيل موقع</option>
129
+ </select>
130
+ <svg viewBox="0 0 24 24"><path d="m7 10 5 5 5-5" /></svg>
131
  </div>
132
+ </label>
133
+ <button id="clearFiltersButton" class="clear-button" type="button">
 
 
 
134
  <svg viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12" /></svg>
135
+ مسح الفلاتر
136
  </button>
137
  </div>
138
  </section>
139
 
140
+ <section id="representativePanel" class="representative-panel" hidden></section>
 
 
 
141
 
142
+ <section class="results-section">
143
  <div class="results-header">
144
  <div>
145
  <p id="resultsMeta" class="results-meta"></p>
146
+ <h2 id="resultsTitle">العينات</h2>
147
  </div>
148
  </div>
149
  <div id="samplesGrid" class="samples-grid"></div>
150
+ <div id="noResults" class="empty-state" hidden>
151
+ <h3>لا توجد نتائج مطابقة</h3>
152
+ <p>جرّب تغيير المدينة أو الحالة أو كتابة جزء من اسم المنشأة.</p>
153
+ </div>
154
  <div id="loadMoreWrap" class="load-more-wrap" hidden>
155
+ <button id="loadMoreButton" class="secondary-button" type="button">عرض المزيد</button>
156
  <p id="loadMoreMeta"></p>
157
  </div>
 
 
 
 
158
  </section>
159
  </div>
160
  </section>
style.css CHANGED
@@ -1,20 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  :root {
2
- --ink: #1CADE4;
3
- --muted: #5C6E88;
4
- --line: #d7e2ee;
5
- --page: #f5f8fb;
6
- --surface: #ffffff;
7
- --primary: #4137A8;
8
- --primary-dark: #1CADE4;
9
- --accent: #00B050;
10
- --accent-soft: #e8f8f0;
11
- --accent-alt: #42BA97;
12
- --info: #27CED7;
13
- --purple: #7030A0;
14
- --warning: #5C6E88;
15
- --warning-soft: #fff7d1;
16
- --danger: #F5544A;
17
- --shadow: 0 10px 28px rgba(92, 110, 136, 0.11);
18
  }
19
 
20
  * {
@@ -22,17 +36,17 @@
22
  }
23
 
24
  html {
25
- min-height: 100%;
26
- background: var(--page);
27
  }
28
 
29
  body {
30
- min-height: 100vh;
31
  margin: 0;
32
  color: var(--ink);
33
- font-family: "Tajawal", Arial, sans-serif;
34
- font-size: 16px;
35
- background: linear-gradient(180deg, #fbfcfd 0%, var(--page) 100%);
 
36
  }
37
 
38
  button,
@@ -41,50 +55,68 @@ select {
41
  font: inherit;
42
  }
43
 
44
- button {
45
- cursor: pointer;
 
46
  }
47
 
48
- [hidden] {
49
- display: none !important;
50
  }
51
 
52
  svg {
53
- width: 20px;
54
- height: 20px;
55
  fill: none;
56
  stroke: currentColor;
 
57
  stroke-linecap: round;
58
  stroke-linejoin: round;
59
- stroke-width: 1.8;
60
  }
61
 
62
- .login-view {
63
- min-height: calc(100vh - 52px);
 
 
 
 
64
  display: grid;
65
  place-items: center;
66
- padding: 24px;
67
  }
68
 
69
- .login-card {
70
- width: min(100%, 430px);
 
71
  padding: 34px;
72
- text-align: right;
73
- background: var(--surface);
74
  border: 1px solid var(--line);
75
  border-radius: 8px;
76
  box-shadow: var(--shadow);
77
  }
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  .brand-mark {
80
- width: 56px;
81
- height: 56px;
82
  display: grid;
83
  place-items: center;
84
- margin-bottom: 18px;
85
- color: #fff;
86
- background: var(--primary);
87
  border-radius: 8px;
 
88
  }
89
 
90
  .brand-mark svg {
@@ -95,13 +127,17 @@ svg {
95
  .brand-mark.small {
96
  width: 44px;
97
  height: 44px;
98
- margin: 0;
 
 
 
 
99
  }
100
 
101
  .eyebrow {
102
- margin: 0 0 6px;
103
- color: var(--accent);
104
- font-size: 13px;
105
  font-weight: 800;
106
  }
107
 
@@ -109,1309 +145,975 @@ h1,
109
  h2,
110
  h3,
111
  p {
112
- overflow-wrap: anywhere;
113
  }
114
 
115
- .login-card h1 {
116
- margin: 0;
117
- color: var(--primary-dark);
118
- font-size: 28px;
119
- line-height: 1.35;
120
  }
121
 
122
- .login-description {
123
- margin: 10px 0 24px;
124
  color: var(--muted);
125
  line-height: 1.8;
126
  }
127
 
128
- .login-card label,
129
- .field-group label {
 
130
  display: block;
131
- margin-bottom: 8px;
132
- color: #34495a;
133
- font-weight: 700;
134
  }
135
 
136
- .password-field,
137
- .search-wrap,
138
- .select-wrap {
139
  position: relative;
140
  }
141
 
142
- .password-field input,
143
- .search-wrap input,
144
- .select-wrap select {
145
  width: 100%;
146
- height: 50px;
 
147
  color: var(--ink);
148
- background: #fbfdfe;
149
- border: 1px solid var(--line);
150
  border-radius: 8px;
151
  outline: none;
152
- transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
153
  }
154
 
155
- .password-field input {
156
- padding: 0 14px 0 48px;
157
  }
158
 
159
- .password-field input:focus,
160
- .search-wrap input:focus,
161
- .select-wrap select:focus {
162
- background: #fff;
 
 
 
 
 
 
 
 
163
  border-color: var(--primary);
164
- box-shadow: 0 0 0 3px rgba(15, 76, 92, 0.12);
165
  }
166
 
167
- .icon-button {
 
 
168
  position: absolute;
169
  top: 50%;
170
- left: 7px;
171
- width: 36px;
172
- height: 36px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  display: grid;
174
  place-items: center;
175
- padding: 0;
176
- color: var(--muted);
177
  background: transparent;
178
  border: 0;
179
  border-radius: 8px;
180
- transform: translateY(-50%);
181
  }
182
 
183
  .icon-button:hover {
184
  color: var(--primary);
185
- background: #edf5f7;
186
- }
187
-
188
- .form-error {
189
- min-height: 22px;
190
- margin: 7px 2px 8px;
191
- color: var(--danger);
192
- font-size: 13px;
193
- font-weight: 700;
194
  }
195
 
196
  .primary-button,
197
- .secondary-button {
 
 
 
198
  display: inline-flex;
199
  align-items: center;
200
  justify-content: center;
201
  gap: 8px;
202
- min-height: 46px;
203
- padding: 0 18px;
204
- font-weight: 800;
205
  border-radius: 8px;
 
 
 
206
  }
207
 
208
  .primary-button {
209
  width: 100%;
210
- color: #fff;
211
- background: var(--primary);
212
- border: 0;
 
 
 
 
 
 
 
213
  }
214
 
215
- .primary-button:hover {
216
- background: var(--primary-dark);
 
 
 
217
  }
218
 
219
- .primary-button:disabled {
220
- cursor: wait;
221
- opacity: 0.65;
222
  }
223
 
224
- .secondary-button {
225
- color: var(--primary);
226
- background: #fff;
227
- border: 1px solid var(--line);
228
  }
229
 
230
- .secondary-button:hover {
231
- background: #edf5f7;
232
  }
233
 
234
- .topbar {
235
- min-height: 72px;
236
- display: flex;
237
- align-items: center;
238
- justify-content: space-between;
239
- gap: 16px;
240
- padding: 12px max(18px, calc((100% - 1180px) / 2));
241
- background: rgba(255, 255, 255, 0.96);
242
- border-bottom: 1px solid var(--line);
243
  }
244
 
245
- .topbar-brand {
 
 
 
 
 
 
 
 
 
 
246
  display: flex;
247
  align-items: center;
248
- gap: 12px;
 
 
 
 
 
 
 
 
249
  }
250
 
251
- .topbar-brand p,
252
- .topbar-brand h1 {
253
- margin: 0;
254
  }
255
 
256
- .topbar-brand p {
257
- color: var(--accent);
258
- font-size: 12px;
259
- font-weight: 800;
260
  }
261
 
262
- .topbar-brand h1 {
263
- color: var(--primary-dark);
264
- font-size: 20px;
265
  }
266
 
267
- .secondary-button.compact {
268
- min-height: 40px;
269
- padding: 0 14px;
270
  }
271
 
272
- .dashboard-shell {
273
- width: min(1180px, calc(100% - 36px));
274
- margin: 22px auto 40px;
275
  }
276
 
277
- .summary-panel,
278
- .controls-panel,
279
- .empty-state,
280
- .no-results,
281
- .sample-card {
282
- background: var(--surface);
283
- border: 1px solid var(--line);
284
- border-radius: 8px;
285
- box-shadow: var(--shadow);
286
  }
287
 
288
- .summary-panel {
 
 
 
 
289
  display: flex;
290
  align-items: center;
291
- justify-content: center;
292
- gap: 18px;
293
- padding: 28px 24px;
294
- border-top: 4px solid var(--primary);
295
- text-align: center;
 
296
  }
297
 
298
- .summary-panel h2 {
299
- margin: 0;
300
- color: var(--primary-dark);
301
- font-size: 24px;
302
- line-height: 1.45;
303
  }
304
 
305
- .counter-pill {
306
- flex: 0 0 auto;
307
- padding: 9px 14px;
308
- color: var(--accent);
309
  font-weight: 800;
310
- background: var(--accent-soft);
311
- border: 1px solid #c3e7d8;
312
- border-radius: 8px;
313
  }
314
 
315
- .controls-panel {
316
- margin-top: 14px;
317
- padding: 18px;
 
318
  }
319
 
320
- .filters-grid {
321
- display: grid;
322
- grid-template-columns: minmax(180px, 0.9fr) minmax(220px, 1fr) minmax(260px, 1.35fr);
323
- gap: 14px;
324
  }
325
 
326
- .select-wrap select {
327
- padding: 0 14px 0 40px;
328
- appearance: none;
 
 
329
  }
330
 
331
- .select-wrap > svg {
332
- position: absolute;
333
- top: 50%;
334
- left: 13px;
335
- color: var(--muted);
336
- transform: translateY(-50%);
337
- pointer-events: none;
338
  }
339
 
340
- .search-wrap input {
341
- padding: 0 42px 0 14px;
 
342
  }
343
 
344
- .search-wrap > svg {
345
- position: absolute;
346
- top: 50%;
347
- right: 13px;
348
- color: var(--muted);
349
- transform: translateY(-50%);
 
 
 
 
 
350
  }
351
 
352
- select:disabled {
353
- color: #9aa8b2;
354
- background: #f2f5f6;
355
- cursor: not-allowed;
356
  }
357
 
358
- .filters-actions {
359
- display: flex;
360
- justify-content: flex-start;
361
- margin-top: 12px;
362
  }
363
 
364
- .clear-filters-button {
365
- display: inline-flex;
366
- align-items: center;
367
- gap: 7px;
368
- min-height: 38px;
369
- padding: 0 12px;
370
- color: var(--muted);
371
- font-weight: 800;
 
 
372
  background: #f8fafb;
373
  border: 1px solid var(--line);
374
  border-radius: 8px;
375
  }
376
 
377
- .clear-filters-button:hover {
378
- color: var(--danger);
379
- background: #fff7f8;
380
- }
381
-
382
- .empty-state,
383
- .no-results {
384
- margin-top: 16px;
385
- padding: 44px 20px;
386
- text-align: center;
387
- box-shadow: none;
388
  }
389
 
390
- .empty-state h3,
391
- .no-results h3 {
392
- margin: 0 0 8px;
393
- color: var(--primary-dark);
394
- font-size: 20px;
395
  }
396
 
397
- .empty-state p,
398
- .no-results p {
399
- margin: 0;
400
  color: var(--muted);
 
 
401
  }
402
 
403
- .results-section {
404
- margin-top: 22px;
405
- }
406
-
407
- .results-header {
408
- display: flex;
409
- align-items: center;
410
- justify-content: space-between;
411
- gap: 16px;
412
- margin-bottom: 14px;
413
  }
414
 
415
- .results-meta {
416
- margin: 0 0 4px;
417
- color: var(--accent);
418
- font-size: 14px;
419
- font-weight: 800;
420
  }
421
 
422
- .results-header h2 {
423
- margin: 0;
424
- color: var(--primary-dark);
425
- font-size: 25px;
426
  }
427
 
428
- .samples-grid {
429
  display: grid;
430
- grid-template-columns: repeat(2, minmax(0, 1fr));
431
- gap: 14px;
432
- }
433
-
434
- .sample-card {
435
- overflow: hidden;
436
  }
437
 
438
- .sample-card-header {
 
439
  display: flex;
440
- align-items: flex-start;
441
- justify-content: space-between;
442
- gap: 12px;
443
- padding: 16px 18px;
444
- background: #fbfdfe;
445
- border-bottom: 1px solid #edf2f5;
446
- border-top: 4px solid var(--accent);
 
 
 
 
447
  }
448
 
449
- .sample-index {
450
- display: inline-block;
451
- margin-bottom: 5px;
452
- color: var(--accent);
453
- font-size: 12px;
454
- font-weight: 800;
455
  }
456
 
457
- .sample-card h3 {
458
- margin: 0;
459
- color: var(--primary-dark);
460
- font-size: 18px;
461
- line-height: 1.6;
462
  }
463
 
464
- .status-badge {
 
 
465
  flex: 0 0 auto;
466
- max-width: 150px;
467
- padding: 7px 10px;
468
- color: var(--warning);
469
- font-size: 12px;
470
- font-weight: 800;
471
- line-height: 1.45;
472
- text-align: center;
473
- background: var(--warning-soft);
474
- border: 1px solid #f0d394;
475
  border-radius: 8px;
476
  }
477
 
478
- .sample-details {
479
- display: grid;
480
- grid-template-columns: repeat(2, minmax(0, 1fr));
481
- gap: 0;
482
- padding: 4px 18px 12px;
483
  }
484
 
485
- .detail-item {
486
- min-width: 0;
487
- padding: 13px 0;
488
- border-bottom: 1px solid #edf2f5;
489
  }
490
 
491
- .detail-item:nth-child(odd) {
492
- padding-left: 14px;
493
- border-left: 1px solid #edf2f5;
494
  }
495
 
496
- .detail-item:nth-child(even) {
497
- padding-right: 14px;
 
498
  }
499
 
500
- .detail-label {
501
- display: block;
502
- margin-bottom: 5px;
503
  color: var(--muted);
504
  font-size: 12px;
505
- font-weight: 800;
506
- }
507
-
508
- .detail-value {
509
- display: block;
510
- color: #253b4c;
511
- font-weight: 700;
512
- line-height: 1.65;
513
  }
514
 
515
- .wide-detail,
516
- .coordinates-detail {
517
- grid-column: 1 / -1;
518
- padding-left: 0 !important;
519
- padding-right: 0 !important;
520
- border-left: 0 !important;
521
  }
522
 
523
- .coordinates-detail {
524
- position: relative;
525
- margin-top: 10px;
526
- padding: 13px 14px !important;
527
- border: 1px solid transparent !important;
528
  border-radius: 8px;
 
529
  }
530
 
531
- .coordinates-detail::before {
532
- content: "";
533
- position: absolute;
534
- top: 12px;
535
- right: 0;
536
- bottom: 12px;
537
- width: 4px;
538
- border-radius: 8px 0 0 8px;
539
- }
540
-
541
- .map-detail {
542
- background: #edf9f3;
543
- border-color: #bfead2 !important;
544
- }
545
-
546
- .map-detail::before {
547
- background: var(--accent);
548
- }
549
-
550
- .statement-detail {
551
- background: #eef7fb;
552
- border-color: #c7e1ec !important;
553
  }
554
 
555
- .statement-detail::before {
556
- background: var(--info);
 
 
557
  }
558
 
559
- .map-detail .detail-label,
560
- .statement-detail .detail-label {
561
- display: inline-flex;
562
  align-items: center;
563
- gap: 6px;
564
- min-height: 24px;
565
- padding: 3px 8px;
 
 
566
  border-radius: 8px;
 
567
  }
568
 
569
- .map-detail .detail-label {
570
- color: var(--primary-dark);
571
- background: #dff4e9;
572
- }
573
-
574
- .statement-detail .detail-label {
575
- color: var(--primary-dark);
576
- background: #dff0f6;
577
- }
578
-
579
- .map-detail .action-link {
580
- color: var(--primary-dark);
581
- background: #ffffff;
582
- border-color: #b7e3ca;
583
  }
584
 
585
- .statement-detail .action-link {
586
- color: var(--primary-dark);
587
- background: #ffffff;
588
- border-color: #bddce8;
589
  }
590
 
591
- .map-detail .action-link:hover,
592
- .statement-detail .action-link:hover {
593
- color: #fff;
594
- background: var(--primary);
595
- border-color: var(--primary);
596
  }
597
 
598
- .ltr-value .detail-value {
599
- direction: ltr;
600
- text-align: right;
601
  }
602
 
603
- .action-link {
604
- display: inline-flex;
605
- align-items: center;
606
- justify-content: center;
607
- gap: 7px;
608
- min-height: 38px;
609
- padding: 0 12px;
610
- color: var(--primary);
611
- font-weight: 800;
612
- text-decoration: none;
613
- background: #eef7f9;
614
- border: 1px solid #cbe2e8;
615
- border-radius: 8px;
616
  }
617
 
618
- button.action-link {
619
- font: inherit;
620
  }
621
 
622
- .action-link:hover {
623
- color: #fff;
624
- background: var(--primary);
625
- border-color: var(--primary);
626
  }
627
 
628
- .contact-note {
629
- margin: 0 0 10px;
630
- color: #253b4c;
631
- font-weight: 700;
632
- line-height: 1.8;
633
- white-space: pre-line;
634
  }
635
 
636
- .contact-actions {
637
  display: flex;
638
- flex-wrap: wrap;
639
- gap: 8px;
 
 
 
 
 
 
 
640
  }
641
 
642
- .load-more-wrap {
 
643
  display: flex;
644
- flex-direction: column;
645
  align-items: center;
646
- gap: 8px;
647
- margin-top: 22px;
648
  }
649
 
650
- .load-more-button {
651
- min-width: 190px;
 
 
 
 
 
 
652
  }
653
 
654
- .load-more-wrap p {
655
- margin: 0;
656
- color: var(--muted);
657
- font-size: 13px;
658
- font-weight: 700;
659
  }
660
 
661
- footer {
662
- min-height: 52px;
663
- display: flex;
664
- align-items: center;
665
- justify-content: center;
666
  color: var(--muted);
667
- font-size: 13px;
668
- font-weight: 700;
669
  }
670
 
671
- .toast {
672
- position: fixed;
673
- z-index: 30;
674
- right: 50%;
675
- bottom: 20px;
676
- padding: 12px 16px;
677
- color: #fff;
678
  font-weight: 800;
679
- background: var(--primary-dark);
680
- border-radius: 8px;
681
- box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
682
- opacity: 0;
683
- transform: translate(50%, 18px);
684
- transition: opacity 0.22s, transform 0.22s;
685
- pointer-events: none;
686
  }
687
 
688
- .toast.show {
689
- opacity: 1;
690
- transform: translate(50%, 0);
 
691
  }
692
 
693
- @media (max-width: 900px) {
694
- .filters-grid {
695
- grid-template-columns: 1fr 1fr;
696
- }
697
-
698
- .search-group {
699
- grid-column: 1 / -1;
700
- }
701
  }
702
 
703
- @media (max-width: 720px) {
704
- .dashboard-shell {
705
- width: min(100% - 24px, 1180px);
706
- margin-top: 14px;
707
- }
708
-
709
- .summary-panel,
710
- .results-header {
711
- align-items: stretch;
712
- flex-direction: column;
713
- }
714
-
715
- .counter-pill {
716
- align-self: flex-start;
717
- }
718
-
719
- .samples-grid {
720
- grid-template-columns: 1fr;
721
- }
722
  }
723
 
724
- @media (max-width: 560px) {
725
- .login-view {
726
- padding: 18px;
727
- }
728
-
729
- .login-card {
730
- padding: 24px;
731
- }
732
-
733
- .topbar {
734
- padding: 10px 12px;
735
- }
736
-
737
- .topbar-brand h1 {
738
- font-size: 16px;
739
- }
740
-
741
- .topbar-brand p {
742
- display: none;
743
- }
744
-
745
- .filters-grid {
746
- grid-template-columns: 1fr;
747
- }
748
-
749
- .search-group {
750
- grid-column: auto;
751
- grid-row: 1;
752
- }
753
-
754
- .password-field input,
755
- .search-wrap input,
756
- .select-wrap select {
757
- height: 54px;
758
- }
759
-
760
- .sample-card-header {
761
- flex-direction: column;
762
- }
763
-
764
- .status-badge {
765
- max-width: 100%;
766
- }
767
-
768
- .sample-details {
769
- grid-template-columns: 1fr;
770
- padding: 4px 16px 12px;
771
- }
772
-
773
- .detail-item:nth-child(odd),
774
- .detail-item:nth-child(even) {
775
- padding-right: 0;
776
- padding-left: 0;
777
- border-left: 0;
778
- }
779
-
780
- .contact-actions .action-link {
781
- flex: 1 1 100%;
782
- }
783
  }
784
 
785
- /* Final responsive polish */
786
- :root {
787
- --soft-shadow: 0 1px 2px rgba(15, 35, 45, 0.06), 0 14px 36px rgba(15, 35, 45, 0.08);
788
- --lift-shadow: 0 18px 44px rgba(15, 35, 45, 0.12);
 
789
  }
790
 
791
- body {
792
- background:
793
- linear-gradient(180deg, rgba(15, 76, 92, 0.06) 0, rgba(15, 76, 92, 0) 260px),
794
- linear-gradient(180deg, #fbfcfd 0%, var(--page) 100%);
795
  }
796
 
797
- button,
798
- a,
799
- input,
800
- select {
801
- -webkit-tap-highlight-color: transparent;
 
 
 
 
 
802
  }
803
 
804
- button:focus-visible,
805
- a:focus-visible,
806
- input:focus-visible,
807
- select:focus-visible {
808
- outline: 3px solid rgba(27, 138, 107, 0.28);
809
- outline-offset: 2px;
810
  }
811
 
812
- .login-card {
813
- position: relative;
814
- overflow: hidden;
815
- border-color: rgba(217, 227, 232, 0.9);
816
- box-shadow: var(--soft-shadow);
 
 
 
 
817
  }
818
 
819
- .login-card::before {
820
- content: "";
821
- position: absolute;
822
- inset: 0 0 auto;
823
- height: 5px;
824
- background: linear-gradient(90deg, var(--accent), var(--primary));
825
  }
826
 
827
- .brand-mark {
828
- background: linear-gradient(145deg, var(--primary), #177b72);
829
- box-shadow: 0 10px 24px rgba(15, 76, 92, 0.22);
 
 
 
830
  }
831
 
832
- .topbar {
833
- position: sticky;
834
- top: 0;
835
- z-index: 20;
836
- box-shadow: 0 8px 24px rgba(28, 53, 68, 0.06);
837
- backdrop-filter: blur(14px);
838
  }
839
 
840
- .dashboard-shell {
841
- margin-top: 18px;
 
 
 
 
 
 
 
 
 
 
842
  }
843
 
844
- .summary-panel,
845
- .controls-panel,
846
- .sample-card {
847
- box-shadow: var(--soft-shadow);
848
  }
849
 
850
- .summary-panel {
851
- align-items: center;
852
- min-height: 128px;
853
- padding: 24px;
854
- border-top: 0;
855
- border-right: 0;
856
- border-bottom: 4px solid var(--primary);
857
  }
858
 
859
- .summary-copy {
860
- display: flex;
861
- flex-direction: column;
862
- justify-content: center;
863
- align-items: center;
864
- min-width: 0;
865
- width: 100%;
866
  }
867
 
868
- .summary-panel h2 {
869
- max-width: 820px;
870
- font-size: 28px;
871
  }
872
 
873
- .counter-pill {
874
- align-self: flex-start;
875
- display: inline-flex;
876
- align-items: center;
877
- justify-content: center;
878
- min-height: 40px;
879
- padding-inline: 16px;
880
  }
881
 
882
- .region-stats {
883
- display: grid;
884
- grid-template-columns: repeat(2, minmax(0, 1fr));
885
- gap: 8px;
886
  }
887
 
888
- .region-stat {
889
- display: flex;
890
- align-items: center;
891
- justify-content: space-between;
892
- gap: 8px;
893
- min-width: 0;
894
- min-height: 42px;
895
- padding: 8px 10px;
896
- color: #315161;
897
- background: #f8fbfc;
898
- border: 1px solid #dbe8ed;
899
- border-radius: 8px;
900
  font-weight: 800;
901
- transition: transform 0.18s, border-color 0.18s, background 0.18s;
902
  }
903
 
904
- .region-stat:hover {
905
- background: #eef7f9;
906
- border-color: #c0dce4;
907
- transform: translateY(-1px);
908
  }
909
 
910
- .region-stat span {
911
- min-width: 0;
912
- overflow: hidden;
913
- text-overflow: ellipsis;
914
- white-space: nowrap;
915
  }
916
 
917
- .region-stat strong {
918
- flex: 0 0 auto;
919
- min-width: 32px;
920
- padding: 3px 7px;
921
- color: var(--accent);
922
- text-align: center;
923
- background: var(--accent-soft);
924
  border-radius: 8px;
925
  }
926
 
927
- .controls-panel {
928
- position: static;
929
- top: auto;
930
- z-index: 15;
931
- border-color: rgba(217, 227, 232, 0.92);
932
  }
933
 
934
- .filters-grid {
935
- align-items: end;
 
936
  }
937
 
938
- .password-field input,
939
- .search-wrap input,
940
- .select-wrap select {
941
- min-height: 52px;
942
- border-color: #d2dfe6;
943
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
944
  }
945
 
946
- .clear-filters-button,
947
- .action-link,
948
- .primary-button,
949
- .secondary-button {
950
- transition: transform 0.18s, color 0.18s, background 0.18s, border-color 0.18s, box-shadow 0.18s;
951
  }
952
 
953
- .clear-filters-button:hover,
954
- .action-link:hover,
955
- .secondary-button:hover {
956
- transform: translateY(-1px);
957
  }
958
 
959
- .results-section {
960
- scroll-margin-top: 150px;
961
  }
962
 
963
- .results-header {
964
- padding: 0 2px;
965
  }
966
 
967
- .samples-grid {
968
- grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
 
 
969
  }
970
 
971
- .sample-card {
 
 
 
 
 
972
  display: flex;
973
- flex-direction: column;
974
- min-height: 100%;
975
- transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
 
976
  }
977
 
978
- .sample-card:hover {
979
- border-color: #c1dbe2;
980
- box-shadow: var(--lift-shadow);
981
- transform: translateY(-2px);
982
  }
983
 
984
- .sample-card-header {
985
- border-top: 0;
986
- border-right: 4px solid var(--accent);
987
- background:
988
- linear-gradient(180deg, rgba(248, 252, 253, 0.98), rgba(255, 255, 255, 0.98));
989
  }
990
 
991
- .sample-card h3 {
992
- font-size: 17px;
 
 
993
  }
994
 
995
- .status-badge {
996
- white-space: normal;
 
 
 
 
997
  }
998
 
999
- .sample-details {
1000
- flex: 1;
1001
  }
1002
 
1003
- .action-link {
1004
- min-height: 40px;
1005
- white-space: nowrap;
1006
  }
1007
 
1008
- .load-more-button {
1009
- box-shadow: var(--soft-shadow);
 
 
 
 
1010
  }
1011
 
1012
- @media (min-width: 1220px) {
1013
- .dashboard-shell {
1014
- width: min(1280px, calc(100% - 48px));
1015
- }
 
 
 
 
 
 
 
 
 
 
 
 
1016
 
1017
- .topbar {
1018
- padding-right: max(24px, calc((100% - 1280px) / 2));
1019
- padding-left: max(24px, calc((100% - 1280px) / 2));
1020
- }
 
 
 
 
 
 
 
1021
  }
1022
 
1023
- @media (max-width: 980px) {
1024
- .summary-panel {
 
1025
  flex-direction: column;
1026
  }
1027
 
1028
- .controls-panel {
1029
- position: static;
1030
  }
1031
- }
1032
 
1033
- @media (max-width: 720px) {
1034
- body {
1035
- background: #f6f8f9;
1036
  }
 
1037
 
 
1038
  .topbar {
1039
- min-height: 64px;
1040
  }
1041
 
1042
- .dashboard-shell {
1043
- width: min(100% - 20px, 1180px);
1044
- margin-bottom: 28px;
1045
  }
1046
 
1047
- .summary-panel {
1048
- min-height: auto;
1049
- padding: 18px;
1050
- border-bottom-width: 4px;
 
1051
  }
1052
 
1053
- .summary-panel h2 {
1054
- font-size: 21px;
1055
- line-height: 1.55;
 
1056
  }
1057
 
1058
- .region-stats {
1059
- grid-template-columns: 1fr 1fr;
1060
  }
1061
 
1062
- .controls-panel {
1063
- padding: 14px;
1064
  }
1065
 
1066
- .filters-actions {
1067
- margin-top: 10px;
1068
  }
1069
 
1070
- .samples-grid {
1071
  grid-template-columns: 1fr;
1072
- gap: 12px;
1073
  }
1074
 
1075
- .sample-card:hover {
1076
- transform: none;
1077
- }
1078
- }
1079
-
1080
- @media (max-width: 560px) {
1081
- .login-card {
1082
  width: 100%;
1083
- padding: 26px 20px 22px;
1084
  }
1085
 
1086
- .login-card h1 {
1087
- font-size: 24px;
 
1088
  }
1089
 
1090
- .login-description {
1091
- font-size: 15px;
1092
  }
1093
 
1094
- .topbar {
1095
- gap: 10px;
1096
  }
 
1097
 
1098
- .secondary-button.compact {
1099
- min-width: 44px;
1100
- padding: 0 10px;
 
1101
  }
1102
 
1103
- .summary-panel,
1104
- .controls-panel,
1105
- .sample-card,
1106
- .empty-state,
1107
- .no-results {
1108
- border-radius: 8px;
1109
  }
1110
 
1111
- .region-stats {
1112
- grid-template-columns: 1fr;
 
1113
  }
1114
 
1115
- .region-stat {
1116
- min-height: 46px;
 
1117
  }
1118
 
1119
- .clear-filters-button {
1120
- width: 100%;
1121
- justify-content: center;
1122
- min-height: 44px;
1123
  }
1124
 
1125
- .sample-card-header {
 
1126
  padding: 15px;
1127
- border-right-width: 4px;
1128
- }
1129
-
1130
- .sample-card h3 {
1131
- font-size: 16px;
1132
- line-height: 1.65;
1133
  }
1134
 
1135
- .sample-details {
1136
- padding: 2px 15px 10px;
 
1137
  }
1138
 
1139
- .detail-item {
1140
- padding: 12px 0;
1141
  }
1142
 
1143
- .detail-value,
1144
- .contact-note {
1145
- font-size: 15px;
1146
  }
1147
 
1148
- .action-link {
1149
- min-height: 46px;
1150
  }
1151
 
1152
- .toast {
1153
- right: 12px;
1154
- left: 12px;
1155
- bottom: 14px;
1156
- text-align: center;
1157
- transform: translateY(18px);
1158
  }
1159
 
1160
- .toast.show {
1161
- transform: translateY(0);
 
1162
  }
1163
- }
1164
 
1165
- /* Region buttons replace the region dropdown */
1166
- .login-card label,
1167
- .field-group label,
1168
- .field-label {
1169
- display: block;
1170
- margin-bottom: 8px;
1171
- color: #34495a;
1172
- font-weight: 700;
1173
- }
1174
-
1175
- .filters-grid {
1176
- grid-template-columns: minmax(220px, 1fr) minmax(300px, 1.35fr);
1177
- }
1178
-
1179
- .region-group {
1180
- grid-column: 1 / -1;
1181
- }
1182
-
1183
- .region-select-hidden {
1184
- position: absolute;
1185
- width: 1px;
1186
- height: 1px;
1187
- overflow: hidden;
1188
- clip: rect(0 0 0 0);
1189
- white-space: nowrap;
1190
- border: 0;
1191
- opacity: 0;
1192
- pointer-events: none;
1193
- }
1194
-
1195
- .region-buttons {
1196
- display: grid;
1197
- grid-template-columns: repeat(4, minmax(0, 1fr));
1198
- gap: 10px;
1199
- }
1200
-
1201
- .region-choice {
1202
- display: flex;
1203
- align-items: center;
1204
- justify-content: space-between;
1205
- gap: 10px;
1206
- min-width: 0;
1207
- min-height: 54px;
1208
- padding: 10px 13px;
1209
- color: #315161;
1210
- text-align: right;
1211
- background: #fbfdfe;
1212
- border: 1px solid #d2dfe6;
1213
- border-radius: 8px;
1214
- font-weight: 800;
1215
- transition: transform 0.18s, color 0.18s, background 0.18s, border-color 0.18s, box-shadow 0.18s;
1216
- }
1217
-
1218
- .region-choice:hover {
1219
- background: #eef7f9;
1220
- border-color: #bcd8df;
1221
- transform: translateY(-1px);
1222
- }
1223
-
1224
- .region-choice.active {
1225
- color: #fff;
1226
- background: linear-gradient(145deg, var(--primary), #177b72);
1227
- border-color: transparent;
1228
- box-shadow: 0 12px 26px rgba(15, 76, 92, 0.16);
1229
- }
1230
-
1231
- .region-choice span {
1232
- min-width: 0;
1233
- overflow: hidden;
1234
- text-overflow: ellipsis;
1235
- white-space: nowrap;
1236
- }
1237
-
1238
- .region-choice strong {
1239
- flex: 0 0 auto;
1240
- min-width: 34px;
1241
- padding: 3px 8px;
1242
- color: var(--accent);
1243
- text-align: center;
1244
- background: var(--accent-soft);
1245
- border-radius: 8px;
1246
- }
1247
-
1248
- .region-choice.active strong {
1249
- color: var(--primary-dark);
1250
- background: #d9fff4;
1251
- }
1252
-
1253
- @media (max-width: 900px) {
1254
- .filters-grid {
1255
  grid-template-columns: 1fr;
1256
  }
1257
 
1258
- .region-buttons {
1259
- grid-template-columns: repeat(2, minmax(0, 1fr));
 
 
 
1260
  }
1261
- }
1262
 
1263
- @media (max-width: 560px) {
1264
- .filters-grid {
1265
- grid-template-columns: 1fr;
1266
  }
1267
 
1268
- .region-buttons {
1269
- grid-template-columns: 1fr;
 
1270
  }
1271
 
1272
- .region-choice {
1273
- min-height: 48px;
 
 
 
 
1274
  }
1275
  }
1276
-
1277
- /* Identity color system */
1278
- body {
1279
- background:
1280
- linear-gradient(180deg, rgba(65, 55, 168, 0.07) 0, rgba(65, 55, 168, 0) 260px),
1281
- linear-gradient(180deg, #fbfcfd 0%, var(--page) 100%);
1282
- }
1283
-
1284
- .eyebrow,
1285
- .results-meta,
1286
- .sample-index {
1287
- color: var(--accent);
1288
- }
1289
-
1290
- .login-card::before {
1291
- background: linear-gradient(90deg, var(--accent), var(--primary));
1292
- }
1293
-
1294
- .brand-mark,
1295
- .primary-button,
1296
- .region-choice.active {
1297
- background: linear-gradient(145deg, var(--primary), var(--primary-dark));
1298
- }
1299
-
1300
- .primary-button:hover,
1301
- .action-link:hover,
1302
- .secondary-button:hover {
1303
- background: var(--primary-dark);
1304
- border-color: var(--primary-dark);
1305
- }
1306
-
1307
- .topbar {
1308
- border-bottom-color: rgba(92, 110, 136, 0.18);
1309
- }
1310
-
1311
- .summary-panel {
1312
- border-bottom-color: var(--primary);
1313
- }
1314
-
1315
- .sample-card-header {
1316
- border-right-color: var(--accent);
1317
- }
1318
-
1319
- .status-badge {
1320
- color: #5C6E88;
1321
- background: #fff4bf;
1322
- border-color: #FFC000;
1323
- }
1324
-
1325
- .map-detail {
1326
- background: rgba(0, 176, 80, 0.08);
1327
- border-color: rgba(0, 176, 80, 0.28) !important;
1328
- }
1329
-
1330
- .map-detail::before {
1331
- background: var(--accent);
1332
- }
1333
-
1334
- .map-detail .detail-label {
1335
- color: #1CADE4;
1336
- background: rgba(0, 176, 80, 0.13);
1337
- }
1338
-
1339
- .map-detail .action-link {
1340
- color: #1CADE4;
1341
- border-color: rgba(0, 176, 80, 0.32);
1342
- }
1343
-
1344
- .statement-detail {
1345
- background: rgba(39, 206, 215, 0.1);
1346
- border-color: rgba(39, 206, 215, 0.35) !important;
1347
- }
1348
-
1349
- .statement-detail::before {
1350
- background: var(--info);
1351
- }
1352
-
1353
- .statement-detail .detail-label {
1354
- color: #1CADE4;
1355
- background: rgba(39, 206, 215, 0.16);
1356
- }
1357
-
1358
- .statement-detail .action-link {
1359
- color: #1CADE4;
1360
- border-color: rgba(39, 206, 215, 0.42);
1361
- }
1362
-
1363
- .action-link,
1364
- .secondary-button,
1365
- .clear-filters-button {
1366
- color: var(--primary-dark);
1367
- background: #ffffff;
1368
- border-color: rgba(39, 206, 215, 0.34);
1369
- }
1370
-
1371
- .clear-filters-button:hover {
1372
- color: #ffffff;
1373
- background: var(--danger);
1374
- border-color: var(--danger);
1375
- }
1376
-
1377
- .region-choice {
1378
- color: var(--primary-dark);
1379
- border-color: rgba(39, 206, 215, 0.28);
1380
- }
1381
-
1382
- .region-choice:hover {
1383
- background: rgba(39, 206, 215, 0.08);
1384
- border-color: var(--info);
1385
- }
1386
-
1387
- .region-choice strong,
1388
- .region-choice.active strong {
1389
- color: var(--primary-dark);
1390
- background: rgba(0, 176, 80, 0.14);
1391
- }
1392
-
1393
- .region-choice.active {
1394
- color: #ffffff;
1395
- }
1396
-
1397
- .region-choice.active strong {
1398
- background: #ffffff;
1399
- }
1400
-
1401
- .password-field input:focus,
1402
- .search-wrap input:focus,
1403
- .select-wrap select:focus {
1404
- border-color: var(--primary);
1405
- box-shadow: 0 0 0 3px rgba(65, 55, 168, 0.13);
1406
- }
1407
-
1408
- button:focus-visible,
1409
- a:focus-visible,
1410
- input:focus-visible,
1411
- select:focus-visible {
1412
- outline-color: rgba(65, 55, 168, 0.28);
1413
- }
1414
-
1415
- .toast {
1416
- background: var(--primary-dark);
1417
- }
 
1
+ @font-face {
2
+ font-family: "TheSansArabic";
3
+ src: url("Font/Bahij_TheSansArabic-SemiLight.ttf") format("truetype");
4
+ font-weight: 400 600;
5
+ font-display: swap;
6
+ }
7
+
8
+ @font-face {
9
+ font-family: "TheSansArabic";
10
+ src: url("Font/Bahij_TheSansArabic-SemiBold.ttf") format("truetype");
11
+ font-weight: 700 900;
12
+ font-display: swap;
13
+ }
14
+
15
  :root {
16
+ --primary: #4137a8;
17
+ --primary-2: #1cade4;
18
+ --green: #00b050;
19
+ --mint: #42ba97;
20
+ --cyan: #27ced7;
21
+ --purple: #7030a0;
22
+ --yellow: #ffc000;
23
+ --red: #f5544a;
24
+ --slate: #5c6e88;
25
+ --ink: #183348;
26
+ --muted: #687b8d;
27
+ --page: #f4f7f9;
28
+ --line: #dce6eb;
29
+ --white: #fff;
30
+ --shadow: 0 12px 34px rgba(39, 58, 74, 0.09);
31
+ --lift: 0 18px 42px rgba(39, 58, 74, 0.14);
32
  }
33
 
34
  * {
 
36
  }
37
 
38
  html {
39
+ scroll-behavior: smooth;
 
40
  }
41
 
42
  body {
43
+ min-width: 320px;
44
  margin: 0;
45
  color: var(--ink);
46
+ background:
47
+ linear-gradient(180deg, rgba(65, 55, 168, 0.08), transparent 260px),
48
+ var(--page);
49
+ font-family: "TheSansArabic", "Tahoma", sans-serif;
50
  }
51
 
52
  button,
 
55
  font: inherit;
56
  }
57
 
58
+ button,
59
+ a {
60
+ -webkit-tap-highlight-color: transparent;
61
  }
62
 
63
+ button {
64
+ cursor: pointer;
65
  }
66
 
67
  svg {
68
+ width: 21px;
69
+ height: 21px;
70
  fill: none;
71
  stroke: currentColor;
72
+ stroke-width: 1.8;
73
  stroke-linecap: round;
74
  stroke-linejoin: round;
 
75
  }
76
 
77
+ [hidden] {
78
+ display: none !important;
79
+ }
80
+
81
+ .center-view {
82
+ min-height: calc(100vh - 48px);
83
  display: grid;
84
  place-items: center;
85
+ padding: 34px 18px;
86
  }
87
 
88
+ .access-panel,
89
+ .researcher-panel {
90
+ width: min(100%, 520px);
91
  padding: 34px;
92
+ background: var(--white);
 
93
  border: 1px solid var(--line);
94
  border-radius: 8px;
95
  box-shadow: var(--shadow);
96
  }
97
 
98
+ .access-panel {
99
+ position: relative;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .access-panel::before {
104
+ content: "";
105
+ position: absolute;
106
+ inset: 0 0 auto;
107
+ height: 5px;
108
+ background: linear-gradient(90deg, var(--green), var(--primary), var(--primary-2));
109
+ }
110
+
111
  .brand-mark {
112
+ width: 62px;
113
+ height: 62px;
114
  display: grid;
115
  place-items: center;
116
+ color: white;
117
+ background: linear-gradient(145deg, var(--primary), var(--primary-2));
 
118
  border-radius: 8px;
119
+ box-shadow: 0 10px 24px rgba(65, 55, 168, 0.2);
120
  }
121
 
122
  .brand-mark svg {
 
127
  .brand-mark.small {
128
  width: 44px;
129
  height: 44px;
130
+ }
131
+
132
+ .brand-mark.small svg {
133
+ width: 25px;
134
+ height: 25px;
135
  }
136
 
137
  .eyebrow {
138
+ margin: 20px 0 5px;
139
+ color: var(--green);
140
+ font-size: 14px;
141
  font-weight: 800;
142
  }
143
 
 
145
  h2,
146
  h3,
147
  p {
148
+ margin-top: 0;
149
  }
150
 
151
+ .access-panel h1,
152
+ .panel-heading h1 {
153
+ margin-bottom: 10px;
154
+ font-size: 30px;
155
+ line-height: 1.45;
156
  }
157
 
158
+ .lead,
159
+ .panel-heading p {
160
  color: var(--muted);
161
  line-height: 1.8;
162
  }
163
 
164
+ .access-panel label,
165
+ .field-group > span,
166
+ .field-label {
167
  display: block;
168
+ margin: 20px 0 8px;
169
+ color: #344d60;
170
+ font-weight: 800;
171
  }
172
 
173
+ .input-shell,
174
+ .search-shell,
175
+ .select-shell {
176
  position: relative;
177
  }
178
 
179
+ .input-shell input,
180
+ .search-shell input,
181
+ .select-shell select {
182
  width: 100%;
183
+ min-height: 52px;
184
+ padding: 0 16px;
185
  color: var(--ink);
186
+ background: #fff;
187
+ border: 1px solid #cfdee5;
188
  border-radius: 8px;
189
  outline: none;
190
+ transition: border-color 0.18s, box-shadow 0.18s;
191
  }
192
 
193
+ .input-shell input {
194
+ padding-left: 52px;
195
  }
196
 
197
+ .search-shell input {
198
+ padding-right: 46px;
199
+ }
200
+
201
+ .select-shell select {
202
+ padding-left: 45px;
203
+ appearance: none;
204
+ }
205
+
206
+ .input-shell input:focus,
207
+ .search-shell input:focus,
208
+ .select-shell select:focus {
209
  border-color: var(--primary);
210
+ box-shadow: 0 0 0 3px rgba(65, 55, 168, 0.12);
211
  }
212
 
213
+ .input-shell .icon-button,
214
+ .search-shell > svg,
215
+ .select-shell > svg {
216
  position: absolute;
217
  top: 50%;
218
+ transform: translateY(-50%);
219
+ }
220
+
221
+ .input-shell .icon-button {
222
+ left: 5px;
223
+ }
224
+
225
+ .search-shell > svg {
226
+ right: 15px;
227
+ color: var(--slate);
228
+ pointer-events: none;
229
+ }
230
+
231
+ .select-shell > svg {
232
+ left: 15px;
233
+ color: var(--slate);
234
+ pointer-events: none;
235
+ }
236
+
237
+ .icon-button {
238
+ width: 42px;
239
+ height: 42px;
240
  display: grid;
241
  place-items: center;
242
+ color: var(--slate);
 
243
  background: transparent;
244
  border: 0;
245
  border-radius: 8px;
 
246
  }
247
 
248
  .icon-button:hover {
249
  color: var(--primary);
250
+ background: rgba(65, 55, 168, 0.07);
 
 
 
 
 
 
 
 
251
  }
252
 
253
  .primary-button,
254
+ .secondary-button,
255
+ .clear-button,
256
+ .mini-action {
257
+ min-height: 44px;
258
  display: inline-flex;
259
  align-items: center;
260
  justify-content: center;
261
  gap: 8px;
262
+ padding: 0 16px;
263
+ border: 1px solid transparent;
 
264
  border-radius: 8px;
265
+ font-weight: 800;
266
+ text-decoration: none;
267
+ transition: transform 0.18s, box-shadow 0.18s, color 0.18s, background 0.18s, border-color 0.18s;
268
  }
269
 
270
  .primary-button {
271
  width: 100%;
272
+ margin-top: 12px;
273
+ color: white;
274
+ background: linear-gradient(145deg, var(--primary), var(--primary-2));
275
+ box-shadow: 0 10px 22px rgba(65, 55, 168, 0.18);
276
+ }
277
+
278
+ .primary-button:hover,
279
+ .tool-action:hover,
280
+ .researcher-choice:hover {
281
+ transform: translateY(-2px);
282
  }
283
 
284
+ .form-error {
285
+ min-height: 22px;
286
+ margin: 8px 0 0;
287
+ color: var(--red);
288
+ font-size: 14px;
289
  }
290
 
291
+ .researcher-panel {
292
+ width: min(100%, 920px);
 
293
  }
294
 
295
+ .panel-heading {
296
+ text-align: center;
 
 
297
  }
298
 
299
+ .panel-heading .eyebrow {
300
+ margin-top: 0;
301
  }
302
 
303
+ .researcher-panel > .search-shell {
304
+ max-width: 520px;
305
+ margin: 24px auto;
 
 
 
 
 
 
306
  }
307
 
308
+ .researcher-grid {
309
+ display: grid;
310
+ grid-template-columns: repeat(2, minmax(0, 1fr));
311
+ gap: 10px;
312
+ max-height: 58vh;
313
+ overflow: auto;
314
+ padding: 2px;
315
+ }
316
+
317
+ .researcher-choice {
318
+ min-height: 70px;
319
  display: flex;
320
  align-items: center;
321
+ justify-content: space-between;
322
+ gap: 10px;
323
+ padding: 12px 15px;
324
+ color: var(--ink);
325
+ text-align: right;
326
+ background: #fbfdfe;
327
+ border: 1px solid var(--line);
328
+ border-radius: 8px;
329
+ transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
330
  }
331
 
332
+ .researcher-choice:hover {
333
+ border-color: var(--cyan);
334
+ box-shadow: 0 8px 18px rgba(39, 206, 215, 0.11);
335
  }
336
 
337
+ .researcher-choice span {
338
+ min-width: 0;
 
 
339
  }
340
 
341
+ .researcher-choice strong,
342
+ .researcher-choice small {
343
+ display: block;
344
  }
345
 
346
+ .researcher-choice strong {
347
+ line-height: 1.55;
 
348
  }
349
 
350
+ .researcher-choice small {
351
+ margin-top: 3px;
352
+ color: var(--green);
353
  }
354
 
355
+ .researcher-choice > svg {
356
+ flex: 0 0 auto;
357
+ color: var(--primary);
 
 
 
 
 
 
358
  }
359
 
360
+ .topbar {
361
+ position: sticky;
362
+ top: 0;
363
+ z-index: 30;
364
+ min-height: 72px;
365
  display: flex;
366
  align-items: center;
367
+ justify-content: space-between;
368
+ gap: 20px;
369
+ padding: 12px max(24px, calc((100% - 1280px) / 2));
370
+ background: rgba(255, 255, 255, 0.94);
371
+ border-bottom: 1px solid rgba(92, 110, 136, 0.17);
372
+ backdrop-filter: blur(14px);
373
  }
374
 
375
+ .topbar-brand,
376
+ .topbar-actions {
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 12px;
380
  }
381
 
382
+ .topbar-brand p {
383
+ margin: 0 0 1px;
384
+ color: var(--green);
385
+ font-size: 12px;
386
  font-weight: 800;
 
 
 
387
  }
388
 
389
+ .topbar-brand h1 {
390
+ margin: 0;
391
+ font-size: 17px;
392
+ line-height: 1.5;
393
  }
394
 
395
+ .secondary-button {
396
+ color: var(--primary);
397
+ background: #fff;
398
+ border-color: rgba(65, 55, 168, 0.25);
399
  }
400
 
401
+ .secondary-button:hover,
402
+ .mini-action:hover {
403
+ color: white;
404
+ background: var(--primary);
405
+ border-color: var(--primary);
406
  }
407
 
408
+ .topbar-icon {
409
+ border: 1px solid var(--line);
 
 
 
 
 
410
  }
411
 
412
+ .dashboard-shell {
413
+ width: min(1280px, calc(100% - 32px));
414
+ margin: 22px auto 45px;
415
  }
416
 
417
+ .workspace-header {
418
+ display: flex;
419
+ align-items: center;
420
+ justify-content: space-between;
421
+ gap: 28px;
422
+ padding: 22px 24px;
423
+ background: white;
424
+ border: 1px solid var(--line);
425
+ border-right: 5px solid var(--primary);
426
+ border-radius: 8px;
427
+ box-shadow: var(--shadow);
428
  }
429
 
430
+ .workspace-header .eyebrow {
431
+ margin-top: 0;
 
 
432
  }
433
 
434
+ .workspace-header h2 {
435
+ max-width: 650px;
436
+ margin-bottom: 0;
437
+ font-size: 25px;
438
  }
439
 
440
+ .summary-stats {
441
+ display: grid;
442
+ grid-template-columns: repeat(4, minmax(90px, 1fr));
443
+ gap: 8px;
444
+ }
445
+
446
+ .stat-item {
447
+ min-width: 100px;
448
+ padding: 10px;
449
+ text-align: center;
450
  background: #f8fafb;
451
  border: 1px solid var(--line);
452
  border-radius: 8px;
453
  }
454
 
455
+ .stat-item strong,
456
+ .stat-item span {
457
+ display: block;
 
 
 
 
 
 
 
 
458
  }
459
 
460
+ .stat-item strong {
461
+ color: var(--primary);
462
+ font-size: 23px;
 
 
463
  }
464
 
465
+ .stat-item span {
 
 
466
  color: var(--muted);
467
+ font-size: 12px;
468
+ line-height: 1.5;
469
  }
470
 
471
+ .stat-item.madon strong {
472
+ color: var(--green);
 
 
 
 
 
 
 
 
473
  }
474
 
475
+ .stat-item.base strong {
476
+ color: var(--primary-2);
 
 
 
477
  }
478
 
479
+ .stat-item.missing strong {
480
+ color: var(--slate);
 
 
481
  }
482
 
483
+ .quick-actions {
484
  display: grid;
485
+ grid-template-columns: repeat(3, minmax(0, 1fr));
486
+ gap: 12px;
487
+ margin-top: 14px;
 
 
 
488
  }
489
 
490
+ .tool-action {
491
+ min-height: 88px;
492
  display: flex;
493
+ align-items: center;
494
+ gap: 13px;
495
+ padding: 14px 16px;
496
+ color: var(--ink);
497
+ text-align: right;
498
+ background: white;
499
+ border: 1px solid var(--line);
500
+ border-radius: 8px;
501
+ box-shadow: 0 8px 24px rgba(39, 58, 74, 0.06);
502
+ text-decoration: none;
503
+ transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
504
  }
505
 
506
+ button.tool-action {
507
+ width: 100%;
 
 
 
 
508
  }
509
 
510
+ .tool-action:hover {
511
+ border-color: var(--cyan);
512
+ box-shadow: var(--lift);
 
 
513
  }
514
 
515
+ .tool-icon {
516
+ width: 48px;
517
+ height: 48px;
518
  flex: 0 0 auto;
519
+ display: grid;
520
+ place-items: center;
521
+ color: white;
522
+ background: var(--primary);
 
 
 
 
 
523
  border-radius: 8px;
524
  }
525
 
526
+ .form-action .tool-icon {
527
+ background: var(--green);
 
 
 
528
  }
529
 
530
+ .share-action .tool-icon {
531
+ background: var(--mint);
 
 
532
  }
533
 
534
+ .tool-action > span:nth-child(2) {
535
+ min-width: 0;
536
+ flex: 1;
537
  }
538
 
539
+ .tool-action strong,
540
+ .tool-action small {
541
+ display: block;
542
  }
543
 
544
+ .tool-action small {
545
+ margin-top: 3px;
 
546
  color: var(--muted);
547
  font-size: 12px;
 
 
 
 
 
 
 
 
548
  }
549
 
550
+ .action-arrow {
551
+ flex: 0 0 auto;
552
+ color: var(--slate);
 
 
 
553
  }
554
 
555
+ .controls-panel {
556
+ margin-top: 14px;
557
+ padding: 20px;
558
+ background: white;
559
+ border: 1px solid var(--line);
560
  border-radius: 8px;
561
+ box-shadow: var(--shadow);
562
  }
563
 
564
+ .city-section .field-label {
565
+ margin-top: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  }
567
 
568
+ .city-buttons {
569
+ display: flex;
570
+ flex-wrap: wrap;
571
+ gap: 8px;
572
  }
573
 
574
+ .city-choice {
575
+ min-height: 44px;
576
+ display: flex;
577
  align-items: center;
578
+ gap: 10px;
579
+ padding: 7px 12px;
580
+ color: var(--ink);
581
+ background: #f9fbfc;
582
+ border: 1px solid var(--line);
583
  border-radius: 8px;
584
+ font-weight: 800;
585
  }
586
 
587
+ .city-choice strong {
588
+ min-width: 29px;
589
+ padding: 2px 7px;
590
+ color: var(--green);
591
+ text-align: center;
592
+ background: rgba(0, 176, 80, 0.1);
593
+ border-radius: 6px;
 
 
 
 
 
 
 
594
  }
595
 
596
+ .city-choice:hover {
597
+ border-color: var(--cyan);
 
 
598
  }
599
 
600
+ .city-choice.active {
601
+ color: white;
602
+ background: linear-gradient(145deg, var(--primary), var(--primary-2));
603
+ border-color: transparent;
 
604
  }
605
 
606
+ .city-choice.active strong {
607
+ color: var(--primary);
608
+ background: white;
609
  }
610
 
611
+ .filters-grid {
612
+ display: grid;
613
+ grid-template-columns: minmax(280px, 1.5fr) minmax(180px, 1fr) minmax(180px, 1fr) auto;
614
+ align-items: end;
615
+ gap: 10px;
 
 
 
 
 
 
 
 
616
  }
617
 
618
+ .field-group > span {
619
+ margin-top: 16px;
620
  }
621
 
622
+ .clear-button {
623
+ color: var(--slate);
624
+ background: #fff;
625
+ border-color: var(--line);
626
  }
627
 
628
+ .clear-button:hover {
629
+ color: white;
630
+ background: var(--red);
631
+ border-color: var(--red);
 
 
632
  }
633
 
634
+ .representative-panel {
635
  display: flex;
636
+ align-items: center;
637
+ justify-content: space-between;
638
+ gap: 20px;
639
+ margin-top: 14px;
640
+ padding: 16px 18px;
641
+ background: rgba(39, 206, 215, 0.08);
642
+ border: 1px solid rgba(39, 206, 215, 0.36);
643
+ border-right: 5px solid var(--cyan);
644
+ border-radius: 8px;
645
  }
646
 
647
+ .representative-copy,
648
+ .representative-actions {
649
  display: flex;
 
650
  align-items: center;
651
+ gap: 10px;
 
652
  }
653
 
654
+ .representative-icon {
655
+ width: 44px;
656
+ height: 44px;
657
+ display: grid;
658
+ place-items: center;
659
+ color: white;
660
+ background: var(--cyan);
661
+ border-radius: 8px;
662
  }
663
 
664
+ .representative-copy small,
665
+ .representative-copy strong,
666
+ .representative-copy div > span {
667
+ display: block;
 
668
  }
669
 
670
+ .representative-copy small {
 
 
 
 
671
  color: var(--muted);
 
 
672
  }
673
 
674
+ .representative-copy div > span {
675
+ direction: ltr;
676
+ text-align: right;
677
+ color: var(--primary);
 
 
 
678
  font-weight: 800;
 
 
 
 
 
 
 
679
  }
680
 
681
+ .mini-action {
682
+ color: var(--primary);
683
+ background: white;
684
+ border-color: rgba(65, 55, 168, 0.22);
685
  }
686
 
687
+ .results-section {
688
+ margin-top: 24px;
 
 
 
 
 
 
689
  }
690
 
691
+ .results-header {
692
+ display: flex;
693
+ align-items: end;
694
+ justify-content: space-between;
695
+ margin-bottom: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  }
697
 
698
+ .results-header h2 {
699
+ margin: 0;
700
+ font-size: 23px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  }
702
 
703
+ .results-meta {
704
+ margin-bottom: 3px;
705
+ color: var(--green);
706
+ font-size: 14px;
707
+ font-weight: 800;
708
  }
709
 
710
+ .samples-grid {
711
+ display: grid;
712
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
713
+ gap: 13px;
714
  }
715
 
716
+ .sample-card {
717
+ min-width: 0;
718
+ display: flex;
719
+ flex-direction: column;
720
+ overflow: hidden;
721
+ background: white;
722
+ border: 1px solid var(--line);
723
+ border-radius: 8px;
724
+ box-shadow: 0 8px 24px rgba(39, 58, 74, 0.07);
725
+ transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
726
  }
727
 
728
+ .sample-card:hover {
729
+ transform: translateY(-2px);
730
+ border-color: #c5d8e1;
731
+ box-shadow: var(--lift);
 
 
732
  }
733
 
734
+ .card-header {
735
+ display: flex;
736
+ align-items: flex-start;
737
+ justify-content: space-between;
738
+ gap: 12px;
739
+ padding: 16px;
740
+ background: linear-gradient(180deg, #f9fcfd, white);
741
+ border-right: 4px solid var(--green);
742
+ border-bottom: 1px solid #e7eef2;
743
  }
744
 
745
+ .card-header > div {
746
+ min-width: 0;
 
 
 
 
747
  }
748
 
749
+ .sample-index {
750
+ display: block;
751
+ margin-bottom: 2px;
752
+ color: var(--green);
753
+ font-size: 12px;
754
+ font-weight: 800;
755
  }
756
 
757
+ .card-header h3 {
758
+ margin: 0;
759
+ font-size: 17px;
760
+ line-height: 1.65;
 
 
761
  }
762
 
763
+ .status-badge {
764
+ flex: 0 0 auto;
765
+ max-width: 135px;
766
+ padding: 5px 9px;
767
+ color: var(--slate);
768
+ text-align: center;
769
+ background: rgba(255, 192, 0, 0.13);
770
+ border: 1px solid rgba(255, 192, 0, 0.55);
771
+ border-radius: 7px;
772
+ font-size: 12px;
773
+ font-weight: 800;
774
+ line-height: 1.5;
775
  }
776
 
777
+ .card-details {
778
+ display: grid;
779
+ grid-template-columns: repeat(2, minmax(0, 1fr));
780
+ padding: 2px 16px 12px;
781
  }
782
 
783
+ .detail-item {
784
+ min-width: 0;
785
+ padding: 12px 0;
786
+ border-bottom: 1px solid #e8eff2;
 
 
 
787
  }
788
 
789
+ .detail-item:nth-child(odd):not(.wide) {
790
+ padding-left: 12px;
791
+ border-left: 1px solid #e8eff2;
 
 
 
 
792
  }
793
 
794
+ .detail-item:nth-child(even):not(.wide) {
795
+ padding-right: 12px;
 
796
  }
797
 
798
+ .detail-item.wide,
799
+ .location-block {
800
+ grid-column: 1 / -1;
 
 
 
 
801
  }
802
 
803
+ .detail-label,
804
+ .detail-value {
805
+ display: block;
 
806
  }
807
 
808
+ .detail-label {
809
+ margin-bottom: 4px;
810
+ color: var(--muted);
811
+ font-size: 12px;
 
 
 
 
 
 
 
 
812
  font-weight: 800;
 
813
  }
814
 
815
+ .detail-value {
816
+ overflow-wrap: anywhere;
817
+ font-weight: 800;
818
+ line-height: 1.65;
819
  }
820
 
821
+ .ltr-value .detail-value {
822
+ direction: ltr;
823
+ text-align: right;
 
 
824
  }
825
 
826
+ .location-block {
827
+ margin-top: 12px;
828
+ padding: 12px;
829
+ background: rgba(0, 176, 80, 0.07);
830
+ border: 1px solid rgba(0, 176, 80, 0.28);
 
 
831
  border-radius: 8px;
832
  }
833
 
834
+ .location-base {
835
+ background: rgba(28, 173, 228, 0.07);
836
+ border-color: rgba(28, 173, 228, 0.28);
 
 
837
  }
838
 
839
+ .location-statement {
840
+ background: rgba(39, 206, 215, 0.08);
841
+ border-color: rgba(39, 206, 215, 0.35);
842
  }
843
 
844
+ .location-heading {
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: space-between;
848
+ gap: 10px;
849
+ margin-bottom: 9px;
850
  }
851
 
852
+ .location-heading span,
853
+ .madon-statement > span {
854
+ color: var(--green);
855
+ font-size: 13px;
856
+ font-weight: 900;
857
  }
858
 
859
+ .location-base .location-heading span {
860
+ color: var(--primary-2);
 
 
861
  }
862
 
863
+ .location-heading small {
864
+ color: var(--muted);
865
  }
866
 
867
+ .location-block > .mini-action {
868
+ width: 100%;
869
  }
870
 
871
+ .madon-statement {
872
+ margin-top: 9px;
873
+ padding-top: 9px;
874
+ border-top: 1px solid rgba(92, 110, 136, 0.16);
875
  }
876
 
877
+ .madon-statement p {
878
+ margin: 3px 0 0;
879
+ line-height: 1.7;
880
+ }
881
+
882
+ .card-actions {
883
  display: flex;
884
+ flex-wrap: wrap;
885
+ gap: 8px;
886
+ margin-top: auto;
887
+ padding: 12px 16px 16px;
888
  }
889
 
890
+ .card-actions .mini-action {
891
+ flex: 1 1 155px;
 
 
892
  }
893
 
894
+ .load-more-wrap {
895
+ display: grid;
896
+ place-items: center;
897
+ gap: 5px;
898
+ margin-top: 18px;
899
  }
900
 
901
+ .load-more-wrap p {
902
+ margin: 0;
903
+ color: var(--muted);
904
+ font-size: 13px;
905
  }
906
 
907
+ .empty-state {
908
+ padding: 38px 18px;
909
+ text-align: center;
910
+ background: white;
911
+ border: 1px dashed #cbdbe3;
912
+ border-radius: 8px;
913
  }
914
 
915
+ .empty-state h3 {
916
+ margin-bottom: 6px;
917
  }
918
 
919
+ .empty-state p {
920
+ margin-bottom: 0;
921
+ color: var(--muted);
922
  }
923
 
924
+ footer {
925
+ min-height: 48px;
926
+ display: grid;
927
+ place-items: center;
928
+ color: var(--slate);
929
+ font-size: 13px;
930
  }
931
 
932
+ .toast {
933
+ position: fixed;
934
+ right: 22px;
935
+ bottom: 22px;
936
+ z-index: 50;
937
+ max-width: calc(100% - 44px);
938
+ padding: 11px 16px;
939
+ color: white;
940
+ background: var(--primary);
941
+ border-radius: 8px;
942
+ box-shadow: var(--lift);
943
+ opacity: 0;
944
+ pointer-events: none;
945
+ transform: translateY(16px);
946
+ transition: opacity 0.18s, transform 0.18s;
947
+ }
948
 
949
+ .toast.show {
950
+ opacity: 1;
951
+ transform: translateY(0);
952
+ }
953
+
954
+ button:focus-visible,
955
+ a:focus-visible,
956
+ input:focus-visible,
957
+ select:focus-visible {
958
+ outline: 3px solid rgba(65, 55, 168, 0.2);
959
+ outline-offset: 2px;
960
  }
961
 
962
+ @media (max-width: 1050px) {
963
+ .workspace-header {
964
+ align-items: flex-start;
965
  flex-direction: column;
966
  }
967
 
968
+ .summary-stats {
969
+ width: 100%;
970
  }
 
971
 
972
+ .filters-grid {
973
+ grid-template-columns: repeat(2, minmax(0, 1fr));
 
974
  }
975
+ }
976
 
977
+ @media (max-width: 760px) {
978
  .topbar {
979
+ padding: 10px 14px;
980
  }
981
 
982
+ .topbar-brand .brand-mark {
983
+ display: none;
 
984
  }
985
 
986
+ .topbar-brand h1 {
987
+ max-width: 200px;
988
+ overflow: hidden;
989
+ text-overflow: ellipsis;
990
+ white-space: nowrap;
991
  }
992
 
993
+ .secondary-button.compact {
994
+ width: 44px;
995
+ padding: 0;
996
+ font-size: 0;
997
  }
998
 
999
+ .quick-actions {
1000
+ grid-template-columns: 1fr;
1001
  }
1002
 
1003
+ .tool-action {
1004
+ min-height: 76px;
1005
  }
1006
 
1007
+ .summary-stats {
1008
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1009
  }
1010
 
1011
+ .filters-grid {
1012
  grid-template-columns: 1fr;
 
1013
  }
1014
 
1015
+ .clear-button {
 
 
 
 
 
 
1016
  width: 100%;
 
1017
  }
1018
 
1019
+ .representative-panel {
1020
+ align-items: stretch;
1021
+ flex-direction: column;
1022
  }
1023
 
1024
+ .representative-actions .mini-action {
1025
+ flex: 1;
1026
  }
1027
 
1028
+ .samples-grid {
1029
+ grid-template-columns: 1fr;
1030
  }
1031
+ }
1032
 
1033
+ @media (max-width: 560px) {
1034
+ .center-view {
1035
+ align-items: start;
1036
+ padding: 18px 10px;
1037
  }
1038
 
1039
+ .access-panel,
1040
+ .researcher-panel {
1041
+ padding: 25px 18px;
 
 
 
1042
  }
1043
 
1044
+ .access-panel h1,
1045
+ .panel-heading h1 {
1046
+ font-size: 24px;
1047
  }
1048
 
1049
+ .researcher-grid {
1050
+ grid-template-columns: 1fr;
1051
+ max-height: none;
1052
  }
1053
 
1054
+ .dashboard-shell {
1055
+ width: calc(100% - 18px);
1056
+ margin-top: 12px;
 
1057
  }
1058
 
1059
+ .workspace-header,
1060
+ .controls-panel {
1061
  padding: 15px;
 
 
 
 
 
 
1062
  }
1063
 
1064
+ .workspace-header h2 {
1065
+ font-size: 20px;
1066
+ line-height: 1.6;
1067
  }
1068
 
1069
+ .stat-item {
1070
+ min-width: 0;
1071
  }
1072
 
1073
+ .city-buttons {
1074
+ display: grid;
1075
+ grid-template-columns: 1fr;
1076
  }
1077
 
1078
+ .city-choice {
1079
+ justify-content: space-between;
1080
  }
1081
 
1082
+ .card-header {
1083
+ align-items: stretch;
1084
+ flex-direction: column;
 
 
 
1085
  }
1086
 
1087
+ .status-badge {
1088
+ align-self: flex-start;
1089
+ max-width: 100%;
1090
  }
 
1091
 
1092
+ .card-details {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1093
  grid-template-columns: 1fr;
1094
  }
1095
 
1096
+ .detail-item:nth-child(odd):not(.wide),
1097
+ .detail-item:nth-child(even):not(.wide) {
1098
+ padding-right: 0;
1099
+ padding-left: 0;
1100
+ border-left: 0;
1101
  }
 
1102
 
1103
+ .detail-item {
1104
+ grid-column: 1 / -1;
 
1105
  }
1106
 
1107
+ .location-heading {
1108
+ align-items: flex-start;
1109
+ flex-direction: column;
1110
  }
1111
 
1112
+ .toast {
1113
+ right: 10px;
1114
+ left: 10px;
1115
+ bottom: 12px;
1116
+ max-width: none;
1117
+ text-align: center;
1118
  }
1119
  }