stat2025 commited on
Commit
42b7ee0
·
verified ·
1 Parent(s): 730b844

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.js +165 -23
  2. index.html +48 -9
  3. style.css +442 -10
app.js CHANGED
@@ -39,7 +39,9 @@ const elements = Object.fromEntries(
39
  "statementCount", "documentationPhotos", "photoPreviews", "documentationError",
40
  "uploadProgress", "uploadProgressBar", "cancelDocumentationButton", "saveDocumentationButton",
41
  "adminView", "adminLogoutButton", "adminUpdatedAt", "refreshAdminButton", "adminStats",
42
- "adminSearch", "adminResearcherFilter", "adminCityFilter", "adminDateFilter",
 
 
43
  "researcherProductivity", "adminRecordsCount", "adminRecords", "adminEmpty",
44
  ].map((id) => [id, document.getElementById(id)]),
45
  );
@@ -203,6 +205,38 @@ function riyadhToday() {
203
  }).format(new Date());
204
  }
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  async function syncResearcherDocumentation() {
207
  try {
208
  const result = await fetchDocumentation("status", sessionAccessCode);
@@ -240,22 +274,90 @@ function setAdminFilterOptions() {
240
  uniqueSorted(payload.rows.map((row) => row.city)),
241
  "جميع المدن",
242
  );
 
 
 
 
 
243
  }
244
 
245
  function renderAdminStats() {
246
- const uniqueRecords = latestRecordsBySample(adminRecords);
247
- const documented = payload.rows.filter((row) => uniqueRecords.has(row.sampleKey)).length;
248
- const todayCount = payload.rows.filter(
249
- (row) => uniqueRecords.get(row.sampleKey)?.documentedDate === riyadhToday(),
250
- ).length;
251
- const percentage = payload.rows.length
252
- ? ((documented / payload.rows.length) * 100).toFixed(1)
253
- : 0;
254
  elements.adminStats.replaceChildren(
255
  adminStat(payload.rows.length, "إجمالي العينات"),
256
- adminStat(documented, "تم التوثيق", `${percentage}% من الإجمالي`),
257
- adminStat(payload.rows.length - documented, "المتبقي"),
258
- adminStat(todayCount, "إنجاز اليوم", riyadhToday()),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  );
260
  }
261
 
@@ -264,10 +366,12 @@ function filteredAdminRecords() {
264
  const researcher = elements.adminResearcherFilter.value;
265
  const city = elements.adminCityFilter.value;
266
  const date = elements.adminDateFilter.value;
 
267
  return adminRecords.filter((record) => {
268
  if (researcher && record.researcher !== researcher) return false;
269
  if (city && record.city !== city) return false;
270
  if (date && record.documentedDate !== date) return false;
 
271
  if (!query) return true;
272
  return normalize(
273
  `${record.establishmentName} ${record.commercialRecord} ${record.researcher} ${record.city}`,
@@ -276,28 +380,40 @@ function filteredAdminRecords() {
276
  }
277
 
278
  function renderProductivity() {
 
279
  const totals = new Map(payload.researchers.map((item) => [item.name, item.count]));
280
  const documented = new Map();
281
- latestRecordsBySample(adminRecords).forEach((record) => {
 
282
  documented.set(record.researcher, (documented.get(record.researcher) || 0) + 1);
 
 
 
283
  });
284
  const fragment = document.createDocumentFragment();
285
- payload.researchers
286
  .map((item) => ({
287
  name: item.name,
288
  total: totals.get(item.name) || 0,
289
  done: documented.get(item.name) || 0,
 
290
  }))
291
- .sort((a, b) => b.done - a.done || a.name.localeCompare(b.name, "ar"))
292
- .forEach((item) => {
 
293
  const row = document.createElement("button");
294
  row.type = "button";
295
  row.className = "productivity-row";
296
- const percent = item.total ? Math.round((item.done / item.total) * 100) : 0;
297
  row.innerHTML = `
298
- <span class="productivity-name">${item.name}</span>
299
- <span class="productivity-numbers"><strong>${item.done}</strong> / ${item.total}</span>
300
- <span class="productivity-track"><i style="width:${percent}%"></i></span>`;
 
 
 
 
 
301
  row.addEventListener("click", () => {
302
  elements.adminResearcherFilter.value = item.name;
303
  renderAdminRecords();
@@ -309,7 +425,16 @@ function renderProductivity() {
309
 
310
  function renderAdminRecords() {
311
  const records = filteredAdminRecords();
312
- elements.adminRecordsCount.textContent = `${records.length} عملية`;
 
 
 
 
 
 
 
 
 
313
  elements.adminEmpty.hidden = records.length > 0;
314
  const fragment = document.createDocumentFragment();
315
  records.slice(0, 100).forEach((record) => {
@@ -333,6 +458,13 @@ function renderAdminRecords() {
333
  const time = document.createElement("time");
334
  time.textContent = date;
335
  heading.append(title, time);
 
 
 
 
 
 
 
336
  const details = document.createElement("div");
337
  details.className = "admin-record-details";
338
  details.append(
@@ -359,7 +491,7 @@ function renderAdminRecords() {
359
  photo.textContent = `الصورة ${index + 1}`;
360
  actions.append(photo);
361
  });
362
- item.append(heading, details, actions);
363
  fragment.append(item);
364
  });
365
  elements.adminRecords.replaceChildren(fragment);
@@ -371,12 +503,14 @@ async function loadAdminDashboard() {
371
  const result = await fetchDocumentation("admin", sessionAccessCode);
372
  adminRecords = hydrateRecordKeys(result.records || []);
373
  documentationRecords = adminRecords;
 
374
  elements.adminUpdatedAt.textContent = `آخر مزامنة: ${new Intl.DateTimeFormat("ar-SA", {
375
  timeZone: "Asia/Riyadh",
376
  dateStyle: "medium",
377
  timeStyle: "short",
378
  }).format(new Date(result.generatedAt))}`;
379
  renderAdminStats();
 
380
  renderProductivity();
381
  renderAdminRecords();
382
  } finally {
@@ -1346,8 +1480,16 @@ elements.adminLogoutButton.addEventListener("click", () => {
1346
  elements.password.focus();
1347
  });
1348
  elements.refreshAdminButton.addEventListener("click", loadAdminDashboard);
1349
- [elements.adminSearch, elements.adminResearcherFilter, elements.adminCityFilter, elements.adminDateFilter]
1350
  .forEach((control) => control.addEventListener(control.tagName === "INPUT" ? "input" : "change", renderAdminRecords));
 
 
 
 
 
 
 
 
1351
  elements.searchInput.addEventListener("input", applyFilters);
1352
  elements.statusSelect.addEventListener("change", applyFilters);
1353
  elements.locationSelect.addEventListener("change", applyFilters);
 
39
  "statementCount", "documentationPhotos", "photoPreviews", "documentationError",
40
  "uploadProgress", "uploadProgressBar", "cancelDocumentationButton", "saveDocumentationButton",
41
  "adminView", "adminLogoutButton", "adminUpdatedAt", "refreshAdminButton", "adminStats",
42
+ "adminProgressOverview", "adminTrendTotal", "adminTrendChart", "adminCityProgress",
43
+ "adminControls", "adminSearch", "adminResearcherFilter", "adminCityFilter", "adminDateFilter",
44
+ "adminStatusFilter", "clearAdminFilters", "activeResearchersCount",
45
  "researcherProductivity", "adminRecordsCount", "adminRecords", "adminEmpty",
46
  ].map((id) => [id, document.getElementById(id)]),
47
  );
 
205
  }).format(new Date());
206
  }
207
 
208
+ function dateOffset(dateValue, offset) {
209
+ const [year, month, day] = dateValue.split("-").map(Number);
210
+ const date = new Date(Date.UTC(year, month - 1, day + offset));
211
+ return [
212
+ date.getUTCFullYear(),
213
+ String(date.getUTCMonth() + 1).padStart(2, "0"),
214
+ String(date.getUTCDate()).padStart(2, "0"),
215
+ ].join("-");
216
+ }
217
+
218
+ function adminSnapshot() {
219
+ const records = [...latestRecordsBySample(adminRecords).values()];
220
+ const recordMap = new Map(records.map((record) => [record.sampleKey, record]));
221
+ const today = riyadhToday();
222
+ const yesterday = dateOffset(today, -1);
223
+ const documented = payload.rows.filter((row) => recordMap.has(row.sampleKey));
224
+ const todayRecords = records.filter((record) => record.documentedDate === today);
225
+ const yesterdayRecords = records.filter((record) => record.documentedDate === yesterday);
226
+ const activeToday = new Set(todayRecords.map((record) => record.researcher).filter(Boolean));
227
+ return {
228
+ records,
229
+ recordMap,
230
+ today,
231
+ yesterday,
232
+ documented,
233
+ todayRecords,
234
+ yesterdayRecords,
235
+ activeToday,
236
+ percentage: payload.rows.length ? (documented.length / payload.rows.length) * 100 : 0,
237
+ };
238
+ }
239
+
240
  async function syncResearcherDocumentation() {
241
  try {
242
  const result = await fetchDocumentation("status", sessionAccessCode);
 
274
  uniqueSorted(payload.rows.map((row) => row.city)),
275
  "جميع المدن",
276
  );
277
+ setSelectOptions(
278
+ elements.adminStatusFilter,
279
+ uniqueSorted(adminRecords.map((record) => record.fieldStatus)),
280
+ "جميع الحالات",
281
+ );
282
  }
283
 
284
  function renderAdminStats() {
285
+ const snapshot = adminSnapshot();
286
+ const inactiveToday = Math.max(payload.researchers.length - snapshot.activeToday.size, 0);
 
 
 
 
 
 
287
  elements.adminStats.replaceChildren(
288
  adminStat(payload.rows.length, "إجمالي العينات"),
289
+ adminStat(snapshot.documented.length, "تم التوثيق", `${snapshot.percentage.toFixed(1)}% من الإجمالي`),
290
+ adminStat(payload.rows.length - snapshot.documented.length, "المتبقي"),
291
+ adminStat(snapshot.todayRecords.length, "إنجاز اليوم", snapshot.today),
292
+ adminStat(snapshot.yesterdayRecords.length, "إنجاز أمس"),
293
+ adminStat(snapshot.activeToday.size, "باحثون أنجزوا اليوم", `${inactiveToday} دون إنجاز اليوم`),
294
+ );
295
+ }
296
+
297
+ function renderAdminOverview() {
298
+ const snapshot = adminSnapshot();
299
+ const remaining = Math.max(payload.rows.length - snapshot.documented.length, 0);
300
+ const degrees = Math.min(snapshot.percentage, 100) * 3.6;
301
+ elements.adminProgressOverview.innerHTML = `
302
+ <div class="completion-ring" style="--progress:${degrees}deg">
303
+ <div><strong>${snapshot.percentage.toFixed(1)}%</strong><span>مكتمل</span></div>
304
+ </div>
305
+ <div class="completion-legend">
306
+ <p><i class="documented-dot"></i><span>موثق</span><strong>${snapshot.documented.length}</strong></p>
307
+ <p><i class="remaining-dot"></i><span>متبقٍ</span><strong>${remaining}</strong></p>
308
+ <small>تُحتسب النسبة من التوثيق الفعلي المسجل في Google Drive.</small>
309
+ </div>`;
310
+
311
+ const days = Array.from({ length: 7 }, (_, index) => dateOffset(snapshot.today, index - 6));
312
+ const dayCounts = days.map((day) =>
313
+ snapshot.records.filter((record) => record.documentedDate === day).length
314
+ );
315
+ const maxCount = Math.max(...dayCounts, 1);
316
+ const weekTotal = dayCounts.reduce((sum, count) => sum + count, 0);
317
+ const dayFormatter = new Intl.DateTimeFormat("ar-SA", {
318
+ weekday: "short",
319
+ timeZone: "UTC",
320
+ });
321
+ elements.adminTrendTotal.textContent = `${weekTotal} توثيق`;
322
+ elements.adminTrendChart.replaceChildren(
323
+ ...days.map((day, index) => {
324
+ const item = document.createElement("div");
325
+ item.className = "trend-day";
326
+ const date = new Date(`${day}T12:00:00Z`);
327
+ const height = dayCounts[index] ? Math.max((dayCounts[index] / maxCount) * 100, 12) : 3;
328
+ item.innerHTML = `
329
+ <strong>${dayCounts[index]}</strong>
330
+ <span class="trend-bar"><i style="height:${height}%"></i></span>
331
+ <small>${dayFormatter.format(date)}</small>`;
332
+ if (day === snapshot.today) item.classList.add("today");
333
+ return item;
334
+ }),
335
+ );
336
+
337
+ const totalsByCity = new Map();
338
+ payload.rows.forEach((row) => totalsByCity.set(row.city, (totalsByCity.get(row.city) || 0) + 1));
339
+ const doneByCity = new Map();
340
+ snapshot.documented.forEach((row) => doneByCity.set(row.city, (doneByCity.get(row.city) || 0) + 1));
341
+ const cities = [...totalsByCity].map(([city, total]) => {
342
+ const done = doneByCity.get(city) || 0;
343
+ return { city, total, done, percent: total ? (done / total) * 100 : 0 };
344
+ }).sort((a, b) => b.done - a.done || b.percent - a.percent || a.city.localeCompare(b.city, "ar"));
345
+ elements.adminCityProgress.replaceChildren(
346
+ ...cities.slice(0, 6).map((item) => {
347
+ const button = document.createElement("button");
348
+ button.type = "button";
349
+ button.className = "city-progress-row";
350
+ button.innerHTML = `
351
+ <span class="city-progress-heading"><strong>${item.city || "غير محدد"}</strong><b>${item.done} / ${item.total}</b></span>
352
+ <span class="city-progress-track"><i style="width:${item.percent}%"></i></span>
353
+ <small>${item.percent.toFixed(1)}% مكتمل</small>`;
354
+ button.addEventListener("click", () => {
355
+ elements.adminCityFilter.value = item.city;
356
+ renderAdminRecords();
357
+ elements.adminControls?.scrollIntoView?.({ behavior: "smooth", block: "start" });
358
+ });
359
+ return button;
360
+ }),
361
  );
362
  }
363
 
 
366
  const researcher = elements.adminResearcherFilter.value;
367
  const city = elements.adminCityFilter.value;
368
  const date = elements.adminDateFilter.value;
369
+ const status = elements.adminStatusFilter.value;
370
  return adminRecords.filter((record) => {
371
  if (researcher && record.researcher !== researcher) return false;
372
  if (city && record.city !== city) return false;
373
  if (date && record.documentedDate !== date) return false;
374
+ if (status && record.fieldStatus !== status) return false;
375
  if (!query) return true;
376
  return normalize(
377
  `${record.establishmentName} ${record.commercialRecord} ${record.researcher} ${record.city}`,
 
380
  }
381
 
382
  function renderProductivity() {
383
+ const snapshot = adminSnapshot();
384
  const totals = new Map(payload.researchers.map((item) => [item.name, item.count]));
385
  const documented = new Map();
386
+ const today = new Map();
387
+ snapshot.records.forEach((record) => {
388
  documented.set(record.researcher, (documented.get(record.researcher) || 0) + 1);
389
+ if (record.documentedDate === snapshot.today) {
390
+ today.set(record.researcher, (today.get(record.researcher) || 0) + 1);
391
+ }
392
  });
393
  const fragment = document.createDocumentFragment();
394
+ const researchers = payload.researchers
395
  .map((item) => ({
396
  name: item.name,
397
  total: totals.get(item.name) || 0,
398
  done: documented.get(item.name) || 0,
399
+ today: today.get(item.name) || 0,
400
  }))
401
+ .sort((a, b) => b.done - a.done || b.today - a.today || a.name.localeCompare(b.name, "ar"));
402
+ elements.activeResearchersCount.textContent = `${snapshot.activeToday.size} نشط اليوم`;
403
+ researchers.forEach((item, index) => {
404
  const row = document.createElement("button");
405
  row.type = "button";
406
  row.className = "productivity-row";
407
+ const percent = item.total ? (item.done / item.total) * 100 : 0;
408
  row.innerHTML = `
409
+ <span class="productivity-rank">${index + 1}</span>
410
+ <span class="productivity-identity">
411
+ <strong class="productivity-name">${item.name}</strong>
412
+ <small>${item.today ? `${item.today} اليوم` : "دون إنجاز اليوم"} · متبقي ${item.total - item.done}</small>
413
+ </span>
414
+ <span class="productivity-numbers"><strong>${item.done}</strong><small>من ${item.total}</small></span>
415
+ <span class="productivity-track"><i style="width:${percent}%"></i></span>
416
+ <span class="productivity-percent">${percent.toFixed(1)}%</span>`;
417
  row.addEventListener("click", () => {
418
  elements.adminResearcherFilter.value = item.name;
419
  renderAdminRecords();
 
425
 
426
  function renderAdminRecords() {
427
  const records = filteredAdminRecords();
428
+ const hasFilters = Boolean(
429
+ elements.adminSearch.value ||
430
+ elements.adminResearcherFilter.value ||
431
+ elements.adminCityFilter.value ||
432
+ elements.adminDateFilter.value ||
433
+ elements.adminStatusFilter.value
434
+ );
435
+ elements.adminRecordsCount.textContent = hasFilters
436
+ ? `${records.length} من ${adminRecords.length}`
437
+ : `${records.length} عملية`;
438
  elements.adminEmpty.hidden = records.length > 0;
439
  const fragment = document.createDocumentFragment();
440
  records.slice(0, 100).forEach((record) => {
 
458
  const time = document.createElement("time");
459
  time.textContent = date;
460
  heading.append(title, time);
461
+ const badges = document.createElement("div");
462
+ badges.className = "admin-record-badges";
463
+ const statusBadge = document.createElement("span");
464
+ statusBadge.textContent = record.fieldStatus || "غير محدد";
465
+ const photoBadge = document.createElement("span");
466
+ photoBadge.textContent = `${record.photoCount || 0} صور`;
467
+ badges.append(statusBadge, photoBadge);
468
  const details = document.createElement("div");
469
  details.className = "admin-record-details";
470
  details.append(
 
491
  photo.textContent = `الصورة ${index + 1}`;
492
  actions.append(photo);
493
  });
494
+ item.append(heading, badges, details, actions);
495
  fragment.append(item);
496
  });
497
  elements.adminRecords.replaceChildren(fragment);
 
503
  const result = await fetchDocumentation("admin", sessionAccessCode);
504
  adminRecords = hydrateRecordKeys(result.records || []);
505
  documentationRecords = adminRecords;
506
+ setAdminFilterOptions();
507
  elements.adminUpdatedAt.textContent = `آخر مزامنة: ${new Intl.DateTimeFormat("ar-SA", {
508
  timeZone: "Asia/Riyadh",
509
  dateStyle: "medium",
510
  timeStyle: "short",
511
  }).format(new Date(result.generatedAt))}`;
512
  renderAdminStats();
513
+ renderAdminOverview();
514
  renderProductivity();
515
  renderAdminRecords();
516
  } finally {
 
1480
  elements.password.focus();
1481
  });
1482
  elements.refreshAdminButton.addEventListener("click", loadAdminDashboard);
1483
+ [elements.adminSearch, elements.adminResearcherFilter, elements.adminCityFilter, elements.adminDateFilter, elements.adminStatusFilter]
1484
  .forEach((control) => control.addEventListener(control.tagName === "INPUT" ? "input" : "change", renderAdminRecords));
1485
+ elements.clearAdminFilters.addEventListener("click", () => {
1486
+ elements.adminSearch.value = "";
1487
+ elements.adminResearcherFilter.value = "";
1488
+ elements.adminCityFilter.value = "";
1489
+ elements.adminDateFilter.value = "";
1490
+ elements.adminStatusFilter.value = "";
1491
+ renderAdminRecords();
1492
+ });
1493
  elements.searchInput.addEventListener("input", applyFilters);
1494
  elements.statusSelect.addEventListener("change", applyFilters);
1495
  elements.locationSelect.addEventListener("change", applyFilters);
index.html CHANGED
@@ -198,19 +198,46 @@
198
  <div class="admin-shell">
199
  <section class="admin-heading">
200
  <div>
201
- <p class="eyebrow">متابعة مباشرة</p>
202
- <h2>الإنتاجية الميدانية</h2>
203
  <p id="adminUpdatedAt">تتم مزامنة البيانات من سجل التوثيق في Google Drive.</p>
204
  </div>
205
- <button id="refreshAdminButton" class="secondary-button compact" type="button">
206
- <svg viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M6.1 9A7 7 0 0 1 18 6l2 5M17.9 15A7 7 0 0 1 6 18l-2-5" /></svg>
207
- تحديث
208
- </button>
 
 
 
209
  </section>
210
 
211
  <section id="adminStats" class="admin-stats"></section>
212
 
213
- <section class="admin-controls">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  <label class="field-group admin-search">
215
  <span>البحث</span>
216
  <div class="search-shell">
@@ -236,18 +263,30 @@
236
  <span>التاريخ</span>
237
  <input id="adminDateFilter" class="date-input" type="date" />
238
  </label>
 
 
 
 
 
 
 
 
 
 
 
239
  </section>
240
 
241
  <section class="admin-grid">
242
  <article class="admin-panel productivity-panel">
243
  <div class="admin-panel-heading">
244
- <div><p class="eyebrow">حسب الباحث</p><h3>الإنتاجية</h3></div>
 
245
  </div>
246
  <div id="researcherProductivity" class="productivity-list"></div>
247
  </article>
248
  <article class="admin-panel records-panel">
249
  <div class="admin-panel-heading">
250
- <div><p class="eyebrow">سجل التوثيق</p><h3>آخر العمليات</h3></div>
251
  <span id="adminRecordsCount"></span>
252
  </div>
253
  <div id="adminRecords" class="admin-records"></div>
 
198
  <div class="admin-shell">
199
  <section class="admin-heading">
200
  <div>
201
+ <p class="eyebrow">لوحة قيادة تشغيلية</p>
202
+ <h2>ملخص الأداء الميداني</h2>
203
  <p id="adminUpdatedAt">تتم مزامنة البيانات من سجل التوثيق في Google Drive.</p>
204
  </div>
205
+ <div class="admin-heading-actions">
206
+ <span class="sync-status"><i></i>متصل بسجل التوثيق</span>
207
+ <button id="refreshAdminButton" class="secondary-button compact" type="button">
208
+ <svg viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M6.1 9A7 7 0 0 1 18 6l2 5M17.9 15A7 7 0 0 1 6 18l-2-5" /></svg>
209
+ تحديث
210
+ </button>
211
+ </div>
212
  </section>
213
 
214
  <section id="adminStats" class="admin-stats"></section>
215
 
216
+ <section class="admin-overview-grid">
217
+ <article class="admin-panel progress-panel">
218
+ <div class="admin-panel-heading">
219
+ <div><p class="eyebrow">الموقف العام</p><h3>نسبة الإنجاز</h3></div>
220
+ </div>
221
+ <div id="adminProgressOverview" class="progress-overview"></div>
222
+ </article>
223
+
224
+ <article class="admin-panel trend-panel">
225
+ <div class="admin-panel-heading">
226
+ <div><p class="eyebrow">آخر 7 أيام</p><h3>حركة التوثيق</h3></div>
227
+ <span id="adminTrendTotal" class="panel-metric"></span>
228
+ </div>
229
+ <div id="adminTrendChart" class="trend-chart" aria-label="إنجاز آخر سبعة أيام"></div>
230
+ </article>
231
+
232
+ <article class="admin-panel city-panel">
233
+ <div class="admin-panel-heading">
234
+ <div><p class="eyebrow">التغطية الميدانية</p><h3>تقدم المدن</h3></div>
235
+ </div>
236
+ <div id="adminCityProgress" class="city-progress-list"></div>
237
+ </article>
238
+ </section>
239
+
240
+ <section id="adminControls" class="admin-controls">
241
  <label class="field-group admin-search">
242
  <span>البحث</span>
243
  <div class="search-shell">
 
263
  <span>التاريخ</span>
264
  <input id="adminDateFilter" class="date-input" type="date" />
265
  </label>
266
+ <label class="field-group">
267
+ <span>حالة التوثيق</span>
268
+ <div class="select-shell">
269
+ <select id="adminStatusFilter"><option value="">جميع الحالات</option></select>
270
+ <svg viewBox="0 0 24 24"><path d="m7 10 5 5 5-5" /></svg>
271
+ </div>
272
+ </label>
273
+ <button id="clearAdminFilters" class="clear-button admin-clear" type="button">
274
+ <svg viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12" /></svg>
275
+ مسح الفلاتر
276
+ </button>
277
  </section>
278
 
279
  <section class="admin-grid">
280
  <article class="admin-panel productivity-panel">
281
  <div class="admin-panel-heading">
282
+ <div><p class="eyebrow">ترتيب الأداء</p><h3>إنتاجية الباحثين</h3></div>
283
+ <span id="activeResearchersCount" class="panel-metric"></span>
284
  </div>
285
  <div id="researcherProductivity" class="productivity-list"></div>
286
  </article>
287
  <article class="admin-panel records-panel">
288
  <div class="admin-panel-heading">
289
+ <div><p class="eyebrow">آخر التحديثات</p><h3>سجل التوثيق</h3></div>
290
  <span id="adminRecordsCount"></span>
291
  </div>
292
  <div id="adminRecords" class="admin-records"></div>
style.css CHANGED
@@ -2256,7 +2256,7 @@ body {
2256
 
2257
  .admin-heading {
2258
  display: flex;
2259
- align-items: end;
2260
  justify-content: space-between;
2261
  gap: 20px;
2262
  }
@@ -2266,12 +2266,58 @@ body {
2266
  color: #667589;
2267
  }
2268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2269
  .admin-stats {
2270
  display: grid;
 
2271
  gap: 10px;
2272
  margin: 20px 0;
2273
  }
2274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2275
  .admin-stat span,
2276
  .admin-stat small {
2277
  display: block;
@@ -2288,13 +2334,267 @@ body {
2288
  color: #7a8797;
2289
  }
2290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2291
  .admin-controls {
2292
  display: grid;
2293
- grid-template-columns: minmax(260px, 2fr) repeat(3, minmax(150px, 1fr));
 
2294
  gap: 12px;
2295
  padding: 16px;
2296
  }
2297
 
 
 
 
 
 
2298
  .date-input {
2299
  width: 100%;
2300
  height: 48px;
@@ -2308,7 +2608,7 @@ body {
2308
 
2309
  .admin-grid {
2310
  display: grid;
2311
- grid-template-columns: minmax(280px, 0.72fr) minmax(0, 2fr);
2312
  gap: 16px;
2313
  margin-top: 16px;
2314
  align-items: start;
@@ -2347,9 +2647,9 @@ body {
2347
 
2348
  .productivity-row {
2349
  display: grid;
2350
- grid-template-columns: minmax(0, 1fr) auto;
2351
- gap: 6px 12px;
2352
- padding: 11px;
2353
  border: 1px solid #e1e8ed;
2354
  border-radius: 5px;
2355
  color: #172033;
@@ -2359,7 +2659,36 @@ body {
2359
  cursor: pointer;
2360
  }
2361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2362
  .productivity-name {
 
2363
  min-width: 0;
2364
  overflow: hidden;
2365
  text-overflow: ellipsis;
@@ -2367,17 +2696,36 @@ body {
2367
  font-weight: 800;
2368
  }
2369
 
 
 
 
 
 
 
 
 
 
 
2370
  .productivity-numbers {
 
2371
  direction: ltr;
 
2372
  color: #667589;
2373
  }
2374
 
2375
  .productivity-numbers strong {
 
2376
  color: #4137a8;
 
 
 
 
 
 
2377
  }
2378
 
2379
  .productivity-track {
2380
- grid-column: 1 / -1;
2381
  height: 5px;
2382
  overflow: hidden;
2383
  border-radius: 2px;
@@ -2387,7 +2735,15 @@ body {
2387
  .productivity-track i {
2388
  display: block;
2389
  height: 100%;
2390
- background: #4137a8;
 
 
 
 
 
 
 
 
2391
  }
2392
 
2393
  .admin-record {
@@ -2445,15 +2801,48 @@ body {
2445
  text-decoration: none;
2446
  }
2447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2448
  @media (max-width: 980px) {
2449
- .admin-controls {
 
 
 
 
2450
  grid-template-columns: 1fr 1fr;
2451
  }
2452
 
2453
- .admin-search {
2454
  grid-column: 1 / -1;
2455
  }
2456
 
 
 
 
 
 
 
 
 
 
 
 
 
2457
  .admin-grid {
2458
  grid-template-columns: 1fr;
2459
  }
@@ -2478,6 +2867,45 @@ body {
2478
 
2479
  .admin-heading {
2480
  align-items: flex-start;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2481
  }
2482
 
2483
  .admin-controls {
@@ -2497,6 +2925,10 @@ body {
2497
  grid-template-columns: 1fr 1fr;
2498
  }
2499
 
 
 
 
 
2500
  .sample-card.completed[data-documented-at]::after {
2501
  position: static;
2502
  display: block;
 
2256
 
2257
  .admin-heading {
2258
  display: flex;
2259
+ align-items: center;
2260
  justify-content: space-between;
2261
  gap: 20px;
2262
  }
 
2266
  color: #667589;
2267
  }
2268
 
2269
+ .admin-heading-actions {
2270
+ display: flex;
2271
+ align-items: center;
2272
+ gap: 10px;
2273
+ }
2274
+
2275
+ .sync-status {
2276
+ min-height: 38px;
2277
+ display: inline-flex;
2278
+ align-items: center;
2279
+ gap: 7px;
2280
+ padding: 0 11px;
2281
+ color: #08783b;
2282
+ background: #effbf4;
2283
+ border: 1px solid #bce7cd;
2284
+ border-radius: 5px;
2285
+ font-size: 12px;
2286
+ font-weight: 900;
2287
+ white-space: nowrap;
2288
+ }
2289
+
2290
+ .sync-status i {
2291
+ width: 8px;
2292
+ height: 8px;
2293
+ border-radius: 50%;
2294
+ background: #00b050;
2295
+ box-shadow: 0 0 0 4px rgba(0, 176, 80, 0.12);
2296
+ }
2297
+
2298
  .admin-stats {
2299
  display: grid;
2300
+ grid-template-columns: repeat(6, minmax(0, 1fr));
2301
  gap: 10px;
2302
  margin: 20px 0;
2303
  }
2304
 
2305
+ .admin-stat {
2306
+ min-height: 98px;
2307
+ }
2308
+
2309
+ .admin-stat:nth-child(4) {
2310
+ border-top-color: #27ced7;
2311
+ }
2312
+
2313
+ .admin-stat:nth-child(5) {
2314
+ border-top-color: #7030a0;
2315
+ }
2316
+
2317
+ .admin-stat:nth-child(6) {
2318
+ border-top-color: #42ba97;
2319
+ }
2320
+
2321
  .admin-stat span,
2322
  .admin-stat small {
2323
  display: block;
 
2334
  color: #7a8797;
2335
  }
2336
 
2337
+ .admin-overview-grid {
2338
+ display: grid;
2339
+ grid-template-columns: minmax(250px, 0.75fr) minmax(360px, 1.25fr) minmax(320px, 1fr);
2340
+ gap: 12px;
2341
+ margin-bottom: 16px;
2342
+ }
2343
+
2344
+ .admin-overview-grid .admin-panel {
2345
+ min-height: 300px;
2346
+ }
2347
+
2348
+ .panel-metric,
2349
+ #adminRecordsCount {
2350
+ padding: 5px 9px;
2351
+ color: #526174;
2352
+ background: #f3f6f8;
2353
+ border: 1px solid #dce5eb;
2354
+ border-radius: 4px;
2355
+ font-size: 12px;
2356
+ font-weight: 900;
2357
+ white-space: nowrap;
2358
+ }
2359
+
2360
+ .progress-overview {
2361
+ min-height: 225px;
2362
+ display: flex;
2363
+ flex-direction: column;
2364
+ align-items: center;
2365
+ justify-content: center;
2366
+ gap: 14px;
2367
+ }
2368
+
2369
+ .completion-ring {
2370
+ --progress: 0deg;
2371
+ width: 126px;
2372
+ aspect-ratio: 1;
2373
+ flex: 0 0 auto;
2374
+ display: grid;
2375
+ place-items: center;
2376
+ border-radius: 50%;
2377
+ background: conic-gradient(#00b050 var(--progress), #e7edf1 0);
2378
+ }
2379
+
2380
+ .completion-ring::before {
2381
+ content: "";
2382
+ grid-area: 1 / 1;
2383
+ width: 94px;
2384
+ aspect-ratio: 1;
2385
+ border-radius: 50%;
2386
+ background: #fff;
2387
+ }
2388
+
2389
+ .completion-ring > div {
2390
+ z-index: 1;
2391
+ grid-area: 1 / 1;
2392
+ text-align: center;
2393
+ }
2394
+
2395
+ .completion-ring strong,
2396
+ .completion-ring span {
2397
+ display: block;
2398
+ }
2399
+
2400
+ .completion-ring strong {
2401
+ color: #172033;
2402
+ font-size: 23px;
2403
+ }
2404
+
2405
+ .completion-ring span {
2406
+ margin-top: 2px;
2407
+ color: #667589;
2408
+ font-size: 12px;
2409
+ font-weight: 800;
2410
+ }
2411
+
2412
+ .completion-legend {
2413
+ width: 100%;
2414
+ display: grid;
2415
+ grid-template-columns: 1fr 1fr;
2416
+ column-gap: 14px;
2417
+ }
2418
+
2419
+ .completion-legend p {
2420
+ display: grid;
2421
+ grid-template-columns: 9px 1fr auto;
2422
+ align-items: center;
2423
+ gap: 8px;
2424
+ margin: 0;
2425
+ padding: 8px 0;
2426
+ border-bottom: 1px solid #e5ebef;
2427
+ }
2428
+
2429
+ .completion-legend p i {
2430
+ width: 9px;
2431
+ height: 9px;
2432
+ border-radius: 2px;
2433
+ }
2434
+
2435
+ .documented-dot {
2436
+ background: #00b050;
2437
+ }
2438
+
2439
+ .remaining-dot {
2440
+ background: #dce5eb;
2441
+ }
2442
+
2443
+ .completion-legend p span {
2444
+ color: #526174;
2445
+ font-size: 12px;
2446
+ }
2447
+
2448
+ .completion-legend p strong {
2449
+ color: #172033;
2450
+ font-size: 15px;
2451
+ }
2452
+
2453
+ .completion-legend > small {
2454
+ grid-column: 1 / -1;
2455
+ display: block;
2456
+ max-width: none;
2457
+ margin-top: 10px;
2458
+ text-align: center;
2459
+ color: #7a8797;
2460
+ font-size: 11px;
2461
+ line-height: 1.6;
2462
+ }
2463
+
2464
+ .trend-chart {
2465
+ height: 224px;
2466
+ display: grid;
2467
+ grid-template-columns: repeat(7, minmax(34px, 1fr));
2468
+ align-items: end;
2469
+ gap: 8px;
2470
+ padding-top: 16px;
2471
+ }
2472
+
2473
+ .trend-day {
2474
+ height: 100%;
2475
+ min-width: 0;
2476
+ display: grid;
2477
+ grid-template-rows: 22px 1fr 24px;
2478
+ gap: 5px;
2479
+ text-align: center;
2480
+ }
2481
+
2482
+ .trend-day > strong {
2483
+ color: #526174;
2484
+ font-size: 12px;
2485
+ }
2486
+
2487
+ .trend-bar {
2488
+ min-height: 126px;
2489
+ display: flex;
2490
+ align-items: flex-end;
2491
+ justify-content: center;
2492
+ overflow: hidden;
2493
+ border-bottom: 1px solid #dce5eb;
2494
+ }
2495
+
2496
+ .trend-bar i {
2497
+ width: min(34px, 66%);
2498
+ min-height: 3px;
2499
+ display: block;
2500
+ background: #4137a8;
2501
+ border-radius: 4px 4px 0 0;
2502
+ }
2503
+
2504
+ .trend-day.today .trend-bar i {
2505
+ background: #00b050;
2506
+ }
2507
+
2508
+ .trend-day.today > strong,
2509
+ .trend-day.today > small {
2510
+ color: #08783b;
2511
+ font-weight: 900;
2512
+ }
2513
+
2514
+ .trend-day > small {
2515
+ overflow: hidden;
2516
+ color: #7a8797;
2517
+ font-size: 11px;
2518
+ white-space: nowrap;
2519
+ }
2520
+
2521
+ .city-progress-list {
2522
+ display: grid;
2523
+ gap: 7px;
2524
+ margin-top: 12px;
2525
+ }
2526
+
2527
+ .city-progress-row {
2528
+ width: 100%;
2529
+ display: grid;
2530
+ gap: 5px;
2531
+ padding: 8px 9px;
2532
+ color: #172033;
2533
+ background: #fbfcfd;
2534
+ border: 1px solid transparent;
2535
+ border-radius: 5px;
2536
+ text-align: right;
2537
+ font: inherit;
2538
+ cursor: pointer;
2539
+ }
2540
+
2541
+ .city-progress-row:hover {
2542
+ border-color: #b8c8d3;
2543
+ background: #fff;
2544
+ }
2545
+
2546
+ .city-progress-heading {
2547
+ display: flex;
2548
+ align-items: center;
2549
+ justify-content: space-between;
2550
+ gap: 8px;
2551
+ }
2552
+
2553
+ .city-progress-heading strong {
2554
+ min-width: 0;
2555
+ overflow: hidden;
2556
+ font-size: 12px;
2557
+ text-overflow: ellipsis;
2558
+ white-space: nowrap;
2559
+ }
2560
+
2561
+ .city-progress-heading b {
2562
+ direction: ltr;
2563
+ color: #4137a8;
2564
+ font-size: 11px;
2565
+ }
2566
+
2567
+ .city-progress-track {
2568
+ height: 5px;
2569
+ overflow: hidden;
2570
+ background: #e5ebef;
2571
+ border-radius: 3px;
2572
+ }
2573
+
2574
+ .city-progress-track i {
2575
+ height: 100%;
2576
+ display: block;
2577
+ background: #42ba97;
2578
+ }
2579
+
2580
+ .city-progress-row > small {
2581
+ color: #7a8797;
2582
+ font-size: 10px;
2583
+ }
2584
+
2585
  .admin-controls {
2586
  display: grid;
2587
+ grid-template-columns: minmax(250px, 1.7fr) repeat(4, minmax(140px, 1fr)) auto;
2588
+ align-items: end;
2589
  gap: 12px;
2590
  padding: 16px;
2591
  }
2592
 
2593
+ .admin-clear {
2594
+ min-height: 48px;
2595
+ white-space: nowrap;
2596
+ }
2597
+
2598
  .date-input {
2599
  width: 100%;
2600
  height: 48px;
 
2608
 
2609
  .admin-grid {
2610
  display: grid;
2611
+ grid-template-columns: minmax(340px, 0.88fr) minmax(0, 1.6fr);
2612
  gap: 16px;
2613
  margin-top: 16px;
2614
  align-items: start;
 
2647
 
2648
  .productivity-row {
2649
  display: grid;
2650
+ grid-template-columns: 26px minmax(0, 1fr) auto;
2651
+ gap: 6px 10px;
2652
+ padding: 10px;
2653
  border: 1px solid #e1e8ed;
2654
  border-radius: 5px;
2655
  color: #172033;
 
2659
  cursor: pointer;
2660
  }
2661
 
2662
+ .productivity-row:hover,
2663
+ .productivity-row:focus-visible {
2664
+ border-color: #aebdca;
2665
+ background: #f9fbfc;
2666
+ }
2667
+
2668
+ .productivity-rank {
2669
+ width: 25px;
2670
+ height: 25px;
2671
+ display: grid;
2672
+ place-items: center;
2673
+ align-self: center;
2674
+ color: #526174;
2675
+ background: #edf2f5;
2676
+ border-radius: 4px;
2677
+ font-size: 11px;
2678
+ font-weight: 900;
2679
+ }
2680
+
2681
+ .productivity-row:nth-child(-n + 3) .productivity-rank {
2682
+ color: #fff;
2683
+ background: #4137a8;
2684
+ }
2685
+
2686
+ .productivity-identity {
2687
+ min-width: 0;
2688
+ }
2689
+
2690
  .productivity-name {
2691
+ display: block;
2692
  min-width: 0;
2693
  overflow: hidden;
2694
  text-overflow: ellipsis;
 
2696
  font-weight: 800;
2697
  }
2698
 
2699
+ .productivity-identity small {
2700
+ display: block;
2701
+ margin-top: 3px;
2702
+ overflow: hidden;
2703
+ color: #7a8797;
2704
+ font-size: 10px;
2705
+ text-overflow: ellipsis;
2706
+ white-space: nowrap;
2707
+ }
2708
+
2709
  .productivity-numbers {
2710
+ align-self: center;
2711
  direction: ltr;
2712
+ text-align: center;
2713
  color: #667589;
2714
  }
2715
 
2716
  .productivity-numbers strong {
2717
+ display: block;
2718
  color: #4137a8;
2719
+ font-size: 17px;
2720
+ }
2721
+
2722
+ .productivity-numbers small {
2723
+ display: block;
2724
+ font-size: 9px;
2725
  }
2726
 
2727
  .productivity-track {
2728
+ grid-column: 2 / 3;
2729
  height: 5px;
2730
  overflow: hidden;
2731
  border-radius: 2px;
 
2735
  .productivity-track i {
2736
  display: block;
2737
  height: 100%;
2738
+ background: #00b050;
2739
+ }
2740
+
2741
+ .productivity-percent {
2742
+ align-self: center;
2743
+ direction: ltr;
2744
+ color: #526174;
2745
+ font-size: 10px;
2746
+ font-weight: 900;
2747
  }
2748
 
2749
  .admin-record {
 
2801
  text-decoration: none;
2802
  }
2803
 
2804
+ .admin-record-badges {
2805
+ display: flex;
2806
+ flex-wrap: wrap;
2807
+ gap: 6px;
2808
+ margin-top: 9px;
2809
+ }
2810
+
2811
+ .admin-record-badges span {
2812
+ padding: 4px 7px;
2813
+ color: #526174;
2814
+ background: #f1f5f7;
2815
+ border: 1px solid #dce5eb;
2816
+ border-radius: 4px;
2817
+ font-size: 10px;
2818
+ font-weight: 900;
2819
+ }
2820
+
2821
  @media (max-width: 980px) {
2822
+ .admin-stats {
2823
+ grid-template-columns: repeat(3, 1fr);
2824
+ }
2825
+
2826
+ .admin-overview-grid {
2827
  grid-template-columns: 1fr 1fr;
2828
  }
2829
 
2830
+ .city-panel {
2831
  grid-column: 1 / -1;
2832
  }
2833
 
2834
+ .city-progress-list {
2835
+ grid-template-columns: 1fr 1fr;
2836
+ }
2837
+
2838
+ .admin-controls {
2839
+ grid-template-columns: 1fr 1fr 1fr;
2840
+ }
2841
+
2842
+ .admin-search {
2843
+ grid-column: span 2;
2844
+ }
2845
+
2846
  .admin-grid {
2847
  grid-template-columns: 1fr;
2848
  }
 
2867
 
2868
  .admin-heading {
2869
  align-items: flex-start;
2870
+ flex-direction: column;
2871
+ }
2872
+
2873
+ .admin-heading-actions {
2874
+ width: 100%;
2875
+ justify-content: space-between;
2876
+ }
2877
+
2878
+ .admin-overview-grid {
2879
+ grid-template-columns: 1fr;
2880
+ }
2881
+
2882
+ .admin-overview-grid .admin-panel {
2883
+ min-height: 0;
2884
+ }
2885
+
2886
+ .city-panel {
2887
+ grid-column: auto;
2888
+ }
2889
+
2890
+ .city-progress-list {
2891
+ grid-template-columns: 1fr;
2892
+ }
2893
+
2894
+ .progress-overview {
2895
+ min-height: 205px;
2896
+ gap: 14px;
2897
+ }
2898
+
2899
+ .completion-ring {
2900
+ width: 120px;
2901
+ }
2902
+
2903
+ .completion-ring::before {
2904
+ width: 90px;
2905
+ }
2906
+
2907
+ .trend-chart {
2908
+ gap: 4px;
2909
  }
2910
 
2911
  .admin-controls {
 
2925
  grid-template-columns: 1fr 1fr;
2926
  }
2927
 
2928
+ .admin-grid {
2929
+ grid-template-columns: 1fr;
2930
+ }
2931
+
2932
  .sample-card.completed[data-documented-at]::after {
2933
  position: static;
2934
  display: block;