stat2025 commited on
Commit
ab3bd11
·
verified ·
1 Parent(s): fca4cf3

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +24 -9
  2. app.js +182 -398
  3. data.js +0 -0
  4. generate-data.mjs +61 -23
  5. index.html +52 -121
  6. style.css +663 -559
README.md CHANGED
@@ -1,24 +1,39 @@
1
  ---
2
- title: DE
3
- emoji: "\U0001F5C2"
4
- colorFrom: blue
5
- colorTo: green
6
  sdk: static
7
  pinned: false
8
  license: mit
9
- short_description: نظام متابعة العينات
10
  ---
11
 
12
- # نظام متابعة العينات
13
 
14
- واجهة عربية للاستعلام عن العينات وبيانات التواصل حسب المشرف والباحث.
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  ## تحديث البيانات
17
 
18
- يشفر الأمر التالي بيانات ملف Excel وينشئ ملف `data.js` مستقلًا:
 
 
19
 
20
  ```powershell
21
  node generate-data.mjs "path\to\input.xlsx" "data.js" "PASSWORD"
22
  ```
23
 
24
- لا ترفع ملف Excel الأصلي إلى المستودع.
 
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
+ رمز الدخول الافتراضي في النسخة الحالية هو `1234`.
32
+
33
+ لتحديث ملف البيانات المشفر:
34
 
35
  ```powershell
36
  node generate-data.mjs "path\to\input.xlsx" "data.js" "PASSWORD"
37
  ```
38
 
39
+ لا ترفع ملف Excel الأصلي إلى المستودع؛ ارفع `data.js` بعد توليده فقط.
app.js CHANGED
@@ -1,49 +1,5 @@
1
  "use strict";
2
 
3
- const FIELD_LABELS = [
4
- "اسم المشرف",
5
- "اسم الباحث",
6
- "رقم السجل(العينة)",
7
- "اسم العينة",
8
- "حالة الاكتمال",
9
- "حالة الاقرار",
10
- "حالة الاستيفاء",
11
- "نوع الاستيفاء",
12
- "حالة الاعتماد",
13
- "النشاط",
14
- "الايميل",
15
- "الموقع",
16
- "رقم التواصل",
17
- ];
18
-
19
- const FIELD_KEYS = [
20
- "supervisor",
21
- "researcher",
22
- "recordNumber",
23
- "sampleName",
24
- "completionStatus",
25
- "declarationStatus",
26
- "fulfillmentStatus",
27
- "fulfillmentType",
28
- "approvalStatus",
29
- "activity",
30
- "email",
31
- "location",
32
- "phone",
33
- ];
34
-
35
- const DISPLAY_FIELDS = [
36
- ["recordNumber", "رقم السجل", false],
37
- ["completionStatus", "حالة الاكتمال", false],
38
- ["declarationStatus", "حالة الإقرار", false],
39
- ["fulfillmentType", "نوع الاستيفاء", false],
40
- ["approvalStatus", "حالة الاعتماد", false],
41
- ["activity", "النشاط", true],
42
- ["email", "البريد الإلكتروني", true],
43
- ["location", "الموقع", true],
44
- ["phone", "رقم التواصل", true],
45
- ];
46
-
47
  const loginView = document.getElementById("loginView");
48
  const dashboardView = document.getElementById("dashboardView");
49
  const loginForm = document.getElementById("loginForm");
@@ -52,36 +8,31 @@ const loginButton = document.getElementById("loginButton");
52
  const loginError = document.getElementById("loginError");
53
  const togglePassword = document.getElementById("togglePassword");
54
  const logoutButton = document.getElementById("logoutButton");
55
- const supervisorSelect = document.getElementById("supervisorSelect");
56
- const researcherSelect = document.getElementById("researcherSelect");
57
- const statusSelect = document.getElementById("statusSelect");
58
  const searchInput = document.getElementById("searchInput");
 
59
  const emptyState = document.getElementById("emptyState");
60
  const resultsSection = document.getElementById("resultsSection");
61
  const resultsMeta = document.getElementById("resultsMeta");
62
  const resultsTitle = document.getElementById("resultsTitle");
63
- const statusChips = document.getElementById("statusChips");
64
  const samplesGrid = document.getElementById("samplesGrid");
65
  const noResults = document.getElementById("noResults");
66
- const copyAllButton = document.getElementById("copyAllButton");
67
  const loadMoreWrap = document.getElementById("loadMoreWrap");
68
  const loadMoreButton = document.getElementById("loadMoreButton");
69
  const loadMoreMeta = document.getElementById("loadMoreMeta");
70
- const clearFiltersButton = document.getElementById("clearFiltersButton");
 
71
  const toast = document.getElementById("toast");
72
 
73
- let allRows = [];
 
 
 
 
74
  let visibleRows = [];
75
- let failedAttempts = 0;
76
- let toastTimer;
77
- let inactivityTimer;
78
- let inactivityWarningTimer;
79
  let renderedCount = 0;
80
- const ALL_SUPERVISORS_VALUE = "__all_supervisors__";
81
- const PAGE_SIZE = 24;
82
- const INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000;
83
- const INACTIVITY_WARNING_MS = 29 * 60 * 1000;
84
- const ACTIVITY_EVENTS = ["click", "keydown", "pointerdown", "touchstart", "scroll"];
85
 
86
  function base64ToBytes(value) {
87
  const binary = atob(value);
@@ -96,14 +47,8 @@ async function deriveKey(password, salt) {
96
  false,
97
  ["deriveKey"],
98
  );
99
-
100
  return crypto.subtle.deriveKey(
101
- {
102
- name: "PBKDF2",
103
- salt,
104
- iterations: ENCRYPTED_DATA.iterations,
105
- hash: "SHA-256",
106
- },
107
  material,
108
  { name: "AES-GCM", length: 256 },
109
  false,
@@ -132,41 +77,22 @@ function normalize(value) {
132
  .toLowerCase();
133
  }
134
 
135
- function formatPhone(value) {
136
- const raw = String(value ?? "").trim();
137
- if (!raw) return "";
138
- const digits = raw.replace(/\D/g, "");
139
- if (!digits) return raw;
140
- if (digits.startsWith("0")) return digits;
141
- if (digits.startsWith("966")) return `0${digits.slice(3)}`;
142
- return `0${digits}`;
143
- }
144
-
145
- function rowToObject(row) {
146
- const item = Object.fromEntries(FIELD_KEYS.map((key, index) => [key, row[index] ?? ""]));
147
- item.phone = formatPhone(item.phone);
148
- return item;
149
- }
150
-
151
  function uniqueSorted(values) {
152
  return [...new Set(values.filter(Boolean))].sort((a, b) =>
153
  String(a).localeCompare(String(b), "ar", { sensitivity: "base" }),
154
  );
155
  }
156
 
157
- function setOptions(select, values, placeholder, counts = null) {
158
  select.replaceChildren(new Option(placeholder, ""));
159
- values.forEach((value) => {
160
- const label = counts ? `${value} (${counts.get(value) || 0})` : value;
161
- select.add(new Option(label, value));
162
- });
163
  }
164
 
165
  function showToast(message) {
166
  clearTimeout(toastTimer);
167
  toast.textContent = message;
168
  toast.classList.add("show");
169
- toastTimer = setTimeout(() => toast.classList.remove("show"), 2300);
170
  }
171
 
172
  async function copyText(text, successMessage) {
@@ -185,119 +111,131 @@ async function copyText(text, successMessage) {
185
  showToast(successMessage);
186
  }
187
 
188
- function buildMessage(row, index = null) {
189
- const title = index === null ? "بيانات العينة للتواصل" : `العينة رقم (${index})`;
190
- const sampleLines = [
191
- title,
192
- "────────────────────",
193
- row.sampleName && `اسم العينة: ${row.sampleName}`,
194
- row.recordNumber && `رقم السجل: ${row.recordNumber}`,
195
- row.fulfillmentStatus && `حالة الاستيفاء: ${row.fulfillmentStatus}`,
196
- row.activity && `النشاط: ${row.activity}`,
197
- ].filter(Boolean);
198
- const contactLines = [
199
- "بيانات التواصل",
200
- "────────────────────",
201
- row.phone && `رقم التواصل: ${formatPhone(row.phone)}`,
202
- row.email && `البريد الإلكتروني: ${row.email}`,
203
- row.location && `الموقع: ${row.location}`,
204
- ].filter(Boolean);
205
- const sections = [];
206
-
207
- if (index === null) {
208
- sections.push(
209
- "السلام عليكم ورحمة الله وبركاته،",
210
- "نأمل التكرم بالتواصل مع العينة التالية واستكمال الإجراء اللازم:",
211
- );
212
- }
213
 
214
- sections.push(sampleLines.join("\n"));
215
- if (contactLines.length > 2) sections.push(contactLines.join("\n"));
216
- if (index === null) sections.push("شاكرين لكم تعاونكم.");
217
- return sections.join("\n\n");
 
 
 
 
218
  }
219
 
220
- function createDetail(key, label, isContact, value) {
221
- const wrapper = document.createElement("div");
222
- wrapper.className = `detail-item${isContact ? " contact-detail" : ""}`;
223
-
224
- const labelElement = document.createElement("span");
225
- labelElement.className = "detail-label";
226
- labelElement.textContent = label;
227
-
228
- const valueRow = document.createElement("div");
229
- valueRow.className = "detail-value-row";
230
- const valueElement = document.createElement("span");
231
- valueElement.className = "detail-value";
232
- valueElement.textContent = String(value);
233
- if (key === "phone" || key === "recordNumber") {
234
- valueElement.dir = "ltr";
235
- valueElement.style.textAlign = "right";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
 
238
- valueRow.append(valueElement);
239
- if (key === "phone" || key === "email") {
240
- const copyFieldButton = document.createElement("button");
241
- copyFieldButton.type = "button";
242
- copyFieldButton.className = "copy-field-button";
243
- copyFieldButton.setAttribute("aria-label", key === "phone" ? "نسخ رقم التواصل" : "نسخ البريد الإلكتروني");
244
- copyFieldButton.title = key === "phone" ? "نسخ رقم التواصل" : "نسخ البريد الإلكتروني";
245
- copyFieldButton.innerHTML = `
246
- <svg viewBox="0 0 24 24" aria-hidden="true">
247
- <rect x="8" y="8" width="11" height="11" rx="2"></rect>
248
- <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
249
- </svg>
250
- `;
251
- copyFieldButton.addEventListener("click", () =>
252
- copyText(String(value), key === "phone" ? "تم نسخ رقم التواصل" : "تم نسخ البريد الإلكتروني"),
253
  );
254
- valueRow.append(copyFieldButton);
 
 
 
 
 
 
255
  }
256
 
257
- wrapper.append(labelElement, valueRow);
258
- return wrapper;
259
  }
260
 
261
- function createSampleCard(row, index) {
262
  const card = document.createElement("article");
263
  card.className = "sample-card";
264
 
265
  const header = document.createElement("div");
266
  header.className = "sample-card-header";
267
-
268
- const headingWrap = document.createElement("div");
269
- const number = document.createElement("span");
270
- number.className = "sample-index";
271
- number.textContent = `العينة ${index + 1}`;
272
  const title = document.createElement("h3");
273
- title.textContent = row.sampleName || "عينة دون اسم";
274
- headingWrap.append(number, title);
275
-
276
  const badge = document.createElement("span");
277
  badge.className = "status-badge";
278
- badge.textContent = row.fulfillmentStatus || "غير محدد";
279
- header.append(headingWrap, badge);
280
 
281
  const details = document.createElement("div");
282
  details.className = "sample-details";
283
- DISPLAY_FIELDS.forEach(([key, label, isContact]) => {
284
- if (String(row[key] ?? "").trim()) {
285
- details.append(createDetail(key, label, isContact, row[key]));
286
- }
 
 
287
  });
 
 
288
 
289
- const footer = document.createElement("div");
290
- footer.className = "sample-card-footer";
291
- const copyButton = document.createElement("button");
292
- copyButton.className = "copy-sample";
293
- copyButton.type = "button";
294
- copyButton.textContent = "نسخ رسالة هذه العينة";
295
- copyButton.addEventListener("click", () =>
296
- copyText(buildMessage(row), "تم نسخ رسالة العينة"),
297
- );
298
- footer.append(copyButton);
299
-
300
- card.append(header, details, footer);
301
  return card;
302
  }
303
 
@@ -306,204 +244,103 @@ function renderVisibleRows(reset = false) {
306
  renderedCount = 0;
307
  samplesGrid.replaceChildren();
308
  }
309
-
310
- const nextRows = visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE);
311
  const fragment = document.createDocumentFragment();
312
- nextRows.forEach((row, offset) => {
313
- fragment.append(createSampleCard(row, renderedCount + offset));
314
  });
315
  samplesGrid.append(fragment);
316
- renderedCount += nextRows.length;
317
-
318
  const remaining = visibleRows.length - renderedCount;
319
  loadMoreWrap.hidden = remaining <= 0;
320
  loadMoreButton.textContent = `عرض المزيد (${Math.min(PAGE_SIZE, remaining)})`;
321
- loadMoreMeta.textContent = `تم عرض ${renderedCount} من أصل ${visibleRows.length} عينة`;
322
- }
323
-
324
- function renderStatusChips(rows) {
325
- const counts = new Map();
326
- rows.forEach((row) => {
327
- const status = row.fulfillmentStatus || "غير محدد";
328
- counts.set(status, (counts.get(status) || 0) + 1);
329
- });
330
-
331
- const createChip = (label, value, count) => {
332
- const chip = document.createElement("button");
333
- chip.type = "button";
334
- chip.className = `status-chip${statusSelect.value === value ? " active" : ""}`;
335
- chip.setAttribute("aria-pressed", String(statusSelect.value === value));
336
- chip.append(document.createTextNode(label));
337
-
338
- const strong = document.createElement("strong");
339
- strong.textContent = count;
340
- chip.append(strong);
341
- chip.addEventListener("click", () => {
342
- statusSelect.value = value;
343
- applyFilters();
344
- });
345
- return chip;
346
- };
347
-
348
- const chips = [createChip("الكل", "", rows.length)];
349
- [...counts.entries()].forEach(([status, count]) => {
350
- chips.push(createChip(status, status, count));
351
- });
352
- statusChips.replaceChildren(...chips);
353
  }
354
 
355
  function applyFilters() {
356
- const supervisor = supervisorSelect.value;
357
- const researcher = researcherSelect.value;
358
  const query = normalize(searchInput.value);
359
 
360
- if (!supervisor && !query) {
361
- resultsSection.hidden = true;
362
- emptyState.hidden = false;
363
- return;
364
- }
365
-
366
- const status = statusSelect.value;
367
- const supervisorRows =
368
- !supervisor || supervisor === ALL_SUPERVISORS_VALUE
369
- ? allRows
370
- : allRows.filter((row) => row.supervisor === supervisor);
371
- const scopedRows = researcher
372
- ? supervisorRows.filter((row) => row.researcher === researcher)
373
- : supervisorRows;
374
 
375
  visibleRows = scopedRows.filter((row) => {
376
- const matchesStatus = !status || row.fulfillmentStatus === status;
377
- const matchesSearch =
378
- !query ||
379
- normalize(row.sampleName).includes(query) ||
380
- normalize(row.recordNumber).includes(query);
381
- return matchesStatus && matchesSearch;
382
  });
383
 
384
- emptyState.hidden = true;
385
- resultsSection.hidden = false;
386
- resultsTitle.textContent =
387
- researcher ||
388
- (supervisor === ALL_SUPERVISORS_VALUE
389
- ? "جميع العينات"
390
- : supervisor
391
- ? `عينات المشرف: ${supervisor}`
392
- : "نتائج البحث");
393
- resultsMeta.textContent = `${visibleRows.length} من أصل ${scopedRows.length} عينة`;
394
- copyAllButton.disabled = visibleRows.length === 0;
395
- copyAllButton.textContent = researcher ? "نسخ جميع عينات الباحث" : "نسخ جميع النتائج المعروضة";
396
- renderStatusChips(scopedRows);
397
-
398
- renderVisibleRows(true);
399
  noResults.hidden = visibleRows.length !== 0;
400
- }
401
-
402
- function resetResearcherControls() {
403
- setOptions(researcherSelect, [], "جميع الباحثين");
404
- setOptions(statusSelect, [], "جميع الحالات");
405
- researcherSelect.disabled = true;
406
- statusSelect.disabled = true;
407
- resultsSection.hidden = true;
408
- emptyState.hidden = false;
409
- emptyState.querySelector("h3").textContent = "ابحث أو اختر المشرف";
410
- emptyState.querySelector("p").textContent =
411
- "يمكنك البحث مباشرة باسم العينة أو رقم السجل، أو اختيار المشرف لعرض كامل عيناته.";
412
  }
413
 
414
  function initializeDashboard(data) {
415
- allRows = data.rows.map(rowToObject);
416
- const supervisorCounts = new Map();
417
- allRows.forEach((row) =>
418
- supervisorCounts.set(row.supervisor, (supervisorCounts.get(row.supervisor) || 0) + 1),
419
- );
420
- setOptions(
421
- supervisorSelect,
422
- uniqueSorted(allRows.map((row) => row.supervisor)),
423
- "اختر المشرف",
424
- supervisorCounts,
 
 
 
 
 
 
 
 
425
  );
426
- supervisorSelect.add(
427
- new Option(`إبراهيم - جميع العينات (${allRows.length})`, ALL_SUPERVISORS_VALUE),
428
- 1,
429
- );
430
- searchInput.value = "";
431
- searchInput.disabled = false;
432
- resetResearcherControls();
433
- startInactivityTimer();
434
- }
435
-
436
- function clearInactivityTimers() {
437
- clearTimeout(inactivityTimer);
438
- clearTimeout(inactivityWarningTimer);
439
- }
440
-
441
- function startInactivityTimer() {
442
- clearInactivityTimers();
443
- if (dashboardView.hidden) return;
444
-
445
- inactivityWarningTimer = setTimeout(() => {
446
- showToast("سيتم تسجيل الخروج تلقائيًا بعد دقيقة لعدم وجود نشاط");
447
- }, INACTIVITY_WARNING_MS);
448
-
449
- inactivityTimer = setTimeout(() => {
450
- performLogout("تم تسجيل الخروج تلقائيًا بعد 30 دقيقة من عدم النشاط");
451
- }, INACTIVITY_TIMEOUT_MS);
452
  }
453
 
454
- function performLogout(message = "") {
455
- clearInactivityTimers();
456
- allRows = [];
457
  visibleRows = [];
458
- renderedCount = 0;
459
- supervisorSelect.value = "";
460
  searchInput.value = "";
461
- resetResearcherControls();
 
462
  dashboardView.hidden = true;
463
  loginView.hidden = false;
464
  passwordInput.focus();
465
- if (message) showToast(message);
466
- }
467
-
468
- function clearFilters() {
469
- supervisorSelect.value = "";
470
- searchInput.value = "";
471
- resetResearcherControls();
472
- window.scrollTo({ top: 0, behavior: "smooth" });
473
- showToast("تم مسح جميع الفلاتر");
474
- }
475
-
476
- function handleActivity() {
477
- if (!dashboardView.hidden) startInactivityTimer();
478
  }
479
 
480
  loginForm.addEventListener("submit", async (event) => {
481
  event.preventDefault();
482
  const password = passwordInput.value.trim();
483
  if (!password) return;
484
-
485
  loginButton.disabled = true;
486
- loginButton.querySelector("span").textContent = "جارٍ التحقق...";
487
  loginError.textContent = "";
488
 
489
- if (failedAttempts >= 3) {
490
- await new Promise((resolve) => setTimeout(resolve, Math.min(failedAttempts * 800, 5000)));
491
- }
492
-
493
  try {
494
  const data = await decryptData(password);
495
  initializeDashboard(data);
496
  passwordInput.value = "";
497
  loginView.hidden = true;
498
  dashboardView.hidden = false;
499
- failedAttempts = 0;
500
  } catch {
501
- failedAttempts += 1;
502
- loginError.textContent = "رمز الدخول غير صحيح. تحقق منه وحاول مرة أخرى.";
503
  passwordInput.select();
504
  } finally {
505
  loginButton.disabled = false;
506
- loginButton.querySelector("span").textContent = "دخول آمن";
507
  }
508
  });
509
 
@@ -513,79 +350,26 @@ togglePassword.addEventListener("click", () => {
513
  togglePassword.setAttribute("aria-label", showing ? "إظهار رمز الدخول" : "إخفاء رمز الدخول");
514
  });
515
 
516
- logoutButton.addEventListener("click", () => {
517
- performLogout();
518
- });
519
 
520
- supervisorSelect.addEventListener("change", () => {
521
- resetResearcherControls();
522
- searchInput.value = "";
523
- if (!supervisorSelect.value) {
524
- applyFilters();
525
- return;
526
- }
527
-
528
- const researchers = uniqueSorted(
529
- allRows
530
- .filter(
531
- (row) =>
532
- supervisorSelect.value === ALL_SUPERVISORS_VALUE ||
533
- row.supervisor === supervisorSelect.value,
534
- )
535
- .map((row) => row.researcher),
536
- );
537
- const scopeRows =
538
- supervisorSelect.value === ALL_SUPERVISORS_VALUE
539
- ? allRows
540
- : allRows.filter((row) => row.supervisor === supervisorSelect.value);
541
- const researcherCounts = new Map();
542
- scopeRows.forEach((row) =>
543
- researcherCounts.set(row.researcher, (researcherCounts.get(row.researcher) || 0) + 1),
544
- );
545
- setOptions(researcherSelect, researchers, "جميع الباحثين", researcherCounts);
546
- setOptions(
547
- statusSelect,
548
- uniqueSorted(scopeRows.map((row) => row.fulfillmentStatus)),
549
- "جميع الحالات",
550
- );
551
- researcherSelect.disabled = false;
552
- statusSelect.disabled = false;
553
  applyFilters();
554
  });
555
 
556
- researcherSelect.addEventListener("change", () => {
557
- const rows = allRows.filter((row) => {
558
- const matchesSupervisor =
559
- supervisorSelect.value === ALL_SUPERVISORS_VALUE ||
560
- row.supervisor === supervisorSelect.value;
561
- const matchesResearcher =
562
- !researcherSelect.value || row.researcher === researcherSelect.value;
563
- return matchesSupervisor && matchesResearcher;
564
- });
565
- const statuses = uniqueSorted(rows.map((row) => row.fulfillmentStatus));
566
- setOptions(statusSelect, statuses, "جميع الحالات");
567
- statusSelect.disabled = false;
568
- applyFilters();
569
- });
570
-
571
- statusSelect.addEventListener("change", applyFilters);
572
  searchInput.addEventListener("input", applyFilters);
573
  loadMoreButton.addEventListener("click", () => renderVisibleRows());
574
- clearFiltersButton.addEventListener("click", clearFilters);
575
- ACTIVITY_EVENTS.forEach((eventName) =>
576
- document.addEventListener(eventName, handleActivity, { passive: true }),
577
- );
578
-
579
- copyAllButton.addEventListener("click", () => {
580
- if (!visibleRows.length) return;
581
- const heading = researcherSelect.value
582
- ? `بيانات العينات الخاصة بالباحث: ${researcherSelect.value}`
583
- : `بيانات العينات المعروضة (${visibleRows.length} عينة)`;
584
- const messages = visibleRows.map((row, index) => buildMessage(row, index + 1));
585
- copyText(
586
- [heading, "════════════════════", ...messages].join("\n\n"),
587
- "تم نسخ جميع العينات المعروضة",
588
- );
589
  });
590
 
591
  if (!window.crypto?.subtle || typeof ENCRYPTED_DATA === "undefined") {
 
1
  "use strict";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const loginView = document.getElementById("loginView");
4
  const dashboardView = document.getElementById("dashboardView");
5
  const loginForm = document.getElementById("loginForm");
 
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 citySelect = document.getElementById("citySelect");
 
13
  const searchInput = document.getElementById("searchInput");
14
+ const clearFiltersButton = document.getElementById("clearFiltersButton");
15
  const emptyState = document.getElementById("emptyState");
16
  const resultsSection = document.getElementById("resultsSection");
17
  const resultsMeta = document.getElementById("resultsMeta");
18
  const resultsTitle = document.getElementById("resultsTitle");
 
19
  const samplesGrid = document.getElementById("samplesGrid");
20
  const noResults = document.getElementById("noResults");
 
21
  const loadMoreWrap = document.getElementById("loadMoreWrap");
22
  const loadMoreButton = document.getElementById("loadMoreButton");
23
  const loadMoreMeta = document.getElementById("loadMoreMeta");
24
+ const totalCounter = document.getElementById("totalCounter");
25
+ const regionStats = document.getElementById("regionStats");
26
  const toast = document.getElementById("toast");
27
 
28
+ const PAGE_SIZE = 24;
29
+ const CONTACT_MESSAGE = (name) =>
30
+ `مرحبا استاذ\nاتواصل معاك لمعرف موقع (${name || "العينة"})\nهل ممكن تزودني بموقع العينة كرما`;
31
+
32
+ let rows = [];
33
  let visibleRows = [];
 
 
 
 
34
  let renderedCount = 0;
35
+ let toastTimer;
 
 
 
 
36
 
37
  function base64ToBytes(value) {
38
  const binary = atob(value);
 
47
  false,
48
  ["deriveKey"],
49
  );
 
50
  return crypto.subtle.deriveKey(
51
+ { name: "PBKDF2", salt, iterations: ENCRYPTED_DATA.iterations, hash: "SHA-256" },
 
 
 
 
 
52
  material,
53
  { name: "AES-GCM", length: 256 },
54
  false,
 
77
  .toLowerCase();
78
  }
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  function uniqueSorted(values) {
81
  return [...new Set(values.filter(Boolean))].sort((a, b) =>
82
  String(a).localeCompare(String(b), "ar", { sensitivity: "base" }),
83
  );
84
  }
85
 
86
+ function setOptions(select, values, placeholder) {
87
  select.replaceChildren(new Option(placeholder, ""));
88
+ values.forEach((value) => select.add(new Option(value, value)));
 
 
 
89
  }
90
 
91
  function showToast(message) {
92
  clearTimeout(toastTimer);
93
  toast.textContent = message;
94
  toast.classList.add("show");
95
+ toastTimer = setTimeout(() => toast.classList.remove("show"), 2200);
96
  }
97
 
98
  async function copyText(text, successMessage) {
 
111
  showToast(successMessage);
112
  }
113
 
114
+ function phoneDigits(value) {
115
+ const match = String(value ?? "").match(/(?:\+?966|0)?5\d{8}/);
116
+ if (!match) return "";
117
+ let digits = match[0].replace(/\D/g, "");
118
+ if (digits.startsWith("966")) return digits;
119
+ if (digits.startsWith("0")) return `966${digits.slice(1)}`;
120
+ return `966${digits}`;
121
+ }
122
+
123
+ function isCoordinate(value) {
124
+ return /^[-+]?\d{1,2}(?:\.\d+)?\s*,\s*[-+]?\d{1,3}(?:\.\d+)?$/.test(String(value ?? "").trim());
125
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ function createSvgIcon(path) {
128
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
129
+ svg.setAttribute("viewBox", "0 0 24 24");
130
+ svg.setAttribute("aria-hidden", "true");
131
+ const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
132
+ p.setAttribute("d", path);
133
+ svg.append(p);
134
+ return svg;
135
  }
136
 
137
+ function createActionLink(label, href, iconPath) {
138
+ const link = document.createElement("a");
139
+ link.className = "action-link";
140
+ link.href = href;
141
+ link.target = "_blank";
142
+ link.rel = "noopener";
143
+ link.append(createSvgIcon(iconPath), document.createTextNode(label));
144
+ return link;
145
+ }
146
+
147
+ function createDetail(label, value, extraClass = "") {
148
+ const item = document.createElement("div");
149
+ item.className = `detail-item ${extraClass}`.trim();
150
+ const title = document.createElement("span");
151
+ title.className = "detail-label";
152
+ title.textContent = label;
153
+ const text = document.createElement("span");
154
+ text.className = "detail-value";
155
+ text.textContent = String(value);
156
+ item.append(title, text);
157
+ return item;
158
+ }
159
+
160
+ function createCoordinatesBlock(row) {
161
+ const value = String(row.coordinates ?? "").trim();
162
+ if (!value) return null;
163
+
164
+ const item = document.createElement("div");
165
+ item.className = "detail-item coordinates-detail";
166
+ const label = document.createElement("span");
167
+ label.className = "detail-label";
168
+ label.textContent = "الإحداثيات";
169
+ item.append(label);
170
+
171
+ if (isCoordinate(value)) {
172
+ item.append(
173
+ createActionLink(
174
+ "اضغط هنا للذهاب بخرائط Google",
175
+ `https://www.google.com/maps?q=${encodeURIComponent(value)}`,
176
+ "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",
177
+ ),
178
+ );
179
+ return item;
180
  }
181
 
182
+ const phone = phoneDigits(value);
183
+ const note = document.createElement("p");
184
+ note.className = "contact-note";
185
+ note.textContent = value;
186
+ item.append(note);
187
+
188
+ if (phone) {
189
+ const message = CONTACT_MESSAGE(row.establishmentName);
190
+ const actions = document.createElement("div");
191
+ actions.className = "contact-actions";
192
+ actions.append(
193
+ createActionLink("اتصال", `tel:+${phone}`, "M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1.9.3 1.8.6 2.6a2 2 0 0 1-.4 2.1L8.1 9.6a16 16 0 0 0 6.3 6.3l1.2-1.2a2 2 0 0 1 2.1-.4c.8.3 1.7.5 2.6.6a2 2 0 0 1 1.7 2Z"),
194
+ 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"),
 
 
195
  );
196
+ const copyButton = document.createElement("button");
197
+ copyButton.type = "button";
198
+ copyButton.className = "action-link";
199
+ 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("نسخ الرسالة"));
200
+ copyButton.addEventListener("click", () => copyText(message, "تم نسخ الرسالة الجاهزة"));
201
+ actions.append(copyButton);
202
+ item.append(actions);
203
  }
204
 
205
+ return item;
 
206
  }
207
 
208
+ function createCard(row, index) {
209
  const card = document.createElement("article");
210
  card.className = "sample-card";
211
 
212
  const header = document.createElement("div");
213
  header.className = "sample-card-header";
214
+ const heading = document.createElement("div");
215
+ const indexEl = document.createElement("span");
216
+ indexEl.className = "sample-index";
217
+ indexEl.textContent = `منشأة ${index + 1}`;
 
218
  const title = document.createElement("h3");
219
+ title.textContent = row.establishmentName || "منشأة دون اسم";
220
+ heading.append(indexEl, title);
 
221
  const badge = document.createElement("span");
222
  badge.className = "status-badge";
223
+ badge.textContent = row.complianceStatus || "غير محدد";
224
+ header.append(heading, badge);
225
 
226
  const details = document.createElement("div");
227
  details.className = "sample-details";
228
+ [
229
+ ["السجل التجاري", row.commercialRecord, "ltr-value"],
230
+ ["حالة الاستيفاء", row.complianceStatus, ""],
231
+ ["توضيح المدينة", row.cityClarification, "wide-detail"],
232
+ ].forEach(([label, value, className]) => {
233
+ if (String(value ?? "").trim()) details.append(createDetail(label, value, className));
234
  });
235
+ const coordinatesBlock = createCoordinatesBlock(row);
236
+ if (coordinatesBlock) details.append(coordinatesBlock);
237
 
238
+ card.append(header, details);
 
 
 
 
 
 
 
 
 
 
 
239
  return card;
240
  }
241
 
 
244
  renderedCount = 0;
245
  samplesGrid.replaceChildren();
246
  }
 
 
247
  const fragment = document.createDocumentFragment();
248
+ visibleRows.slice(renderedCount, renderedCount + PAGE_SIZE).forEach((row, offset) => {
249
+ fragment.append(createCard(row, renderedCount + offset));
250
  });
251
  samplesGrid.append(fragment);
252
+ renderedCount += Math.min(PAGE_SIZE, visibleRows.length - renderedCount);
 
253
  const remaining = visibleRows.length - renderedCount;
254
  loadMoreWrap.hidden = remaining <= 0;
255
  loadMoreButton.textContent = `عرض المزيد (${Math.min(PAGE_SIZE, remaining)})`;
256
+ loadMoreMeta.textContent = `تم عرض ${renderedCount} من أصل ${visibleRows.length} منشأة`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  }
258
 
259
  function applyFilters() {
260
+ const region = regionSelect.value;
261
+ const city = citySelect.value;
262
  const query = normalize(searchInput.value);
263
 
264
+ const scopedRows = rows.filter((row) => {
265
+ const matchesRegion = !region || row.region === region;
266
+ const matchesCity = !city || row.city === city;
267
+ return matchesRegion && matchesCity;
268
+ });
 
 
 
 
 
 
 
 
 
269
 
270
  visibleRows = scopedRows.filter((row) => {
271
+ if (!query) return Boolean(city || region);
272
+ return (
273
+ normalize(row.establishmentName).includes(query) ||
274
+ normalize(row.commercialRecord).includes(query) ||
275
+ normalize(row.cityClarification).includes(query)
276
+ );
277
  });
278
 
279
+ const shouldShow = visibleRows.length > 0 || city || region || query;
280
+ emptyState.hidden = shouldShow;
281
+ resultsSection.hidden = !shouldShow;
 
 
 
 
 
 
 
 
 
 
 
 
282
  noResults.hidden = visibleRows.length !== 0;
283
+ resultsTitle.textContent = city || region || "نتائج البحث";
284
+ resultsMeta.textContent = `${visibleRows.length} من أصل ${scopedRows.length} منشأة`;
285
+ renderVisibleRows(true);
 
 
 
 
 
 
 
 
 
286
  }
287
 
288
  function initializeDashboard(data) {
289
+ rows = data.rows || [];
290
+ totalCounter.textContent = `${rows.length} منشأة`;
291
+ setOptions(regionSelect, data.regions || uniqueSorted(rows.map((row) => row.region)), "اختر المنطقة");
292
+ const regionCounts = new Map();
293
+ rows.forEach((row) => regionCounts.set(row.region, (regionCounts.get(row.region) || 0) + 1));
294
+ regionStats.replaceChildren(
295
+ ...(data.regions || uniqueSorted(rows.map((row) => row.region))).map((region) => {
296
+ const chip = document.createElement("button");
297
+ chip.type = "button";
298
+ chip.className = "region-stat";
299
+ chip.innerHTML = `<span>${region}</span><strong>${regionCounts.get(region) || 0}</strong>`;
300
+ chip.addEventListener("click", () => {
301
+ regionSelect.value = region;
302
+ regionSelect.dispatchEvent(new Event("change"));
303
+ window.scrollTo({ top: document.querySelector(".controls-panel").offsetTop - 84, behavior: "smooth" });
304
+ });
305
+ return chip;
306
+ }),
307
  );
308
+ setOptions(citySelect, [], "اختر المدينة الصناعية");
309
+ citySelect.disabled = true;
310
+ applyFilters();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  }
312
 
313
+ function performLogout() {
314
+ rows = [];
 
315
  visibleRows = [];
 
 
316
  searchInput.value = "";
317
+ regionSelect.value = "";
318
+ citySelect.value = "";
319
  dashboardView.hidden = true;
320
  loginView.hidden = false;
321
  passwordInput.focus();
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
 
324
  loginForm.addEventListener("submit", async (event) => {
325
  event.preventDefault();
326
  const password = passwordInput.value.trim();
327
  if (!password) return;
 
328
  loginButton.disabled = true;
329
+ loginButton.querySelector("span").textContent = "جاري التحقق...";
330
  loginError.textContent = "";
331
 
 
 
 
 
332
  try {
333
  const data = await decryptData(password);
334
  initializeDashboard(data);
335
  passwordInput.value = "";
336
  loginView.hidden = true;
337
  dashboardView.hidden = false;
 
338
  } catch {
339
+ loginError.textContent = "رمز الدخول غير صحيح.";
 
340
  passwordInput.select();
341
  } finally {
342
  loginButton.disabled = false;
343
+ loginButton.querySelector("span").textContent = "دخول";
344
  }
345
  });
346
 
 
350
  togglePassword.setAttribute("aria-label", showing ? "إظهار رمز الدخول" : "إخفاء رمز الدخول");
351
  });
352
 
353
+ logoutButton.addEventListener("click", performLogout);
 
 
354
 
355
+ regionSelect.addEventListener("change", () => {
356
+ const cities = uniqueSorted(rows.filter((row) => row.region === regionSelect.value).map((row) => row.city));
357
+ setOptions(citySelect, cities, "اختر المدينة الصناعية");
358
+ citySelect.disabled = !regionSelect.value;
359
+ citySelect.value = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  applyFilters();
361
  });
362
 
363
+ citySelect.addEventListener("change", applyFilters);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  searchInput.addEventListener("input", applyFilters);
365
  loadMoreButton.addEventListener("click", () => renderVisibleRows());
366
+ clearFiltersButton.addEventListener("click", () => {
367
+ regionSelect.value = "";
368
+ searchInput.value = "";
369
+ setOptions(citySelect, [], "اختر المدينة الصناعية");
370
+ citySelect.disabled = true;
371
+ applyFilters();
372
+ showToast("تم مسح الاختيارات");
 
 
 
 
 
 
 
 
373
  });
374
 
375
  if (!window.crypto?.subtle || typeof ENCRYPTED_DATA === "undefined") {
data.js CHANGED
The diff for this file is too large to render. See raw diff
 
generate-data.mjs CHANGED
@@ -2,29 +2,74 @@ import fs from "node:fs/promises";
2
  import crypto from "node:crypto";
3
  import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
4
 
5
- const [inputPath, outputPath, password] = process.argv.slice(2);
6
 
7
- if (!inputPath || !outputPath || !password) {
8
- console.error("Usage: node generate-data.mjs <input.xlsx> <output.js> <password>");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  process.exitCode = 1;
10
  } else {
11
  const blob = await FileBlob.load(inputPath);
12
  const workbook = await SpreadsheetFile.importXlsx(blob);
13
- const sheet = workbook.worksheets.getItemAt(0);
14
- const values = sheet.getUsedRange(true).values;
15
- const headers = values[0].map((value) => String(value ?? "").trim());
16
- const rows = values.slice(1).map((row) =>
17
- headers.map((_, index) => {
18
- const value = row[index];
19
- if (value === null || value === undefined) return "";
20
- return typeof value === "string" ? value.trim() : String(value);
21
- }),
22
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  const payload = JSON.stringify({
25
- version: 1,
26
  generatedAt: new Date().toISOString(),
27
- headers,
28
  rows,
29
  });
30
 
@@ -49,12 +94,5 @@ if (!inputPath || !outputPath || !password) {
49
  ].join("\n");
50
 
51
  await fs.writeFile(outputPath, output, "utf8");
52
- console.log(
53
- JSON.stringify({
54
- rows: rows.length,
55
- columns: headers.length,
56
- encryptedBytes: combined.length,
57
- outputPath,
58
- }),
59
- );
60
  }
 
2
  import crypto from "node:crypto";
3
  import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
4
 
5
+ const [inputPath, outputPath, password = "1234"] = 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
 
 
94
  ].join("\n");
95
 
96
  await fs.writeFile(outputPath, output, "utf8");
97
+ console.log(JSON.stringify({ rows: rows.length, regions: REGIONS.length, outputPath }));
 
 
 
 
 
 
 
98
  }
index.html CHANGED
@@ -3,203 +3,134 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="theme-color" content="#123c69" />
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;600;700;800&display=swap"
13
  rel="stylesheet"
14
  />
15
  <link rel="stylesheet" href="style.css" />
16
  </head>
17
  <body>
18
- <div class="background-shape shape-one"></div>
19
- <div class="background-shape shape-two"></div>
20
-
21
  <main>
22
  <section id="loginView" class="login-view">
23
- <div class="login-card">
24
  <div class="brand-mark" aria-hidden="true">
25
- <svg viewBox="0 0 48 48" role="img">
26
- <path d="M24 4 42 14v20L24 44 6 34V14L24 4Z" />
27
- <path d="M16 25.5 21.5 31 33 18.5" />
28
- </svg>
29
  </div>
30
- <p class="eyebrow">بوابة العمل الميداني</p>
31
- <h1>نظام متابعة العينات</h1>
32
- <p class="login-description">
33
- أدخل رمز الدخول للوصول إلى بيانات التواصل ومتابعة حالات العينات.
34
- </p>
35
 
36
- <form id="loginForm" class="login-form" autocomplete="off">
37
- <label for="password">رمز الدخول</label>
38
- <div class="password-field">
39
- <input
40
- id="password"
41
- name="password"
42
- type="password"
43
- inputmode="numeric"
44
- placeholder="أدخل رمز الدخول"
45
- required
46
- autofocus
47
- />
48
- <button id="togglePassword" type="button" class="icon-button" aria-label="إظهار رمز الدخول">
49
- <svg viewBox="0 0 24 24" aria-hidden="true">
50
- <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" />
51
- <circle cx="12" cy="12" r="2.8" />
52
- </svg>
53
- </button>
54
- </div>
55
- <p id="loginError" class="form-error" role="alert"></p>
56
- <button id="loginButton" class="primary-button" type="submit">
57
- <span>دخول آمن</span>
58
- <svg viewBox="0 0 24 24" aria-hidden="true">
59
- <path d="m9 18 6-6-6-6" />
60
- </svg>
61
  </button>
62
- </form>
63
-
64
- <div class="security-note">
65
- <svg viewBox="0 0 24 24" aria-hidden="true">
66
- <rect x="5" y="10" width="14" height="10" rx="2" />
67
- <path d="M8 10V7a4 4 0 0 1 8 0v3" />
68
- </svg>
69
- <span>البيانات مشفرة ولا تُعرض قبل تسجيل الدخول.</span>
70
  </div>
71
- </div>
 
 
 
 
 
72
  </section>
73
 
74
  <section id="dashboardView" class="dashboard-view" hidden>
75
  <header class="topbar">
76
  <div class="topbar-brand">
77
  <div class="brand-mark small" aria-hidden="true">
78
- <svg viewBox="0 0 48 48">
79
- <path d="M24 4 42 14v20L24 44 6 34V14L24 4Z" />
80
- <path d="M16 25.5 21.5 31 33 18.5" />
81
- </svg>
82
  </div>
83
  <div>
84
- <p>بوابة العمل الميداني</p>
85
- <h1>نظام متابعة العينات</h1>
86
  </div>
87
  </div>
88
  <button id="logoutButton" class="secondary-button compact" type="button">
89
- <svg viewBox="0 0 24 24" aria-hidden="true">
90
- <path d="M10 17l5-5-5-5M15 12H3M15 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4" />
91
- </svg>
92
  خروج
93
  </button>
94
  </header>
95
 
96
  <div class="dashboard-shell">
97
- <section class="welcome-panel">
98
- <div>
99
  <p class="eyebrow">لوحة الاستعلام</p>
100
- <h2>الوصول إلى بيانات العينة بسهولة</h2>
101
- <p>ابحث مباشرة، أو اختر المشرف لعرض عيناته ثم حدّد الباحث عند الحاجة.</p>
102
  </div>
103
- <div class="privacy-pill">
104
- <span class="status-dot"></span>
105
- جلسة خاصة
106
  </div>
107
  </section>
108
 
109
- <section class="controls-card">
110
  <div class="filters-grid">
111
  <div class="field-group">
112
- <label for="supervisorSelect">اسم المشرف</label>
113
- <div class="select-wrap">
114
- <select id="supervisorSelect">
115
- <option value="">اختر المشرف</option>
116
- </select>
117
- <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg>
118
- </div>
119
- </div>
120
-
121
- <div class="field-group">
122
- <label for="researcherSelect">اسم الباحث</label>
123
  <div class="select-wrap">
124
- <select id="researcherSelect" disabled>
125
- <option value="">اختر الباحث</option>
126
- </select>
127
- <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg>
128
  </div>
129
  </div>
130
 
131
  <div class="field-group">
132
- <label for="statusSelect">حالة الاستيفاء</label>
133
  <div class="select-wrap">
134
- <select id="statusSelect" disabled>
135
- <option value="">جميع الحالات</option>
136
- </select>
137
- <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg>
138
  </div>
139
  </div>
140
 
141
  <div class="field-group search-group">
142
- <label for="searchInput">البحث الذكي</label>
143
  <div class="search-wrap">
144
- <svg viewBox="0 0 24 24" aria-hidden="true">
145
- <circle cx="11" cy="11" r="7" />
146
- <path d="m20 20-4-4" />
147
- </svg>
148
- <input
149
- id="searchInput"
150
- type="search"
151
- placeholder="اسم العينة أو رقم السجل"
152
- />
153
  </div>
154
  </div>
155
  </div>
 
 
 
 
 
 
 
156
  </section>
157
 
158
  <section id="emptyState" class="empty-state">
159
- <div class="empty-icon">
160
- <svg viewBox="0 0 48 48" aria-hidden="true">
161
- <path d="M8 13h32v27H8z" />
162
- <path d="M15 8h18v10H15zM15 26h18M15 33h11" />
163
- </svg>
164
- </div>
165
- <h3>ابحث أو اختر المشرف</h3>
166
- <p>اكتب اسم العينة أو رقم السجل مباشرة، أو اختر المشرف لعرض كامل عيناته.</p>
167
  </section>
168
 
169
  <section id="resultsSection" class="results-section" hidden>
170
  <div class="results-header">
171
  <div>
172
  <p id="resultsMeta" class="results-meta"></p>
173
- <h2 id="resultsTitle">العينات</h2>
174
  </div>
175
- <button id="copyAllButton" class="primary-button copy-all" type="button">
176
- <svg viewBox="0 0 24 24" aria-hidden="true">
177
- <rect x="8" y="8" width="11" height="11" rx="2" />
178
- <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
179
- </svg>
180
- نسخ جميع عينات الباحث
181
- </button>
182
  </div>
183
-
184
- <div id="statusChips" class="status-chips" aria-label="ملخص الحالات"></div>
185
  <div id="samplesGrid" class="samples-grid"></div>
186
  <div id="loadMoreWrap" class="load-more-wrap" hidden>
187
- <button id="loadMoreButton" class="secondary-button load-more-button" type="button">
188
- عرض المزيد
189
- </button>
190
  <p id="loadMoreMeta"></p>
191
  </div>
192
-
193
  <div id="noResults" class="no-results" hidden>
194
  <h3>لا توجد نتائج مطابقة</h3>
195
- <p>جرّب تغيير حالة الاستيفاء أو عبارة البحث.</p>
196
  </div>
197
  </section>
198
  </div>
199
  </section>
200
  </main>
201
 
202
- <footer id="footer">إعداد نوف الناصر</footer>
203
  <div id="toast" class="toast" role="status" aria-live="polite"></div>
204
 
205
  <script src="data.js"></script>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#0f4c5c" />
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>
35
+ <p id="loginError" class="form-error" role="alert"></p>
36
+ <button id="loginButton" class="primary-button" type="submit">
37
+ <span>دخول</span>
38
+ <svg viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></svg>
39
+ </button>
40
+ </form>
41
  </section>
42
 
43
  <section id="dashboardView" class="dashboard-view" hidden>
44
  <header class="topbar">
45
  <div class="topbar-brand">
46
  <div class="brand-mark small" aria-hidden="true">
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
+ <div class="summary-side">
67
+ <div id="totalCounter" class="counter-pill">0 منشأة</div>
68
+ <div id="regionStats" class="region-stats" aria-label="ملخص المناطق"></div>
69
  </div>
70
  </section>
71
 
72
+ <section class="controls-panel">
73
  <div class="filters-grid">
74
  <div class="field-group">
75
+ <label for="regionSelect">المنطقة</label>
 
 
 
 
 
 
 
 
 
 
76
  <div class="select-wrap">
77
+ <select id="regionSelect"><option value="">اختر المنطقة</option></select>
78
+ <svg viewBox="0 0 24 24"><path d="m7 10 5 5 5-5" /></svg>
 
 
79
  </div>
80
  </div>
81
 
82
  <div class="field-group">
83
+ <label for="citySelect">المدينة الصناعية</label>
84
  <div class="select-wrap">
85
+ <select id="citySelect" disabled><option value="">اختر المدينة الصناعية</option></select>
86
+ <svg viewBox="0 0 24 24"><path d="m7 10 5 5 5-5" /></svg>
 
 
87
  </div>
88
  </div>
89
 
90
  <div class="field-group search-group">
91
+ <label for="searchInput">اسم المنشأة</label>
92
  <div class="search-wrap">
93
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4-4" /></svg>
94
+ <input id="searchInput" type="search" placeholder="ابحث باسم المنشأة أو السجل التجاري" />
 
 
 
 
 
 
 
95
  </div>
96
  </div>
97
  </div>
98
+
99
+ <div class="filters-actions">
100
+ <button id="clearFiltersButton" class="clear-filters-button" type="button">
101
+ <svg viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12" /></svg>
102
+ مسح الاختيارات
103
+ </button>
104
+ </div>
105
  </section>
106
 
107
  <section id="emptyState" class="empty-state">
108
+ <h3>ابدأ بالاختيار أو البحث</h3>
109
+ <p>عند اختيار مدينة صناعية ستظهر كل بياناتها بالأسفل، ويمكن تضييق النتائج بالبحث.</p>
 
 
 
 
 
 
110
  </section>
111
 
112
  <section id="resultsSection" class="results-section" hidden>
113
  <div class="results-header">
114
  <div>
115
  <p id="resultsMeta" class="results-meta"></p>
116
+ <h2 id="resultsTitle">النتائج</h2>
117
  </div>
 
 
 
 
 
 
 
118
  </div>
 
 
119
  <div id="samplesGrid" class="samples-grid"></div>
120
  <div id="loadMoreWrap" class="load-more-wrap" hidden>
121
+ <button id="loadMoreButton" class="secondary-button load-more-button" type="button">عرض المزيد</button>
 
 
122
  <p id="loadMoreMeta"></p>
123
  </div>
 
124
  <div id="noResults" class="no-results" hidden>
125
  <h3>لا توجد نتائج مطابقة</h3>
126
+ <p>جرّب كتابة جزء من اسم المنشأة أو رقم السجل التجاري.</p>
127
  </div>
128
  </section>
129
  </div>
130
  </section>
131
  </main>
132
 
133
+ <footer>إعداد نظام الاستعلام</footer>
134
  <div id="toast" class="toast" role="status" aria-live="polite"></div>
135
 
136
  <script src="data.js"></script>
style.css CHANGED
@@ -1,17 +1,17 @@
1
  :root {
2
- --navy: #123c69;
3
- --navy-dark: #0a2c50;
4
- --blue: #1d659b;
5
- --sky: #e9f4fb;
6
- --teal: #12877d;
7
- --teal-soft: #e8f6f3;
8
- --ink: #172b3a;
9
- --muted: #657786;
10
- --line: #dce6ec;
11
  --surface: #ffffff;
12
- --page: #f4f8fa;
 
 
 
 
 
13
  --danger: #b43d4f;
14
- --shadow: 0 18px 50px rgba(15, 54, 82, 0.11);
15
  }
16
 
17
  * {
@@ -27,11 +27,9 @@ body {
27
  min-height: 100vh;
28
  margin: 0;
29
  color: var(--ink);
30
- font-size: 16px;
31
  font-family: "Tajawal", Arial, sans-serif;
32
- background:
33
- radial-gradient(circle at 10% 10%, rgba(41, 151, 173, 0.12), transparent 30%),
34
- linear-gradient(145deg, #f9fbfc 0%, #eff6f8 100%);
35
  }
36
 
37
  button,
@@ -49,6 +47,8 @@ button {
49
  }
50
 
51
  svg {
 
 
52
  fill: none;
53
  stroke: currentColor;
54
  stroke-linecap: round;
@@ -56,119 +56,77 @@ svg {
56
  stroke-width: 1.8;
57
  }
58
 
59
- .background-shape {
60
- position: fixed;
61
- z-index: -1;
62
- border-radius: 999px;
63
- filter: blur(2px);
64
- pointer-events: none;
65
- }
66
-
67
- .shape-one {
68
- width: 340px;
69
- height: 340px;
70
- top: -170px;
71
- right: -100px;
72
- background: rgba(29, 101, 155, 0.1);
73
- }
74
-
75
- .shape-two {
76
- width: 280px;
77
- height: 280px;
78
- bottom: -150px;
79
- left: -80px;
80
- background: rgba(18, 135, 125, 0.1);
81
- }
82
-
83
  .login-view {
84
- min-height: calc(100vh - 56px);
85
  display: grid;
86
  place-items: center;
87
- padding: 32px 20px;
88
  }
89
 
90
  .login-card {
91
- width: min(100%, 450px);
92
- padding: 42px;
93
- text-align: center;
94
- background: rgba(255, 255, 255, 0.95);
95
- border: 1px solid rgba(220, 230, 236, 0.9);
96
- border-radius: 28px;
97
  box-shadow: var(--shadow);
98
- backdrop-filter: blur(18px);
99
  }
100
 
101
  .brand-mark {
102
- width: 72px;
103
- height: 72px;
104
  display: grid;
105
  place-items: center;
106
- margin: 0 auto 22px;
107
- color: white;
108
- background: linear-gradient(145deg, var(--navy), var(--blue));
109
- border-radius: 22px;
110
- box-shadow: 0 14px 30px rgba(18, 60, 105, 0.25);
111
  }
112
 
113
  .brand-mark svg {
114
- width: 43px;
115
- height: 43px;
116
- }
117
-
118
- .brand-mark svg path:first-child {
119
- fill: rgba(255, 255, 255, 0.08);
120
- }
121
-
122
- .brand-mark svg path:last-child {
123
- stroke-width: 3;
124
  }
125
 
126
  .brand-mark.small {
127
- width: 48px;
128
- height: 48px;
129
  margin: 0;
130
- border-radius: 15px;
131
- box-shadow: none;
132
- }
133
-
134
- .brand-mark.small svg {
135
- width: 29px;
136
- height: 29px;
137
  }
138
 
139
  .eyebrow {
140
- margin: 0 0 8px;
141
- color: var(--teal);
142
  font-size: 13px;
143
  font-weight: 800;
144
- letter-spacing: 0.04em;
 
 
 
 
 
 
145
  }
146
 
147
  .login-card h1 {
148
  margin: 0;
149
- color: var(--navy-dark);
150
- font-size: clamp(27px, 5vw, 34px);
151
  line-height: 1.35;
152
  }
153
 
154
  .login-description {
155
- max-width: 340px;
156
- margin: 12px auto 28px;
157
  color: var(--muted);
158
- font-size: 16px;
159
  line-height: 1.8;
160
  }
161
 
162
- .login-form {
163
- text-align: right;
164
- }
165
-
166
- .login-form label,
167
  .field-group label {
168
  display: block;
169
  margin-bottom: 8px;
170
- color: #314b5e;
171
- font-size: 15px;
172
  font-weight: 700;
173
  }
174
 
@@ -182,60 +140,54 @@ svg {
182
  .search-wrap input,
183
  .select-wrap select {
184
  width: 100%;
185
- height: 52px;
186
  color: var(--ink);
187
  background: #fbfdfe;
188
  border: 1px solid var(--line);
189
- border-radius: 14px;
190
- font-size: 16px;
191
  outline: none;
192
- transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
193
  }
194
 
195
  .password-field input {
196
- padding: 0 16px 0 50px;
197
  }
198
 
199
  .password-field input:focus,
200
  .search-wrap input:focus,
201
  .select-wrap select:focus {
202
- background: white;
203
- border-color: var(--blue);
204
- box-shadow: 0 0 0 4px rgba(29, 101, 155, 0.1);
205
  }
206
 
207
  .icon-button {
208
  position: absolute;
209
  top: 50%;
210
- left: 8px;
211
- width: 38px;
212
- height: 38px;
213
  display: grid;
214
  place-items: center;
215
  padding: 0;
216
  color: var(--muted);
217
  background: transparent;
218
  border: 0;
219
- border-radius: 10px;
220
  transform: translateY(-50%);
221
  }
222
 
223
  .icon-button:hover {
224
- color: var(--navy);
225
- background: var(--sky);
226
- }
227
-
228
- .icon-button svg {
229
- width: 22px;
230
- height: 22px;
231
  }
232
 
233
  .form-error {
234
  min-height: 22px;
235
- margin: 7px 2px 5px;
236
  color: var(--danger);
237
  font-size: 13px;
238
- font-weight: 600;
239
  }
240
 
241
  .primary-button,
@@ -243,280 +195,198 @@ svg {
243
  display: inline-flex;
244
  align-items: center;
245
  justify-content: center;
246
- gap: 9px;
247
- min-height: 48px;
248
- padding: 0 20px;
249
- font-weight: 700;
250
- border-radius: 13px;
251
- transition: transform 0.18s, box-shadow 0.18s, background 0.18s;
252
  }
253
 
254
  .primary-button {
255
- color: white;
256
- background: linear-gradient(135deg, var(--navy), var(--blue));
257
- border: 0;
258
- box-shadow: 0 9px 22px rgba(18, 60, 105, 0.2);
259
- }
260
-
261
- .login-form .primary-button {
262
  width: 100%;
 
 
 
263
  }
264
 
265
  .primary-button:hover {
266
- transform: translateY(-1px);
267
- box-shadow: 0 12px 25px rgba(18, 60, 105, 0.26);
268
  }
269
 
270
  .primary-button:disabled {
271
  cursor: wait;
272
  opacity: 0.65;
273
- transform: none;
274
- }
275
-
276
- .primary-button svg,
277
- .secondary-button svg {
278
- width: 20px;
279
- height: 20px;
280
  }
281
 
282
- .security-note {
283
- display: flex;
284
- align-items: center;
285
- justify-content: center;
286
- gap: 8px;
287
- margin-top: 22px;
288
- color: var(--muted);
289
- font-size: 12px;
290
  }
291
 
292
- .security-note svg {
293
- width: 17px;
294
- height: 17px;
295
- color: var(--teal);
296
  }
297
 
298
  .topbar {
299
- min-height: 78px;
300
  display: flex;
301
  align-items: center;
302
  justify-content: space-between;
303
- padding: 13px max(20px, calc((100% - 1240px) / 2));
304
- background: rgba(255, 255, 255, 0.92);
 
305
  border-bottom: 1px solid var(--line);
306
- box-shadow: 0 5px 22px rgba(27, 64, 90, 0.05);
307
- backdrop-filter: blur(12px);
308
  }
309
 
310
  .topbar-brand {
311
  display: flex;
312
  align-items: center;
313
- gap: 13px;
314
- }
315
-
316
- .topbar-brand p {
317
- margin: 0 0 2px;
318
- color: var(--teal);
319
- font-size: 11px;
320
- font-weight: 700;
321
  }
322
 
 
323
  .topbar-brand h1 {
324
  margin: 0;
325
- color: var(--navy-dark);
326
- font-size: 20px;
327
  }
328
 
329
- .secondary-button {
330
- color: var(--navy);
331
- background: white;
332
- border: 1px solid var(--line);
333
  }
334
 
335
- .secondary-button:hover {
336
- background: var(--sky);
337
- border-color: #b9d4e4;
338
  }
339
 
340
  .secondary-button.compact {
341
- min-height: 42px;
342
- padding: 0 15px;
343
  }
344
 
345
  .dashboard-shell {
346
- width: min(1240px, calc(100% - 40px));
347
- min-height: calc(100vh - 134px);
348
- margin: 28px auto 44px;
 
 
 
 
 
 
 
 
 
 
349
  }
350
 
351
- .welcome-panel {
352
  display: flex;
353
  align-items: center;
354
  justify-content: space-between;
355
- gap: 24px;
356
- padding: 24px 28px;
357
- color: white;
358
- overflow: hidden;
359
- position: relative;
360
- background:
361
- radial-gradient(circle at 12% 130%, rgba(89, 223, 206, 0.28), transparent 32%),
362
- linear-gradient(115deg, rgba(8, 39, 72, 0.99), rgba(25, 105, 161, 0.95)),
363
- var(--navy);
364
- border-radius: 22px;
365
- box-shadow: 0 14px 32px rgba(18, 60, 105, 0.17);
366
- }
367
-
368
- .welcome-panel .eyebrow {
369
- color: #91dfd6;
370
- }
371
-
372
- .welcome-panel h2 {
373
- margin: 0 0 5px;
374
- font-size: clamp(21px, 3vw, 28px);
375
  }
376
 
377
- .welcome-panel > div > p:last-child {
378
  margin: 0;
379
- color: rgba(255, 255, 255, 0.77);
380
- font-size: 15px;
 
381
  }
382
 
383
- .privacy-pill {
384
  flex: 0 0 auto;
385
- display: flex;
386
- align-items: center;
387
- gap: 8px;
388
- padding: 10px 14px;
389
- color: #d9fff9;
390
- font-size: 13px;
391
- font-weight: 700;
392
- background: rgba(255, 255, 255, 0.11);
393
- border: 1px solid rgba(255, 255, 255, 0.18);
394
- border-radius: 999px;
395
- }
396
-
397
- .status-dot {
398
- width: 8px;
399
- height: 8px;
400
- background: #68ebce;
401
- border-radius: 50%;
402
- box-shadow: 0 0 0 5px rgba(104, 235, 206, 0.14);
403
  }
404
 
405
- .controls-card {
406
- position: relative;
407
- z-index: 1;
408
- margin-top: 18px;
409
- padding: 22px;
410
- background: rgba(255, 255, 255, 0.96);
411
- border: 1px solid var(--line);
412
- border-radius: 20px;
413
- box-shadow: 0 14px 34px rgba(24, 66, 91, 0.08);
414
- backdrop-filter: blur(16px);
415
  }
416
 
417
  .filters-grid {
418
  display: grid;
419
- grid-template-columns: repeat(3, minmax(180px, 1fr)) minmax(220px, 1.2fr);
420
- gap: 16px;
421
- }
422
-
423
- .filters-actions {
424
- display: flex;
425
- justify-content: flex-start;
426
- margin-top: 14px;
427
- }
428
-
429
- .clear-filters-button {
430
- display: inline-flex;
431
- align-items: center;
432
- justify-content: center;
433
- gap: 7px;
434
- min-height: 38px;
435
- padding: 0 13px;
436
- color: #607583;
437
- font-size: 13px;
438
- font-weight: 700;
439
- background: #f7fafb;
440
- border: 1px solid var(--line);
441
- border-radius: 11px;
442
- }
443
-
444
- .clear-filters-button:hover {
445
- color: var(--danger);
446
- background: #fff7f8;
447
- border-color: #ecc9cf;
448
- }
449
-
450
- .clear-filters-button svg {
451
- width: 17px;
452
- height: 17px;
453
  }
454
 
455
  .select-wrap select {
456
- padding: 0 15px 0 42px;
457
  appearance: none;
458
  }
459
 
460
  .select-wrap > svg {
461
  position: absolute;
462
  top: 50%;
463
- left: 15px;
464
- width: 19px;
465
  color: var(--muted);
466
  transform: translateY(-50%);
467
  pointer-events: none;
468
  }
469
 
470
  .search-wrap input {
471
- padding: 0 44px 0 14px;
472
  }
473
 
474
  .search-wrap > svg {
475
  position: absolute;
476
  top: 50%;
477
- right: 15px;
478
- width: 20px;
479
  color: var(--muted);
480
  transform: translateY(-50%);
481
  }
482
 
483
- select:disabled,
484
- input:disabled {
485
- cursor: not-allowed;
486
  color: #9aa8b2;
487
- background: #f3f6f7;
 
488
  }
489
 
490
- .empty-state,
491
- .no-results {
492
- margin-top: 18px;
493
- padding: 64px 24px;
494
- text-align: center;
495
- background: rgba(255, 255, 255, 0.7);
496
- border: 1px dashed #bfd0da;
497
- border-radius: 20px;
498
  }
499
 
500
- .empty-icon {
501
- width: 72px;
502
- height: 72px;
503
- display: grid;
504
- place-items: center;
505
- margin: 0 auto 16px;
506
- color: var(--blue);
507
- background: var(--sky);
508
- border-radius: 22px;
 
 
 
 
 
 
 
509
  }
510
 
511
- .empty-icon svg {
512
- width: 39px;
513
- height: 39px;
 
 
 
514
  }
515
 
516
  .empty-state h3,
517
  .no-results h3 {
518
- margin: 0 0 7px;
519
- color: var(--navy-dark);
520
  font-size: 20px;
521
  }
522
 
@@ -527,171 +397,85 @@ input:disabled {
527
  }
528
 
529
  .results-section {
530
- margin-top: 24px;
531
  }
532
 
533
  .results-header {
534
  display: flex;
535
  align-items: center;
536
  justify-content: space-between;
537
- gap: 18px;
538
  margin-bottom: 14px;
539
  }
540
 
541
  .results-meta {
542
  margin: 0 0 4px;
543
- color: var(--teal);
544
  font-size: 14px;
545
  font-weight: 800;
546
  }
547
 
548
  .results-header h2 {
549
  margin: 0;
550
- color: var(--navy-dark);
551
- font-size: 26px;
552
- }
553
-
554
- .copy-all {
555
- min-height: 44px;
556
- background: var(--teal);
557
- box-shadow: 0 8px 20px rgba(18, 135, 125, 0.18);
558
- }
559
-
560
- .status-chips {
561
- display: flex;
562
- flex-wrap: wrap;
563
- gap: 8px;
564
- margin-bottom: 17px;
565
- }
566
-
567
- .status-chip {
568
- min-height: 38px;
569
- padding: 7px 11px;
570
- color: #466071;
571
- font-family: inherit;
572
- font-size: 13px;
573
- font-weight: 700;
574
- background: white;
575
- border: 1px solid var(--line);
576
- border-radius: 999px;
577
- box-shadow: 0 3px 10px rgba(26, 63, 87, 0.04);
578
- cursor: pointer;
579
- transition: color 0.18s, background 0.18s, border-color 0.18s, transform 0.18s;
580
- }
581
-
582
- .status-chip:hover {
583
- color: var(--navy);
584
- background: var(--sky);
585
- border-color: #b7d3e3;
586
- transform: translateY(-1px);
587
- }
588
-
589
- .status-chip.active {
590
- color: white;
591
- background: linear-gradient(135deg, var(--navy), var(--blue));
592
- border-color: transparent;
593
- box-shadow: 0 7px 17px rgba(18, 60, 105, 0.2);
594
- }
595
-
596
- .status-chip strong {
597
- margin-right: 5px;
598
- color: var(--navy);
599
- }
600
-
601
- .status-chip.active strong {
602
- color: #a9eee5;
603
  }
604
 
605
  .samples-grid {
606
  display: grid;
607
  grid-template-columns: repeat(2, minmax(0, 1fr));
608
- gap: 16px;
609
- }
610
-
611
- .load-more-wrap {
612
- display: flex;
613
- flex-direction: column;
614
- align-items: center;
615
- gap: 8px;
616
- margin-top: 24px;
617
- }
618
-
619
- .load-more-button {
620
- min-width: 210px;
621
- min-height: 50px;
622
- color: var(--navy);
623
- font-size: 16px;
624
- background: white;
625
- box-shadow: 0 8px 22px rgba(26, 63, 87, 0.08);
626
- }
627
-
628
- .load-more-wrap p {
629
- margin: 0;
630
- color: var(--muted);
631
- font-size: 13px;
632
- font-weight: 600;
633
  }
634
 
635
  .sample-card {
636
  overflow: hidden;
637
- background: white;
638
- border: 1px solid var(--line);
639
- border-radius: 21px;
640
- box-shadow: 0 10px 28px rgba(26, 63, 87, 0.07);
641
- transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
642
- }
643
-
644
- .sample-card:hover {
645
- border-color: #b9d4e4;
646
- box-shadow: 0 13px 30px rgba(26, 63, 87, 0.1);
647
- transform: translateY(-2px);
648
  }
649
 
650
  .sample-card-header {
651
  display: flex;
652
  align-items: flex-start;
653
  justify-content: space-between;
654
- gap: 14px;
655
- padding: 18px 20px 15px;
656
- background: linear-gradient(180deg, #fbfdfe, white);
657
  border-bottom: 1px solid #edf2f5;
658
- border-top: 4px solid var(--teal);
659
  }
660
 
661
  .sample-index {
662
  display: inline-block;
663
  margin-bottom: 5px;
664
- color: var(--teal);
665
  font-size: 12px;
666
  font-weight: 800;
667
  }
668
 
669
  .sample-card h3 {
670
  margin: 0;
671
- color: var(--navy-dark);
672
- font-size: 19px;
673
- line-height: 1.65;
674
  }
675
 
676
  .status-badge {
677
  flex: 0 0 auto;
678
- max-width: 155px;
679
  padding: 7px 10px;
680
- color: #8f4f16;
681
  font-size: 12px;
682
  font-weight: 800;
683
- line-height: 1.4;
684
  text-align: center;
685
- background: #fff4e7;
686
- border: 1px solid #f4d3aa;
687
- border-radius: 999px;
688
  }
689
 
690
  .sample-details {
691
  display: grid;
692
  grid-template-columns: repeat(2, minmax(0, 1fr));
693
  gap: 0;
694
- padding: 4px 20px;
695
  }
696
 
697
  .detail-item {
@@ -714,103 +498,111 @@ input:disabled {
714
  margin-bottom: 5px;
715
  color: var(--muted);
716
  font-size: 12px;
717
- font-weight: 700;
718
  }
719
 
720
  .detail-value {
721
  display: block;
722
- overflow-wrap: anywhere;
723
- color: #233d4f;
724
- font-size: 15px;
725
- font-weight: 600;
726
- line-height: 1.6;
727
- }
728
-
729
- .detail-value-row {
730
- display: flex;
731
- align-items: center;
732
- justify-content: space-between;
733
- gap: 8px;
734
- }
735
-
736
- .copy-field-button {
737
- flex: 0 0 auto;
738
- width: 34px;
739
- height: 34px;
740
- display: grid;
741
- place-items: center;
742
- padding: 0;
743
- color: var(--blue);
744
- background: var(--sky);
745
- border: 1px solid #cee3ef;
746
- border-radius: 9px;
747
  }
748
 
749
- .copy-field-button:hover {
750
- color: white;
751
- background: var(--blue);
752
- border-color: var(--blue);
 
 
753
  }
754
 
755
- .copy-field-button svg {
756
- width: 17px;
757
- height: 17px;
758
  }
759
 
760
- .contact-detail .detail-value {
761
- color: var(--blue);
 
 
 
 
 
 
 
 
 
 
 
762
  }
763
 
764
- .sample-card-footer {
765
- padding: 14px 20px 18px;
766
  }
767
 
768
- .copy-sample {
769
- width: 100%;
770
- min-height: 42px;
771
- color: var(--navy);
772
- font-weight: 800;
773
- background: var(--sky);
774
- border: 1px solid #cde2ee;
775
- border-radius: 12px;
776
- font-size: 15px;
777
  }
778
 
779
- .copy-sample:hover {
780
- color: white;
781
- background: var(--navy);
782
- border-color: var(--navy);
 
 
783
  }
784
 
785
- .no-results {
786
- margin-top: 0;
 
 
787
  }
788
 
789
- footer {
790
- min-height: 56px;
791
  display: flex;
 
792
  align-items: center;
793
- justify-content: center;
794
- color: #70828f;
795
- font-size: 13px;
796
- font-weight: 600;
797
  }
798
 
799
- .toast {
800
- position: fixed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  z-index: 30;
802
  right: 50%;
803
- bottom: 24px;
804
- padding: 12px 18px;
805
- color: white;
806
- font-size: 14px;
807
- font-weight: 700;
808
- background: #173b52;
809
- border-radius: 12px;
810
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2);
811
  opacity: 0;
812
- transform: translate(50%, 20px);
813
- transition: opacity 0.25s, transform 0.25s;
814
  pointer-events: none;
815
  }
816
 
@@ -819,174 +611,486 @@ footer {
819
  transform: translate(50%, 0);
820
  }
821
 
822
- @media (max-width: 980px) {
823
  .filters-grid {
824
- grid-template-columns: repeat(2, minmax(0, 1fr));
 
 
 
 
825
  }
826
  }
827
 
828
- @media (max-width: 760px) {
829
  .dashboard-shell {
830
- width: min(100% - 24px, 1240px);
831
  margin-top: 14px;
832
  }
833
 
834
- .welcome-panel,
835
  .results-header {
836
  align-items: stretch;
837
  flex-direction: column;
838
  }
839
 
840
- .privacy-pill {
841
  align-self: flex-start;
842
  }
843
 
844
  .samples-grid {
845
  grid-template-columns: 1fr;
846
  }
 
847
 
848
- .copy-all {
849
- width: 100%;
 
850
  }
851
 
852
- .load-more-button {
853
- width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
854
  }
855
 
856
- .results-header h2 {
857
- font-size: 23px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  }
859
  }
860
 
861
- @media (max-width: 560px) {
862
- .login-card {
863
- padding: 32px 22px;
864
- border-radius: 22px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  }
866
 
867
  .topbar {
868
- padding: 11px 14px;
 
869
  }
 
870
 
871
- .topbar-brand p {
872
- display: none;
 
873
  }
874
 
875
- .topbar-brand h1 {
876
- font-size: 16px;
 
 
877
  }
878
 
879
- .brand-mark.small {
880
- width: 42px;
881
- height: 42px;
882
  }
 
883
 
884
- .secondary-button.compact {
885
- padding: 0 12px;
886
- font-size: 13px;
887
  }
888
 
889
- .welcome-panel,
890
- .controls-card {
891
- padding: 19px;
892
- border-radius: 17px;
893
  }
894
 
895
- .controls-card {
896
- top: 6px;
897
- max-height: calc(100vh - 12px);
898
- overflow-y: auto;
899
  }
900
 
901
- .filters-grid {
902
- grid-template-columns: 1fr;
903
- gap: 13px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  }
905
 
906
  .filters-actions {
907
- margin-top: 12px;
908
  }
909
 
910
- .clear-filters-button {
911
- width: 100%;
912
- min-height: 44px;
913
- font-size: 14px;
914
  }
915
 
916
- .status-chips {
917
- flex-wrap: nowrap;
918
- overflow-x: auto;
919
- margin-left: -12px;
920
- margin-right: -12px;
921
- padding: 2px 12px 9px;
922
- scrollbar-width: thin;
923
- scroll-snap-type: x proximity;
924
  }
 
925
 
926
- .status-chip {
927
- flex: 0 0 auto;
928
- min-height: 42px;
929
- padding: 8px 14px;
930
- font-size: 14px;
931
- scroll-snap-align: start;
932
  }
933
 
934
- .search-group {
935
- grid-row: 1;
936
  }
937
 
938
- .password-field input,
939
- .search-wrap input,
940
- .select-wrap select {
941
- height: 56px;
942
- font-size: 16px;
943
  }
944
 
945
- .sample-card-header {
946
- flex-direction: column;
947
  }
948
 
949
- .status-badge {
950
- max-width: 100%;
 
951
  }
952
 
953
- .sample-details {
 
 
 
 
 
 
 
 
954
  grid-template-columns: 1fr;
955
- padding: 4px 17px;
956
  }
957
 
958
- .detail-item:nth-child(odd),
959
- .detail-item:nth-child(even) {
960
- padding-right: 0;
961
- padding-left: 0;
962
- border-left: 0;
963
  }
964
 
965
- .sample-card-header,
966
- .sample-card-footer {
967
- padding-right: 17px;
968
- padding-left: 17px;
969
  }
970
 
971
- .detail-label {
972
- font-size: 13px;
 
973
  }
974
 
975
- .detail-value {
976
  font-size: 16px;
 
977
  }
978
 
979
- .copy-sample {
980
- min-height: 48px;
981
- font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
982
  }
983
- }
984
 
985
- @media (prefers-reduced-motion: reduce) {
986
- *,
987
- *::before,
988
- *::after {
989
- scroll-behavior: auto !important;
990
- transition: none !important;
991
  }
992
  }
 
1
  :root {
2
+ --ink: #1d2935;
3
+ --muted: #667789;
4
+ --line: #d9e3e8;
5
+ --page: #f4f7f8;
 
 
 
 
 
6
  --surface: #ffffff;
7
+ --primary: #0f4c5c;
8
+ --primary-dark: #0a3440;
9
+ --accent: #1b8a6b;
10
+ --accent-soft: #e6f5ef;
11
+ --warning: #8a5a13;
12
+ --warning-soft: #fff5df;
13
  --danger: #b43d4f;
14
+ --shadow: 0 10px 28px rgba(28, 53, 68, 0.08);
15
  }
16
 
17
  * {
 
27
  min-height: 100vh;
28
  margin: 0;
29
  color: var(--ink);
 
30
  font-family: "Tajawal", Arial, sans-serif;
31
+ font-size: 16px;
32
+ background: linear-gradient(180deg, #fbfcfd 0%, var(--page) 100%);
 
33
  }
34
 
35
  button,
 
47
  }
48
 
49
  svg {
50
+ width: 20px;
51
+ height: 20px;
52
  fill: none;
53
  stroke: currentColor;
54
  stroke-linecap: round;
 
56
  stroke-width: 1.8;
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  .login-view {
60
+ min-height: calc(100vh - 52px);
61
  display: grid;
62
  place-items: center;
63
+ padding: 24px;
64
  }
65
 
66
  .login-card {
67
+ width: min(100%, 430px);
68
+ padding: 34px;
69
+ text-align: right;
70
+ background: var(--surface);
71
+ border: 1px solid var(--line);
72
+ border-radius: 8px;
73
  box-shadow: var(--shadow);
 
74
  }
75
 
76
  .brand-mark {
77
+ width: 56px;
78
+ height: 56px;
79
  display: grid;
80
  place-items: center;
81
+ margin-bottom: 18px;
82
+ color: #fff;
83
+ background: var(--primary);
84
+ border-radius: 8px;
 
85
  }
86
 
87
  .brand-mark svg {
88
+ width: 32px;
89
+ height: 32px;
 
 
 
 
 
 
 
 
90
  }
91
 
92
  .brand-mark.small {
93
+ width: 44px;
94
+ height: 44px;
95
  margin: 0;
 
 
 
 
 
 
 
96
  }
97
 
98
  .eyebrow {
99
+ margin: 0 0 6px;
100
+ color: var(--accent);
101
  font-size: 13px;
102
  font-weight: 800;
103
+ }
104
+
105
+ h1,
106
+ h2,
107
+ h3,
108
+ p {
109
+ overflow-wrap: anywhere;
110
  }
111
 
112
  .login-card h1 {
113
  margin: 0;
114
+ color: var(--primary-dark);
115
+ font-size: 28px;
116
  line-height: 1.35;
117
  }
118
 
119
  .login-description {
120
+ margin: 10px 0 24px;
 
121
  color: var(--muted);
 
122
  line-height: 1.8;
123
  }
124
 
125
+ .login-card label,
 
 
 
 
126
  .field-group label {
127
  display: block;
128
  margin-bottom: 8px;
129
+ color: #34495a;
 
130
  font-weight: 700;
131
  }
132
 
 
140
  .search-wrap input,
141
  .select-wrap select {
142
  width: 100%;
143
+ height: 50px;
144
  color: var(--ink);
145
  background: #fbfdfe;
146
  border: 1px solid var(--line);
147
+ border-radius: 8px;
 
148
  outline: none;
149
+ transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
150
  }
151
 
152
  .password-field input {
153
+ padding: 0 14px 0 48px;
154
  }
155
 
156
  .password-field input:focus,
157
  .search-wrap input:focus,
158
  .select-wrap select:focus {
159
+ background: #fff;
160
+ border-color: var(--primary);
161
+ box-shadow: 0 0 0 3px rgba(15, 76, 92, 0.12);
162
  }
163
 
164
  .icon-button {
165
  position: absolute;
166
  top: 50%;
167
+ left: 7px;
168
+ width: 36px;
169
+ height: 36px;
170
  display: grid;
171
  place-items: center;
172
  padding: 0;
173
  color: var(--muted);
174
  background: transparent;
175
  border: 0;
176
+ border-radius: 8px;
177
  transform: translateY(-50%);
178
  }
179
 
180
  .icon-button:hover {
181
+ color: var(--primary);
182
+ background: #edf5f7;
 
 
 
 
 
183
  }
184
 
185
  .form-error {
186
  min-height: 22px;
187
+ margin: 7px 2px 8px;
188
  color: var(--danger);
189
  font-size: 13px;
190
+ font-weight: 700;
191
  }
192
 
193
  .primary-button,
 
195
  display: inline-flex;
196
  align-items: center;
197
  justify-content: center;
198
+ gap: 8px;
199
+ min-height: 46px;
200
+ padding: 0 18px;
201
+ font-weight: 800;
202
+ border-radius: 8px;
 
203
  }
204
 
205
  .primary-button {
 
 
 
 
 
 
 
206
  width: 100%;
207
+ color: #fff;
208
+ background: var(--primary);
209
+ border: 0;
210
  }
211
 
212
  .primary-button:hover {
213
+ background: var(--primary-dark);
 
214
  }
215
 
216
  .primary-button:disabled {
217
  cursor: wait;
218
  opacity: 0.65;
 
 
 
 
 
 
 
219
  }
220
 
221
+ .secondary-button {
222
+ color: var(--primary);
223
+ background: #fff;
224
+ border: 1px solid var(--line);
 
 
 
 
225
  }
226
 
227
+ .secondary-button:hover {
228
+ background: #edf5f7;
 
 
229
  }
230
 
231
  .topbar {
232
+ min-height: 72px;
233
  display: flex;
234
  align-items: center;
235
  justify-content: space-between;
236
+ gap: 16px;
237
+ padding: 12px max(18px, calc((100% - 1180px) / 2));
238
+ background: rgba(255, 255, 255, 0.96);
239
  border-bottom: 1px solid var(--line);
 
 
240
  }
241
 
242
  .topbar-brand {
243
  display: flex;
244
  align-items: center;
245
+ gap: 12px;
 
 
 
 
 
 
 
246
  }
247
 
248
+ .topbar-brand p,
249
  .topbar-brand h1 {
250
  margin: 0;
 
 
251
  }
252
 
253
+ .topbar-brand p {
254
+ color: var(--accent);
255
+ font-size: 12px;
256
+ font-weight: 800;
257
  }
258
 
259
+ .topbar-brand h1 {
260
+ color: var(--primary-dark);
261
+ font-size: 20px;
262
  }
263
 
264
  .secondary-button.compact {
265
+ min-height: 40px;
266
+ padding: 0 14px;
267
  }
268
 
269
  .dashboard-shell {
270
+ width: min(1180px, calc(100% - 36px));
271
+ margin: 22px auto 40px;
272
+ }
273
+
274
+ .summary-panel,
275
+ .controls-panel,
276
+ .empty-state,
277
+ .no-results,
278
+ .sample-card {
279
+ background: var(--surface);
280
+ border: 1px solid var(--line);
281
+ border-radius: 8px;
282
+ box-shadow: var(--shadow);
283
  }
284
 
285
+ .summary-panel {
286
  display: flex;
287
  align-items: center;
288
  justify-content: space-between;
289
+ gap: 18px;
290
+ padding: 22px;
291
+ border-top: 4px solid var(--primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
 
294
+ .summary-panel h2 {
295
  margin: 0;
296
+ color: var(--primary-dark);
297
+ font-size: 24px;
298
+ line-height: 1.45;
299
  }
300
 
301
+ .counter-pill {
302
  flex: 0 0 auto;
303
+ padding: 9px 14px;
304
+ color: var(--accent);
305
+ font-weight: 800;
306
+ background: var(--accent-soft);
307
+ border: 1px solid #c3e7d8;
308
+ border-radius: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
 
311
+ .controls-panel {
312
+ margin-top: 14px;
313
+ padding: 18px;
 
 
 
 
 
 
 
314
  }
315
 
316
  .filters-grid {
317
  display: grid;
318
+ grid-template-columns: minmax(180px, 0.9fr) minmax(220px, 1fr) minmax(260px, 1.35fr);
319
+ gap: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  }
321
 
322
  .select-wrap select {
323
+ padding: 0 14px 0 40px;
324
  appearance: none;
325
  }
326
 
327
  .select-wrap > svg {
328
  position: absolute;
329
  top: 50%;
330
+ left: 13px;
 
331
  color: var(--muted);
332
  transform: translateY(-50%);
333
  pointer-events: none;
334
  }
335
 
336
  .search-wrap input {
337
+ padding: 0 42px 0 14px;
338
  }
339
 
340
  .search-wrap > svg {
341
  position: absolute;
342
  top: 50%;
343
+ right: 13px;
 
344
  color: var(--muted);
345
  transform: translateY(-50%);
346
  }
347
 
348
+ select:disabled {
 
 
349
  color: #9aa8b2;
350
+ background: #f2f5f6;
351
+ cursor: not-allowed;
352
  }
353
 
354
+ .filters-actions {
355
+ display: flex;
356
+ justify-content: flex-start;
357
+ margin-top: 12px;
 
 
 
 
358
  }
359
 
360
+ .clear-filters-button {
361
+ display: inline-flex;
362
+ align-items: center;
363
+ gap: 7px;
364
+ min-height: 38px;
365
+ padding: 0 12px;
366
+ color: var(--muted);
367
+ font-weight: 800;
368
+ background: #f8fafb;
369
+ border: 1px solid var(--line);
370
+ border-radius: 8px;
371
+ }
372
+
373
+ .clear-filters-button:hover {
374
+ color: var(--danger);
375
+ background: #fff7f8;
376
  }
377
 
378
+ .empty-state,
379
+ .no-results {
380
+ margin-top: 16px;
381
+ padding: 44px 20px;
382
+ text-align: center;
383
+ box-shadow: none;
384
  }
385
 
386
  .empty-state h3,
387
  .no-results h3 {
388
+ margin: 0 0 8px;
389
+ color: var(--primary-dark);
390
  font-size: 20px;
391
  }
392
 
 
397
  }
398
 
399
  .results-section {
400
+ margin-top: 22px;
401
  }
402
 
403
  .results-header {
404
  display: flex;
405
  align-items: center;
406
  justify-content: space-between;
407
+ gap: 16px;
408
  margin-bottom: 14px;
409
  }
410
 
411
  .results-meta {
412
  margin: 0 0 4px;
413
+ color: var(--accent);
414
  font-size: 14px;
415
  font-weight: 800;
416
  }
417
 
418
  .results-header h2 {
419
  margin: 0;
420
+ color: var(--primary-dark);
421
+ font-size: 25px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  }
423
 
424
  .samples-grid {
425
  display: grid;
426
  grid-template-columns: repeat(2, minmax(0, 1fr));
427
+ gap: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  }
429
 
430
  .sample-card {
431
  overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
 
434
  .sample-card-header {
435
  display: flex;
436
  align-items: flex-start;
437
  justify-content: space-between;
438
+ gap: 12px;
439
+ padding: 16px 18px;
440
+ background: #fbfdfe;
441
  border-bottom: 1px solid #edf2f5;
442
+ border-top: 4px solid var(--accent);
443
  }
444
 
445
  .sample-index {
446
  display: inline-block;
447
  margin-bottom: 5px;
448
+ color: var(--accent);
449
  font-size: 12px;
450
  font-weight: 800;
451
  }
452
 
453
  .sample-card h3 {
454
  margin: 0;
455
+ color: var(--primary-dark);
456
+ font-size: 18px;
457
+ line-height: 1.6;
458
  }
459
 
460
  .status-badge {
461
  flex: 0 0 auto;
462
+ max-width: 150px;
463
  padding: 7px 10px;
464
+ color: var(--warning);
465
  font-size: 12px;
466
  font-weight: 800;
467
+ line-height: 1.45;
468
  text-align: center;
469
+ background: var(--warning-soft);
470
+ border: 1px solid #f0d394;
471
+ border-radius: 8px;
472
  }
473
 
474
  .sample-details {
475
  display: grid;
476
  grid-template-columns: repeat(2, minmax(0, 1fr));
477
  gap: 0;
478
+ padding: 4px 18px 12px;
479
  }
480
 
481
  .detail-item {
 
498
  margin-bottom: 5px;
499
  color: var(--muted);
500
  font-size: 12px;
501
+ font-weight: 800;
502
  }
503
 
504
  .detail-value {
505
  display: block;
506
+ color: #253b4c;
507
+ font-weight: 700;
508
+ line-height: 1.65;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  }
510
 
511
+ .wide-detail,
512
+ .coordinates-detail {
513
+ grid-column: 1 / -1;
514
+ padding-left: 0 !important;
515
+ padding-right: 0 !important;
516
+ border-left: 0 !important;
517
  }
518
 
519
+ .ltr-value .detail-value {
520
+ direction: ltr;
521
+ text-align: right;
522
  }
523
 
524
+ .action-link {
525
+ display: inline-flex;
526
+ align-items: center;
527
+ justify-content: center;
528
+ gap: 7px;
529
+ min-height: 38px;
530
+ padding: 0 12px;
531
+ color: var(--primary);
532
+ font-weight: 800;
533
+ text-decoration: none;
534
+ background: #eef7f9;
535
+ border: 1px solid #cbe2e8;
536
+ border-radius: 8px;
537
  }
538
 
539
+ button.action-link {
540
+ font: inherit;
541
  }
542
 
543
+ .action-link:hover {
544
+ color: #fff;
545
+ background: var(--primary);
546
+ border-color: var(--primary);
 
 
 
 
 
547
  }
548
 
549
+ .contact-note {
550
+ margin: 0 0 10px;
551
+ color: #253b4c;
552
+ font-weight: 700;
553
+ line-height: 1.8;
554
+ white-space: pre-line;
555
  }
556
 
557
+ .contact-actions {
558
+ display: flex;
559
+ flex-wrap: wrap;
560
+ gap: 8px;
561
  }
562
 
563
+ .load-more-wrap {
 
564
  display: flex;
565
+ flex-direction: column;
566
  align-items: center;
567
+ gap: 8px;
568
+ margin-top: 22px;
 
 
569
  }
570
 
571
+ .load-more-button {
572
+ min-width: 190px;
573
+ }
574
+
575
+ .load-more-wrap p {
576
+ margin: 0;
577
+ color: var(--muted);
578
+ font-size: 13px;
579
+ font-weight: 700;
580
+ }
581
+
582
+ footer {
583
+ min-height: 52px;
584
+ display: flex;
585
+ align-items: center;
586
+ justify-content: center;
587
+ color: var(--muted);
588
+ font-size: 13px;
589
+ font-weight: 700;
590
+ }
591
+
592
+ .toast {
593
+ position: fixed;
594
  z-index: 30;
595
  right: 50%;
596
+ bottom: 20px;
597
+ padding: 12px 16px;
598
+ color: #fff;
599
+ font-weight: 800;
600
+ background: var(--primary-dark);
601
+ border-radius: 8px;
602
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
 
603
  opacity: 0;
604
+ transform: translate(50%, 18px);
605
+ transition: opacity 0.22s, transform 0.22s;
606
  pointer-events: none;
607
  }
608
 
 
611
  transform: translate(50%, 0);
612
  }
613
 
614
+ @media (max-width: 900px) {
615
  .filters-grid {
616
+ grid-template-columns: 1fr 1fr;
617
+ }
618
+
619
+ .search-group {
620
+ grid-column: 1 / -1;
621
  }
622
  }
623
 
624
+ @media (max-width: 720px) {
625
  .dashboard-shell {
626
+ width: min(100% - 24px, 1180px);
627
  margin-top: 14px;
628
  }
629
 
630
+ .summary-panel,
631
  .results-header {
632
  align-items: stretch;
633
  flex-direction: column;
634
  }
635
 
636
+ .counter-pill {
637
  align-self: flex-start;
638
  }
639
 
640
  .samples-grid {
641
  grid-template-columns: 1fr;
642
  }
643
+ }
644
 
645
+ @media (max-width: 560px) {
646
+ .login-view {
647
+ padding: 18px;
648
  }
649
 
650
+ .login-card {
651
+ padding: 24px;
652
+ }
653
+
654
+ .topbar {
655
+ padding: 10px 12px;
656
+ }
657
+
658
+ .topbar-brand h1 {
659
+ font-size: 16px;
660
+ }
661
+
662
+ .topbar-brand p {
663
+ display: none;
664
  }
665
 
666
+ .filters-grid {
667
+ grid-template-columns: 1fr;
668
+ }
669
+
670
+ .search-group {
671
+ grid-column: auto;
672
+ grid-row: 1;
673
+ }
674
+
675
+ .password-field input,
676
+ .search-wrap input,
677
+ .select-wrap select {
678
+ height: 54px;
679
+ }
680
+
681
+ .sample-card-header {
682
+ flex-direction: column;
683
+ }
684
+
685
+ .status-badge {
686
+ max-width: 100%;
687
+ }
688
+
689
+ .sample-details {
690
+ grid-template-columns: 1fr;
691
+ padding: 4px 16px 12px;
692
+ }
693
+
694
+ .detail-item:nth-child(odd),
695
+ .detail-item:nth-child(even) {
696
+ padding-right: 0;
697
+ padding-left: 0;
698
+ border-left: 0;
699
+ }
700
+
701
+ .contact-actions .action-link {
702
+ flex: 1 1 100%;
703
  }
704
  }
705
 
706
+ /* Final responsive polish */
707
+ :root {
708
+ --soft-shadow: 0 1px 2px rgba(15, 35, 45, 0.06), 0 14px 36px rgba(15, 35, 45, 0.08);
709
+ --lift-shadow: 0 18px 44px rgba(15, 35, 45, 0.12);
710
+ }
711
+
712
+ body {
713
+ background:
714
+ linear-gradient(180deg, rgba(15, 76, 92, 0.06) 0, rgba(15, 76, 92, 0) 260px),
715
+ linear-gradient(180deg, #fbfcfd 0%, var(--page) 100%);
716
+ }
717
+
718
+ button,
719
+ a,
720
+ input,
721
+ select {
722
+ -webkit-tap-highlight-color: transparent;
723
+ }
724
+
725
+ button:focus-visible,
726
+ a:focus-visible,
727
+ input:focus-visible,
728
+ select:focus-visible {
729
+ outline: 3px solid rgba(27, 138, 107, 0.28);
730
+ outline-offset: 2px;
731
+ }
732
+
733
+ .login-card {
734
+ position: relative;
735
+ overflow: hidden;
736
+ border-color: rgba(217, 227, 232, 0.9);
737
+ box-shadow: var(--soft-shadow);
738
+ }
739
+
740
+ .login-card::before {
741
+ content: "";
742
+ position: absolute;
743
+ inset: 0 0 auto;
744
+ height: 5px;
745
+ background: linear-gradient(90deg, var(--accent), var(--primary));
746
+ }
747
+
748
+ .brand-mark {
749
+ background: linear-gradient(145deg, var(--primary), #177b72);
750
+ box-shadow: 0 10px 24px rgba(15, 76, 92, 0.22);
751
+ }
752
+
753
+ .topbar {
754
+ position: sticky;
755
+ top: 0;
756
+ z-index: 20;
757
+ box-shadow: 0 8px 24px rgba(28, 53, 68, 0.06);
758
+ backdrop-filter: blur(14px);
759
+ }
760
+
761
+ .dashboard-shell {
762
+ margin-top: 18px;
763
+ }
764
+
765
+ .summary-panel,
766
+ .controls-panel,
767
+ .sample-card {
768
+ box-shadow: var(--soft-shadow);
769
+ }
770
+
771
+ .summary-panel {
772
+ align-items: stretch;
773
+ min-height: 150px;
774
+ padding: 24px;
775
+ border-top: 0;
776
+ border-right: 5px solid var(--primary);
777
+ }
778
+
779
+ .summary-copy {
780
+ display: flex;
781
+ flex-direction: column;
782
+ justify-content: center;
783
+ min-width: 0;
784
+ }
785
+
786
+ .summary-panel h2 {
787
+ max-width: 760px;
788
+ font-size: 27px;
789
+ }
790
+
791
+ .summary-side {
792
+ flex: 0 0 min(380px, 42%);
793
+ display: flex;
794
+ flex-direction: column;
795
+ justify-content: center;
796
+ gap: 12px;
797
+ min-width: 280px;
798
+ }
799
+
800
+ .counter-pill {
801
+ align-self: flex-start;
802
+ display: inline-flex;
803
+ align-items: center;
804
+ justify-content: center;
805
+ min-height: 40px;
806
+ padding-inline: 16px;
807
+ }
808
+
809
+ .region-stats {
810
+ display: grid;
811
+ grid-template-columns: repeat(2, minmax(0, 1fr));
812
+ gap: 8px;
813
+ }
814
+
815
+ .region-stat {
816
+ display: flex;
817
+ align-items: center;
818
+ justify-content: space-between;
819
+ gap: 8px;
820
+ min-width: 0;
821
+ min-height: 42px;
822
+ padding: 8px 10px;
823
+ color: #315161;
824
+ background: #f8fbfc;
825
+ border: 1px solid #dbe8ed;
826
+ border-radius: 8px;
827
+ font-weight: 800;
828
+ transition: transform 0.18s, border-color 0.18s, background 0.18s;
829
+ }
830
+
831
+ .region-stat:hover {
832
+ background: #eef7f9;
833
+ border-color: #c0dce4;
834
+ transform: translateY(-1px);
835
+ }
836
+
837
+ .region-stat span {
838
+ min-width: 0;
839
+ overflow: hidden;
840
+ text-overflow: ellipsis;
841
+ white-space: nowrap;
842
+ }
843
+
844
+ .region-stat strong {
845
+ flex: 0 0 auto;
846
+ min-width: 32px;
847
+ padding: 3px 7px;
848
+ color: var(--accent);
849
+ text-align: center;
850
+ background: var(--accent-soft);
851
+ border-radius: 8px;
852
+ }
853
+
854
+ .controls-panel {
855
+ position: sticky;
856
+ top: 86px;
857
+ z-index: 15;
858
+ border-color: rgba(217, 227, 232, 0.92);
859
+ }
860
+
861
+ .filters-grid {
862
+ align-items: end;
863
+ }
864
+
865
+ .password-field input,
866
+ .search-wrap input,
867
+ .select-wrap select {
868
+ min-height: 52px;
869
+ border-color: #d2dfe6;
870
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
871
+ }
872
+
873
+ .clear-filters-button,
874
+ .action-link,
875
+ .primary-button,
876
+ .secondary-button {
877
+ transition: transform 0.18s, color 0.18s, background 0.18s, border-color 0.18s, box-shadow 0.18s;
878
+ }
879
+
880
+ .clear-filters-button:hover,
881
+ .action-link:hover,
882
+ .secondary-button:hover {
883
+ transform: translateY(-1px);
884
+ }
885
+
886
+ .results-section {
887
+ scroll-margin-top: 150px;
888
+ }
889
+
890
+ .results-header {
891
+ padding: 0 2px;
892
+ }
893
+
894
+ .samples-grid {
895
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
896
+ }
897
+
898
+ .sample-card {
899
+ display: flex;
900
+ flex-direction: column;
901
+ min-height: 100%;
902
+ transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
903
+ }
904
+
905
+ .sample-card:hover {
906
+ border-color: #c1dbe2;
907
+ box-shadow: var(--lift-shadow);
908
+ transform: translateY(-2px);
909
+ }
910
+
911
+ .sample-card-header {
912
+ border-top: 0;
913
+ border-right: 4px solid var(--accent);
914
+ background:
915
+ linear-gradient(180deg, rgba(248, 252, 253, 0.98), rgba(255, 255, 255, 0.98));
916
+ }
917
+
918
+ .sample-card h3 {
919
+ font-size: 17px;
920
+ }
921
+
922
+ .status-badge {
923
+ white-space: normal;
924
+ }
925
+
926
+ .sample-details {
927
+ flex: 1;
928
+ }
929
+
930
+ .action-link {
931
+ min-height: 40px;
932
+ white-space: nowrap;
933
+ }
934
+
935
+ .load-more-button {
936
+ box-shadow: var(--soft-shadow);
937
+ }
938
+
939
+ @media (min-width: 1220px) {
940
+ .dashboard-shell {
941
+ width: min(1280px, calc(100% - 48px));
942
  }
943
 
944
  .topbar {
945
+ padding-right: max(24px, calc((100% - 1280px) / 2));
946
+ padding-left: max(24px, calc((100% - 1280px) / 2));
947
  }
948
+ }
949
 
950
+ @media (max-width: 980px) {
951
+ .summary-panel {
952
+ flex-direction: column;
953
  }
954
 
955
+ .summary-side {
956
+ flex-basis: auto;
957
+ width: 100%;
958
+ min-width: 0;
959
  }
960
 
961
+ .controls-panel {
962
+ position: static;
 
963
  }
964
+ }
965
 
966
+ @media (max-width: 720px) {
967
+ body {
968
+ background: #f6f8f9;
969
  }
970
 
971
+ .topbar {
972
+ min-height: 64px;
 
 
973
  }
974
 
975
+ .dashboard-shell {
976
+ width: min(100% - 20px, 1180px);
977
+ margin-bottom: 28px;
 
978
  }
979
 
980
+ .summary-panel {
981
+ min-height: auto;
982
+ padding: 18px;
983
+ border-right-width: 4px;
984
+ }
985
+
986
+ .summary-panel h2 {
987
+ font-size: 21px;
988
+ line-height: 1.55;
989
+ }
990
+
991
+ .region-stats {
992
+ grid-template-columns: 1fr 1fr;
993
+ }
994
+
995
+ .controls-panel {
996
+ padding: 14px;
997
  }
998
 
999
  .filters-actions {
1000
+ margin-top: 10px;
1001
  }
1002
 
1003
+ .samples-grid {
1004
+ grid-template-columns: 1fr;
1005
+ gap: 12px;
 
1006
  }
1007
 
1008
+ .sample-card:hover {
1009
+ transform: none;
 
 
 
 
 
 
1010
  }
1011
+ }
1012
 
1013
+ @media (max-width: 560px) {
1014
+ .login-card {
1015
+ width: 100%;
1016
+ padding: 26px 20px 22px;
 
 
1017
  }
1018
 
1019
+ .login-card h1 {
1020
+ font-size: 24px;
1021
  }
1022
 
1023
+ .login-description {
1024
+ font-size: 15px;
 
 
 
1025
  }
1026
 
1027
+ .topbar {
1028
+ gap: 10px;
1029
  }
1030
 
1031
+ .secondary-button.compact {
1032
+ min-width: 44px;
1033
+ padding: 0 10px;
1034
  }
1035
 
1036
+ .summary-panel,
1037
+ .controls-panel,
1038
+ .sample-card,
1039
+ .empty-state,
1040
+ .no-results {
1041
+ border-radius: 8px;
1042
+ }
1043
+
1044
+ .region-stats {
1045
  grid-template-columns: 1fr;
 
1046
  }
1047
 
1048
+ .region-stat {
1049
+ min-height: 46px;
 
 
 
1050
  }
1051
 
1052
+ .clear-filters-button {
1053
+ width: 100%;
1054
+ justify-content: center;
1055
+ min-height: 44px;
1056
  }
1057
 
1058
+ .sample-card-header {
1059
+ padding: 15px;
1060
+ border-right-width: 4px;
1061
  }
1062
 
1063
+ .sample-card h3 {
1064
  font-size: 16px;
1065
+ line-height: 1.65;
1066
  }
1067
 
1068
+ .sample-details {
1069
+ padding: 2px 15px 10px;
1070
+ }
1071
+
1072
+ .detail-item {
1073
+ padding: 12px 0;
1074
+ }
1075
+
1076
+ .detail-value,
1077
+ .contact-note {
1078
+ font-size: 15px;
1079
+ }
1080
+
1081
+ .action-link {
1082
+ min-height: 46px;
1083
+ }
1084
+
1085
+ .toast {
1086
+ right: 12px;
1087
+ left: 12px;
1088
+ bottom: 14px;
1089
+ text-align: center;
1090
+ transform: translateY(18px);
1091
  }
 
1092
 
1093
+ .toast.show {
1094
+ transform: translateY(0);
 
 
 
 
1095
  }
1096
  }