|
| 1 | +use crate::models::{AdminScan, DailyScanCount}; |
| 2 | +use maud::{html, Markup}; |
| 3 | + |
| 4 | +pub fn admin_scans( |
| 5 | + scans: &[AdminScan], |
| 6 | + daily_counts: &[DailyScanCount], |
| 7 | + current_page: i64, |
| 8 | + total_pages: i64, |
| 9 | + days: i64, |
| 10 | +) -> Markup { |
| 11 | + let max_count = daily_counts |
| 12 | + .iter() |
| 13 | + .map(|d| d.count) |
| 14 | + .max() |
| 15 | + .unwrap_or(1) |
| 16 | + .max(1); |
| 17 | + |
| 18 | + html! { |
| 19 | + div class="mb-8" { |
| 20 | + div class="flex justify-between items-center mb-8" { |
| 21 | + h1 class="text-4xl font-black text-primary" style="letter-spacing: -0.02em;" { |
| 22 | + "SCAN LOG" |
| 23 | + } |
| 24 | + } |
| 25 | + |
| 26 | + // Timeframe selector |
| 27 | + div class="flex flex-wrap gap-2 mb-6" { |
| 28 | + @for &d in &[30, 60, 90] { |
| 29 | + @if d == days { |
| 30 | + a href={"/admin/scans?days=" (d)} class="btn-brutal-fill" { |
| 31 | + (d) " DAYS" |
| 32 | + } |
| 33 | + } @else { |
| 34 | + a href={"/admin/scans?days=" (d)} class="btn-brutal" { |
| 35 | + (d) " DAYS" |
| 36 | + } |
| 37 | + } |
| 38 | + } |
| 39 | + @if days == 0 { |
| 40 | + a href="/admin/scans?days=0" class="btn-brutal-fill" { "ALL" } |
| 41 | + } @else { |
| 42 | + a href="/admin/scans?days=0" class="btn-brutal" { "ALL" } |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + // Daily scan graph |
| 47 | + div class="card-brutal mb-8" { |
| 48 | + h2 class="text-xl font-black text-primary mb-4" { |
| 49 | + "DAILY SCANS" |
| 50 | + } |
| 51 | + @let num_days = daily_counts.len(); |
| 52 | + @let label_interval = if num_days <= 14 { 1 } else if num_days <= 31 { 7 } else if num_days <= 90 { 14 } else { 30 }; |
| 53 | + div style="overflow-x: auto;" { |
| 54 | + // Bars |
| 55 | + div style="display: flex; align-items: flex-end; height: 180px; gap: 1px; min-width: 100%;" { |
| 56 | + @for day in daily_counts { |
| 57 | + @let bar_height = if max_count > 0 { (day.count as f64 / max_count as f64 * 100.0).max(1.0) } else { 1.0 }; |
| 58 | + div |
| 59 | + style={"flex: 1; min-width: 2px; height: " (bar_height as i64) "%; background: var(--highlight);"} |
| 60 | + title={(day.date) ": " (day.count) " scans"} {} |
| 61 | + } |
| 62 | + } |
| 63 | + // Date labels |
| 64 | + div style="display: flex; gap: 1px; min-width: 100%;" { |
| 65 | + @for (i, day) in daily_counts.iter().enumerate() { |
| 66 | + div style="flex: 1; min-width: 2px; text-align: center; overflow: visible; position: relative;" { |
| 67 | + @if i % label_interval == 0 { |
| 68 | + span class="mono text-muted select-none" style="font-size: 0.55rem; position: absolute; left: 0; white-space: nowrap;" { |
| 69 | + (&day.date[5..]) |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + // Spacer for labels |
| 76 | + div style="height: 14px;" {} |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // Scan list table |
| 81 | + @if scans.is_empty() { |
| 82 | + div class="card-brutal-inset text-center" style="padding: 3rem;" { |
| 83 | + div class="text-6xl mb-6 text-muted" { |
| 84 | + i class="fa-solid fa-nfc-magnifying-glass" {} |
| 85 | + } |
| 86 | + h3 class="text-2xl font-black text-primary mb-3" { "NO SCANS" } |
| 87 | + p class="text-secondary mb-8 font-bold" { |
| 88 | + "NO SCANS RECORDED YET." |
| 89 | + } |
| 90 | + } |
| 91 | + } @else { |
| 92 | + div class="card-brutal overflow-x-auto" { |
| 93 | + table class="w-full text-sm" style="border-collapse: collapse;" { |
| 94 | + thead { |
| 95 | + tr style="border-bottom: 3px solid var(--accent-muted);" { |
| 96 | + th class="text-left py-3 px-3 font-black text-primary" { "TIMESTAMP" } |
| 97 | + th class="text-left py-3 px-3 font-black text-primary" { "LOCATION" } |
| 98 | + th class="text-left py-3 px-3 font-black text-primary" { "SCANNER" } |
| 99 | + th class="text-right py-3 px-3 font-black text-primary" { "SATS" } |
| 100 | + } |
| 101 | + } |
| 102 | + tbody { |
| 103 | + @for scan in scans { |
| 104 | + tr style="border-bottom: 1px solid var(--accent-muted);" { |
| 105 | + td class="py-2 px-3 mono text-secondary" style="white-space: nowrap;" { |
| 106 | + (scan.scanned_at.format("%Y-%m-%dT%H:%M:%S").to_string()) |
| 107 | + } |
| 108 | + td class="py-2 px-3" { |
| 109 | + a href={"/locations/" (scan.location_id)} class="font-bold text-primary hover:text-highlight" { |
| 110 | + (scan.location_name) |
| 111 | + } |
| 112 | + span class="text-xs text-muted ml-1" { |
| 113 | + "by " (scan.creator_display_name()) |
| 114 | + } |
| 115 | + } |
| 116 | + td class="py-2 px-3 font-bold mono" { |
| 117 | + (scan.scanner_display_name()) |
| 118 | + } |
| 119 | + td class="py-2 px-3 text-right mono" { |
| 120 | + @if scan.claimed_at.is_some() { |
| 121 | + span class="text-highlight font-bold" { |
| 122 | + (scan.sats_claimed()) |
| 123 | + } |
| 124 | + } @else { |
| 125 | + span class="text-muted" { "-" } |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + // Pagination |
| 135 | + @if total_pages > 1 { |
| 136 | + div class="flex justify-center items-center gap-2 mt-6" { |
| 137 | + @if current_page > 1 { |
| 138 | + a href={"/admin/scans?page=" (current_page - 1) "&days=" (days)} class="btn-brutal" { |
| 139 | + i class="fa-solid fa-chevron-left mr-1" {} |
| 140 | + "PREV" |
| 141 | + } |
| 142 | + } |
| 143 | + span class="px-4 py-2 font-bold mono text-secondary" { |
| 144 | + (current_page) " / " (total_pages) |
| 145 | + } |
| 146 | + @if current_page < total_pages { |
| 147 | + a href={"/admin/scans?page=" (current_page + 1) "&days=" (days)} class="btn-brutal" { |
| 148 | + "NEXT" |
| 149 | + i class="fa-solid fa-chevron-right ml-1" {} |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | +} |
0 commit comments