Upload 3 files
Browse files- app.js +165 -23
- index.html +48 -9
- 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 |
-
"
|
|
|
|
|
|
|
| 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
|
| 247 |
-
const
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 293 |
const row = document.createElement("button");
|
| 294 |
row.type = "button";
|
| 295 |
row.className = "productivity-row";
|
| 296 |
-
const percent = item.total ?
|
| 297 |
row.innerHTML = `
|
| 298 |
-
<span class="productivity-
|
| 299 |
-
<span class="productivity-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 202 |
-
<h2>ال
|
| 203 |
<p id="adminUpdatedAt">تتم مزامنة البيانات من سجل التوثيق في Google Drive.</p>
|
| 204 |
</div>
|
| 205 |
-
<
|
| 206 |
-
<
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
| 209 |
</section>
|
| 210 |
|
| 211 |
<section id="adminStats" class="admin-stats"></section>
|
| 212 |
|
| 213 |
-
<section class="admin-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
| 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">
|
| 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:
|
| 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(
|
|
|
|
| 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(
|
| 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
|
| 2352 |
-
padding:
|
| 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:
|
| 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: #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2391 |
}
|
| 2392 |
|
| 2393 |
.admin-record {
|
|
@@ -2445,15 +2801,48 @@ body {
|
|
| 2445 |
text-decoration: none;
|
| 2446 |
}
|
| 2447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2448 |
@media (max-width: 980px) {
|
| 2449 |
-
.admin-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2450 |
grid-template-columns: 1fr 1fr;
|
| 2451 |
}
|
| 2452 |
|
| 2453 |
-
.
|
| 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;
|