Index.cshtml
18,987 bytes
| 1 | @model AdminDashboardViewModel |
|---|---|
| 2 | |
| 3 | <div class="admin-page-header"> |
| 4 | <div> |
| 5 | <h1><i class="bi bi-speedometer2 me-2"></i>@Localizer["Dashboard"]</h1> |
| 6 | <p class="lead">@Localizer["System overview and real-time statistics"]</p> |
| 7 | </div> |
| 8 | <div class="text-muted small"> |
| 9 | <i class="bi bi-clock"></i> @DateTime.UtcNow.ToString("dd MMM yyyy, HH:mm") UTC |
| 10 | </div> |
| 11 | </div> |
| 12 | |
| 13 | @if (Model.PendingPayments > 0 || Model.MarkedPaidPayments > 0 || Model.PendingInvitations > 0) |
| 14 | { |
| 15 | <div class="row g-2 mb-3"> |
| 16 | @if (Model.PendingPayments > 0) |
| 17 | { |
| 18 | <div class="col-md-4"><div class="alert alert-warning mb-0"><i class="bi bi-exclamation-triangle me-1"></i><strong>@Model.PendingPayments</strong> @Localizer["payments awaiting action"]</div></div> |
| 19 | } |
| 20 | @if (Model.MarkedPaidPayments > 0) |
| 21 | { |
| 22 | <div class="col-md-4"><div class="alert alert-info mb-0"><i class="bi bi-info-circle me-1"></i><strong>@Model.MarkedPaidPayments</strong> @Localizer["payments awaiting confirmation"]</div></div> |
| 23 | } |
| 24 | @if (Model.PendingInvitations > 0) |
| 25 | { |
| 26 | <div class="col-md-4"><div class="alert alert-info mb-0"><i class="bi bi-envelope me-1"></i><strong>@Model.PendingInvitations</strong> @Localizer["pending invitations"]</div></div> |
| 27 | } |
| 28 | </div> |
| 29 | } |
| 30 | |
| 31 | <!-- Primary metric cards --> |
| 32 | <div class="row g-3 mb-4"> |
| 33 | <div class="col-6 col-md-4 col-xl"> |
| 34 | <div class="admin-metric"> |
| 35 | <div class="admin-metric-icon bg-primary"><i class="bi bi-people-fill"></i></div> |
| 36 | <div> |
| 37 | <div class="admin-metric-value">@Model.UserCount</div> |
| 38 | <div class="admin-metric-label">@Localizer["Total Users"]</div> |
| 39 | </div> |
| 40 | </div> |
| 41 | </div> |
| 42 | <div class="col-6 col-md-4 col-xl"> |
| 43 | <div class="admin-metric"> |
| 44 | <div class="admin-metric-icon bg-success"><i class="bi bi-suitcase-lg-fill"></i></div> |
| 45 | <div> |
| 46 | <div class="admin-metric-value">@Model.TripCount</div> |
| 47 | <div class="admin-metric-label">@Localizer["Trips"]</div> |
| 48 | </div> |
| 49 | </div> |
| 50 | </div> |
| 51 | <div class="col-6 col-md-4 col-xl"> |
| 52 | <div class="admin-metric"> |
| 53 | <div class="admin-metric-icon bg-warning"><i class="bi bi-cash-coin"></i></div> |
| 54 | <div> |
| 55 | <div class="admin-metric-value">@Model.ExpenseCount</div> |
| 56 | <div class="admin-metric-label">@Localizer["Expenses"]</div> |
| 57 | </div> |
| 58 | </div> |
| 59 | </div> |
| 60 | <div class="col-6 col-md-4 col-xl"> |
| 61 | <div class="admin-metric"> |
| 62 | <div class="admin-metric-icon bg-info"><i class="bi bi-currency-euro"></i></div> |
| 63 | <div> |
| 64 | <div class="admin-metric-value">@Model.TotalExpenseAmount.ToString("N2")</div> |
| 65 | <div class="admin-metric-label">@Localizer["Total amount"]</div> |
| 66 | </div> |
| 67 | </div> |
| 68 | </div> |
| 69 | <div class="col-6 col-md-4 col-xl"> |
| 70 | <div class="admin-metric"> |
| 71 | <div class="admin-metric-icon bg-purple"><i class="bi bi-diagram-3-fill"></i></div> |
| 72 | <div> |
| 73 | <div class="admin-metric-value">@Model.SettlementCount</div> |
| 74 | <div class="admin-metric-label">@Localizer["Settlement plans"]</div> |
| 75 | </div> |
| 76 | </div> |
| 77 | </div> |
| 78 | <div class="col-6 col-md-4 col-xl"> |
| 79 | <div class="admin-metric"> |
| 80 | <div class="admin-metric-icon bg-danger"><i class="bi bi-hourglass-split"></i></div> |
| 81 | <div> |
| 82 | <div class="admin-metric-value">@Model.PendingPayments</div> |
| 83 | <div class="admin-metric-label">@Localizer["Pending payments"]</div> |
| 84 | </div> |
| 85 | </div> |
| 86 | </div> |
| 87 | </div> |
| 88 | |
| 89 | <!-- Status breakdowns row --> |
| 90 | <div class="row g-3 mb-4"> |
| 91 | <div class="col-lg-6"> |
| 92 | <div class="admin-card h-100"> |
| 93 | <div class="admin-card-header"> |
| 94 | <span><i class="bi bi-pie-chart-fill"></i>@Localizer["Trip status breakdown"]</span> |
| 95 | <span class="text-muted small">@Model.TripCount @Localizer["total"]</span> |
| 96 | </div> |
| 97 | <div class="admin-card-body"> |
| 98 | <div class="d-flex flex-wrap gap-2 mb-3"> |
| 99 | <span class="badge status-active fs-6 px-3 py-2"><i class="bi bi-circle-fill me-1"></i>@Model.ActiveTrips @Localizer["Active"]</span> |
| 100 | <span class="badge status-settled fs-6 px-3 py-2"><i class="bi bi-check2-circle me-1"></i>@Model.SettledTrips @Localizer["Settled"]</span> |
| 101 | <span class="badge status-archived fs-6 px-3 py-2"><i class="bi bi-archive me-1"></i>@Model.ArchivedTrips @Localizer["Archived"]</span> |
| 102 | </div> |
| 103 | @if (Model.TripCount > 0) |
| 104 | { |
| 105 | <div class="progress" style="height: 10px;"> |
| 106 | <div class="progress-bar bg-success" style="width: @(Model.ActiveTrips * 100 / Model.TripCount)%"></div> |
| 107 | <div class="progress-bar bg-primary" style="width: @(Model.SettledTrips * 100 / Model.TripCount)%"></div> |
| 108 | <div class="progress-bar bg-secondary" style="width: @(Model.ArchivedTrips * 100 / Model.TripCount)%"></div> |
| 109 | </div> |
| 110 | } |
| 111 | </div> |
| 112 | </div> |
| 113 | </div> |
| 114 | <div class="col-lg-6"> |
| 115 | <div class="admin-card h-100"> |
| 116 | <div class="admin-card-header"> |
| 117 | <span><i class="bi bi-diagram-3"></i>@Localizer["Settlement status"]</span> |
| 118 | <span class="text-muted small">@Model.SettlementCount @Localizer["total"]</span> |
| 119 | </div> |
| 120 | <div class="admin-card-body"> |
| 121 | <div class="d-flex flex-wrap gap-2 mb-3"> |
| 122 | <span class="badge status-pending fs-6 px-3 py-2"><i class="bi bi-hourglass me-1"></i>@Model.PendingSettlements @Localizer["Pending"]</span> |
| 123 | <span class="badge status-inprogress fs-6 px-3 py-2"><i class="bi bi-arrow-repeat me-1"></i>@Model.InProgressSettlements @Localizer["In progress"]</span> |
| 124 | <span class="badge status-completed fs-6 px-3 py-2"><i class="bi bi-check-circle me-1"></i>@Model.CompletedSettlements @Localizer["Completed"]</span> |
| 125 | </div> |
| 126 | @if (Model.SettlementCount > 0) |
| 127 | { |
| 128 | <div class="progress" style="height: 10px;"> |
| 129 | <div class="progress-bar bg-warning" style="width: @(Model.PendingSettlements * 100 / Model.SettlementCount)%"></div> |
| 130 | <div class="progress-bar bg-info" style="width: @(Model.InProgressSettlements * 100 / Model.SettlementCount)%"></div> |
| 131 | <div class="progress-bar bg-success" style="width: @(Model.CompletedSettlements * 100 / Model.SettlementCount)%"></div> |
| 132 | </div> |
| 133 | } |
| 134 | </div> |
| 135 | </div> |
| 136 | </div> |
| 137 | </div> |
| 138 | |
| 139 | <!-- Top Active Trips + Biggest Expenses --> |
| 140 | <div class="row g-3 mb-4"> |
| 141 | <div class="col-lg-6"> |
| 142 | <div class="admin-card h-100"> |
| 143 | <div class="admin-card-header"> |
| 144 | <span><i class="bi bi-trophy-fill"></i>@Localizer["Top active trips"]</span> |
| 145 | <a asp-area="Admin" asp-controller="Trips" asp-action="Index" class="btn btn-sm btn-outline-primary">@Localizer["View all"]</a> |
| 146 | </div> |
| 147 | <div class="admin-card-body p-0"> |
| 148 | @if (Model.TopActiveTrips.Any()) |
| 149 | { |
| 150 | <table class="table admin-table mb-0"> |
| 151 | <thead> |
| 152 | <tr> |
| 153 | <th>@Localizer["Trip"]</th> |
| 154 | <th class="text-center">@Localizer["Participants"]</th> |
| 155 | <th class="text-center">@Localizer["Expenses"]</th> |
| 156 | <th class="text-end">@Localizer["Total"]</th> |
| 157 | </tr> |
| 158 | </thead> |
| 159 | <tbody> |
| 160 | @foreach (var row in Model.TopActiveTrips) |
| 161 | { |
| 162 | <tr> |
| 163 | <td> |
| 164 | <a asp-area="Admin" asp-controller="Trips" asp-action="Details" asp-route-id="@row.Trip.Id" class="text-decoration-none fw-semibold">@row.Trip.Name</a> |
| 165 | @if (!string.IsNullOrEmpty(row.Trip.Destination)) |
| 166 | { |
| 167 | <br /><small class="text-muted">@row.Trip.Destination</small> |
| 168 | } |
| 169 | </td> |
| 170 | <td class="text-center"><span class="badge bg-light text-dark border">@row.ParticipantCount</span></td> |
| 171 | <td class="text-center"><span class="badge bg-light text-dark border">@row.ExpenseCount</span></td> |
| 172 | <td class="text-end fw-semibold">@row.ExpenseSum.ToString("N2")</td> |
| 173 | </tr> |
| 174 | } |
| 175 | </tbody> |
| 176 | </table> |
| 177 | } |
| 178 | else |
| 179 | { |
| 180 | <div class="admin-empty"><i class="bi bi-inbox"></i>@Localizer["No active trips yet"]</div> |
| 181 | } |
| 182 | </div> |
| 183 | </div> |
| 184 | </div> |
| 185 | <div class="col-lg-6"> |
| 186 | <div class="admin-card h-100"> |
| 187 | <div class="admin-card-header"> |
| 188 | <span><i class="bi bi-graph-up-arrow"></i>@Localizer["Biggest expenses"]</span> |
| 189 | <a asp-area="Admin" asp-controller="Expenses" asp-action="Index" class="btn btn-sm btn-outline-primary">@Localizer["View all"]</a> |
| 190 | </div> |
| 191 | <div class="admin-card-body p-0"> |
| 192 | @if (Model.BiggestExpenses.Any()) |
| 193 | { |
| 194 | <table class="table admin-table mb-0"> |
| 195 | <thead> |
| 196 | <tr> |
| 197 | <th>@Localizer["Description"]</th> |
| 198 | <th>@Localizer["Trip"]</th> |
| 199 | <th class="text-end">@Localizer["Amount"]</th> |
| 200 | </tr> |
| 201 | </thead> |
| 202 | <tbody> |
| 203 | @foreach (var e in Model.BiggestExpenses) |
| 204 | { |
| 205 | <tr> |
| 206 | <td> |
| 207 | <a asp-area="Admin" asp-controller="Expenses" asp-action="Details" asp-route-id="@e.Id" class="text-decoration-none"> |
| 208 | @(string.IsNullOrWhiteSpace(e.Description) ? Localizer["No description"].Value : e.Description) |
| 209 | </a> |
| 210 | @if (e.PaidByUser != null) |
| 211 | { |
| 212 | <br /><small class="text-muted">@Localizer["by"] @e.PaidByUser.FirstName @e.PaidByUser.LastName</small> |
| 213 | } |
| 214 | </td> |
| 215 | <td class="text-muted small">@(e.Trip?.Name ?? "—")</td> |
| 216 | <td class="text-end fw-bold">@e.Amount.ToString("N2")</td> |
| 217 | </tr> |
| 218 | } |
| 219 | </tbody> |
| 220 | </table> |
| 221 | } |
| 222 | else |
| 223 | { |
| 224 | <div class="admin-empty"><i class="bi bi-inbox"></i>@Localizer["No expenses yet"]</div> |
| 225 | } |
| 226 | </div> |
| 227 | </div> |
| 228 | </div> |
| 229 | </div> |
| 230 | |
| 231 | <!-- User stats + Activity Feed --> |
| 232 | <div class="row g-3 mb-4"> |
| 233 | <div class="col-lg-5"> |
| 234 | <div class="admin-card h-100"> |
| 235 | <div class="admin-card-header"> |
| 236 | <span><i class="bi bi-person-lines-fill"></i>@Localizer["User activity"]</span> |
| 237 | </div> |
| 238 | <div class="admin-card-body"> |
| 239 | <div class="row g-2 mb-3"> |
| 240 | <div class="col-4 text-center"> |
| 241 | <div class="fs-4 fw-bold text-primary">@Model.UserCount</div> |
| 242 | <div class="small text-muted">@Localizer["Total"]</div> |
| 243 | </div> |
| 244 | <div class="col-4 text-center"> |
| 245 | <div class="fs-4 fw-bold text-success">@Model.NewUsersLast7Days</div> |
| 246 | <div class="small text-muted">@Localizer["Active 7d"]</div> |
| 247 | </div> |
| 248 | <div class="col-4 text-center"> |
| 249 | <div class="fs-4 fw-bold text-info">@Model.NewUsersLast30Days</div> |
| 250 | <div class="small text-muted">@Localizer["Active 30d"]</div> |
| 251 | </div> |
| 252 | </div> |
| 253 | <hr /> |
| 254 | <div class="small fw-semibold text-muted mb-2 text-uppercase">@Localizer["Most active users"]</div> |
| 255 | @if (Model.TopActiveUsers.Any()) |
| 256 | { |
| 257 | <ul class="list-unstyled mb-0"> |
| 258 | @foreach (var u in Model.TopActiveUsers) |
| 259 | { |
| 260 | <li class="d-flex justify-content-between align-items-center py-1 border-bottom border-light"> |
| 261 | <div> |
| 262 | <div class="fw-semibold small">@u.FullName</div> |
| 263 | <div class="text-muted" style="font-size: 0.78rem;">@u.Email</div> |
| 264 | </div> |
| 265 | <div class="text-end"> |
| 266 | <span class="badge bg-primary">@u.ExpenseCount @Localizer["expenses"]</span> |
| 267 | </div> |
| 268 | </li> |
| 269 | } |
| 270 | </ul> |
| 271 | } |
| 272 | else |
| 273 | { |
| 274 | <div class="admin-empty"><i class="bi bi-person-slash"></i>@Localizer["No activity yet"]</div> |
| 275 | } |
| 276 | </div> |
| 277 | </div> |
| 278 | </div> |
| 279 | <div class="col-lg-7"> |
| 280 | <div class="admin-card h-100"> |
| 281 | <div class="admin-card-header"> |
| 282 | <span><i class="bi bi-activity"></i>@Localizer["Recent activity"]</span> |
| 283 | <span class="text-muted small">@Model.ActivityFeed.Count @Localizer["events"]</span> |
| 284 | </div> |
| 285 | <div class="admin-card-body"> |
| 286 | @if (Model.ActivityFeed.Any()) |
| 287 | { |
| 288 | <ul class="admin-feed"> |
| 289 | @foreach (var item in Model.ActivityFeed) |
| 290 | { |
| 291 | <li class="admin-feed-item"> |
| 292 | <div class="admin-feed-icon @item.BadgeCssClass"> |
| 293 | <i class="bi @item.IconCssClass"></i> |
| 294 | </div> |
| 295 | <div class="admin-feed-body"> |
| 296 | <div class="admin-feed-message">@item.Message</div> |
| 297 | <div class="admin-feed-date">@item.Date.ToString("dd MMM yyyy, HH:mm") UTC</div> |
| 298 | </div> |
| 299 | </li> |
| 300 | } |
| 301 | </ul> |
| 302 | } |
| 303 | else |
| 304 | { |
| 305 | <div class="admin-empty"><i class="bi bi-clock-history"></i>@Localizer["No recent activity"]</div> |
| 306 | } |
| 307 | </div> |
| 308 | </div> |
| 309 | </div> |
| 310 | </div> |
| 311 | |
| 312 | <!-- Secondary counts --> |
| 313 | <div class="row g-3 mb-4"> |
| 314 | <div class="col-6 col-md-4 col-lg-2"> |
| 315 | <div class="admin-metric"> |
| 316 | <div class="admin-metric-icon bg-info"><i class="bi bi-people"></i></div> |
| 317 | <div><div class="admin-metric-value">@Model.ParticipantCount</div><div class="admin-metric-label">@Localizer["Participants"]</div></div> |
| 318 | </div> |
| 319 | </div> |
| 320 | <div class="col-6 col-md-4 col-lg-2"> |
| 321 | <div class="admin-metric"> |
| 322 | <div class="admin-metric-icon bg-warning"><i class="bi bi-tags"></i></div> |
| 323 | <div><div class="admin-metric-value">@Model.CategoryCount</div><div class="admin-metric-label">@Localizer["Categories"]</div></div> |
| 324 | </div> |
| 325 | </div> |
| 326 | <div class="col-6 col-md-4 col-lg-2"> |
| 327 | <div class="admin-metric"> |
| 328 | <div class="admin-metric-icon bg-success"><i class="bi bi-currency-exchange"></i></div> |
| 329 | <div><div class="admin-metric-value">@Model.CurrencyCount</div><div class="admin-metric-label">@Localizer["Currencies"]</div></div> |
| 330 | </div> |
| 331 | </div> |
| 332 | <div class="col-6 col-md-4 col-lg-2"> |
| 333 | <div class="admin-metric"> |
| 334 | <div class="admin-metric-icon bg-purple"><i class="bi bi-stars"></i></div> |
| 335 | <div><div class="admin-metric-value">@Model.WishlistCount</div><div class="admin-metric-label">@Localizer["Wishlist"]</div></div> |
| 336 | </div> |
| 337 | </div> |
| 338 | <div class="col-6 col-md-4 col-lg-2"> |
| 339 | <div class="admin-metric"> |
| 340 | <div class="admin-metric-icon bg-primary"><i class="bi bi-bar-chart"></i></div> |
| 341 | <div><div class="admin-metric-value">@Model.PollCount</div><div class="admin-metric-label">@Localizer["Polls"]</div></div> |
| 342 | </div> |
| 343 | </div> |
| 344 | <div class="col-6 col-md-4 col-lg-2"> |
| 345 | <div class="admin-metric"> |
| 346 | <div class="admin-metric-icon bg-danger"><i class="bi bi-envelope"></i></div> |
| 347 | <div><div class="admin-metric-value">@Model.InvitationCount</div><div class="admin-metric-label">@Localizer["Invitations"]</div></div> |
| 348 | </div> |
| 349 | </div> |
| 350 | </div> |
| 351 | |
| 352 | <!-- Quick actions --> |
| 353 | <div class="admin-card"> |
| 354 | <div class="admin-card-header"> |
| 355 | <span><i class="bi bi-plus-circle"></i>@Localizer["Quick actions"]</span> |
| 356 | </div> |
| 357 | <div class="admin-card-body"> |
| 358 | <div class="row g-2"> |
| 359 | <div class="col-md-3 col-6"> |
| 360 | <a asp-area="Admin" asp-controller="Trips" asp-action="Create" class="btn btn-outline-primary w-100"><i class="bi bi-plus-lg me-1"></i>@Localizer["New Trip"]</a> |
| 361 | </div> |
| 362 | <div class="col-md-3 col-6"> |
| 363 | <a asp-area="Admin" asp-controller="Expenses" asp-action="Create" class="btn btn-outline-primary w-100"><i class="bi bi-plus-lg me-1"></i>@Localizer["New Expense"]</a> |
| 364 | </div> |
| 365 | <div class="col-md-3 col-6"> |
| 366 | <a asp-area="Admin" asp-controller="Currencies" asp-action="Create" class="btn btn-outline-primary w-100"><i class="bi bi-plus-lg me-1"></i>@Localizer["New Currency"]</a> |
| 367 | </div> |
| 368 | <div class="col-md-3 col-6"> |
| 369 | <a asp-area="Admin" asp-controller="BudgetCategories" asp-action="Create" class="btn btn-outline-primary w-100"><i class="bi bi-plus-lg me-1"></i>@Localizer["New Category"]</a> |
| 370 | </div> |
| 371 | </div> |
| 372 | </div> |
| 373 | </div> |
| 374 | |