@page "/" @rendermode InteractiveServer @using System @using System.Collections.Generic @using System.Linq @using System.Threading.Tasks Overview Dashboard

Travel Overview

Consolidated single source of truth, May 2026 reporting period

Total Travel Spend

R @(_kpiSpend.ToString("N0"))
▲ 4.8% vs. April 2026

Negotiated Savings

R @(_kpiSavings.ToString("N0"))
▲ 14.5% discounted fares

Active Travellers

@(_kpiTravellers)
▲ 12 this month

Bookings Outside Policy

@(_kpiPolicy)
▼ 3 vs. last month

Monthly Spend Trend

Air, accommodation and ground transport over 6 months

Live from GDS
R @((MaxValue).ToString("N0"))k R @((MaxValue * 0.75).ToString("N0"))k R @((MaxValue * 0.5).ToString("N0"))k R @((MaxValue * 0.25).ToString("N0"))k R 0k
@for (int i = 0; i < Months.Length; i++) { var groundCoord = GetCoordinate(i, GroundData[i]); var accCoord = GetCoordinate(i, AccData[i]); var airCoord = GetCoordinate(i, AirData[i]); double delaySeconds = 1.5 * ((double)i / (Months.Length - 1)); string delayStyle = $"transition-delay: {delaySeconds.ToString(System.Globalization.CultureInfo.InvariantCulture)}s;"; R @GroundData[i]k R @AccData[i]k R @AirData[i]k }
@foreach (var month in Months) { @month }
Air
Accommodation
Ground

Spend by Category

May 2026

Air Travel R 1.24M
Accommodation R 720k
Car Rental R 290k
Shuttle R 110k
Other R 80k

Recent Bookings

Last confirmed trips

@foreach (var item in _recentBookings) { }
Traveller Route / Stay Type Status Cost
@item.Initials
@item.Traveller@item.Role
@item.Details @if (item.Type == "Flight") { Flight } else if (item.Type == "Hotel") { Hotel } else if (item.Type == "Car") { Car } else if (item.Type == "Shuttle") { Shuttle } else { @item.Type } @if (item.Status == "Confirmed") { Confirmed } else if (item.Status == "Pending") { Pending } else if (item.Status == "Cancelled") { Cancelled } else { @item.Status } R @item.Cost.ToString("N0")

Top Destinations

By spend, this month

CT
Cape Town42 trips · ZA
R 612,400
DBN
Durban28 trips · ZA
R 318,750
PE
Gqeberha19 trips · ZA
R 214,300
LDN
London4 trips · UK
R 188,900
NBO
Nairobi6 trips · KE
R 142,100
@* Create Booking Modal *@ @if (_showNewBookingModal) {

New Travel Booking

This booking will be automatically synced with the global GDS network and reconciled against the corporate Lodge Card parameters.
} @code { private bool _isLoaded = false; // KPI Data variables to manage count-up animation private double _kpiSpend = 0; private double _kpiSavings = 0; private int _kpiTravellers = 0; private int _kpiPolicy = 0; // Dynamic Chart Data configuration private readonly string[] Months = { "Dec", "Jan", "Feb", "Mar", "Apr", "May" }; // HTML Data private readonly double[] AirData = { 820, 905, 870, 1020, 1150, 1240 }; private readonly double[] AccData = { 510, 560, 590, 640, 680, 720 }; private readonly double[] GroundData = { 180, 195, 210, 240, 260, 290 }; // Calculate maximums for scaling (approx 1400 based on data) private double MaxValue => 1400; // Chart dimensions mapped to SVG viewBox private double ChartWidth = 800; private double ChartHeight = 220; // Recent Bookings nested model & active list public class HomeBookingItem { public string Traveller { get; set; } = string.Empty; public string Role { get; set; } = string.Empty; public string Initials { get; set; } = string.Empty; public string Details { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; // Flight, Hotel, Car, Shuttle public string Status { get; set; } = string.Empty; // Confirmed, Pending, Cancelled public decimal Cost { get; set; } } private List _recentBookings = new() { new() { Traveller = "J. Mokoena", Role = "Executive", Initials = "JM", Details = "JNB → CPT, 02 Jun", Type = "Flight", Status = "Confirmed", Cost = 3240M }, new() { Traveller = "N. Ndlovu", Role = "Manager", Initials = "NN", Details = "Premier Hotel OR Tambo", Type = "Hotel", Status = "Confirmed", Cost = 1895M }, new() { Traveller = "S. Pillay", Role = "Coordinator", Initials = "SP", Details = "Avis Group B, 3 days", Type = "Car", Status = "Pending", Cost = 1420M }, new() { Traveller = "T. Maluleke", Role = "Director", Initials = "TM", Details = "JNB → LHR, 14 Jun", Type = "Flight", Status = "Confirmed", Cost = 18940M }, new() { Traveller = "L. Khumalo", Role = "Officer", Initials = "LK", Details = "Shuttle: Sandton ⇄ OR Tambo", Type = "Shuttle", Status = "Cancelled", Cost = 380M } }; // Modal creation state bindings private bool _showNewBookingModal = false; private string _newTraveller = "J. Mokoena"; private string _newRole = "Executive"; private string _newDetails = "JNB → CPT, 18 Jun"; private string _newType = "Flight"; private decimal _newCost = 2500M; private void OpenBookingModal() { _newTraveller = ""; _newRole = "Executive"; _newDetails = ""; _newType = "Flight"; _newCost = 2500M; _showNewBookingModal = true; } private string GetInitials(string name) { if (string.IsNullOrWhiteSpace(name)) return "??"; var parts = name.Split(new[] { ' ', '.' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) return "??"; if (parts.Length == 1) return parts[0].Substring(0, Math.Min(2, parts[0].Length)).ToUpper(); return (parts[0][0].ToString() + parts[parts.Length - 1][0].ToString()).ToUpper(); } private void CreateBooking() { if (string.IsNullOrWhiteSpace(_newTraveller)) _newTraveller = "Anonymous"; if (string.IsNullOrWhiteSpace(_newDetails)) _newDetails = "Local Trip"; var newBooking = new HomeBookingItem { Traveller = _newTraveller, Role = _newRole, Initials = GetInitials(_newTraveller), Details = _newDetails, Type = _newType, Status = "Confirmed", Cost = _newCost }; _recentBookings.Insert(0, newBooking); // Update total spend dynamically _kpiSpend += (double)_newCost; _kpiTravellers += 1; _showNewBookingModal = false; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await Task.Delay(50); _isLoaded = true; StateHasChanged(); // Run the KPI Count-up animation logic int steps = 40; double targetSpend = 2847310; double targetSavings = 412580; double targetTravellers = 238; double targetPolicy = 7; for (int i = 1; i <= steps; i++) { _kpiSpend = (targetSpend / steps) * i; _kpiSavings = (targetSavings / steps) * i; _kpiTravellers = (int)((targetTravellers / steps) * i); _kpiPolicy = (int)((targetPolicy / steps) * i); StateHasChanged(); await Task.Delay(22); // Replicates the Javascript animation timing } // Ensure exact final values _kpiSpend = targetSpend; _kpiSavings = targetSavings; _kpiTravellers = (int)targetTravellers; _kpiPolicy = (int)targetPolicy; StateHasChanged(); } } private string GetPolylinePoints(double[] data) { var points = new List(); for (int i = 0; i < data.Length; i++) { var coord = GetCoordinate(i, data[i]); points.Add($"{coord.X.ToString(System.Globalization.CultureInfo.InvariantCulture)},{coord.Y.ToString(System.Globalization.CultureInfo.InvariantCulture)}"); } return string.Join(" ", points); } private string GetPolygonPoints(double[] data) { var points = new List(); points.Add($"0,{ChartHeight.ToString(System.Globalization.CultureInfo.InvariantCulture)}"); for (int i = 0; i < data.Length; i++) { var coord = GetCoordinate(i, data[i]); points.Add($"{coord.X.ToString(System.Globalization.CultureInfo.InvariantCulture)},{coord.Y.ToString(System.Globalization.CultureInfo.InvariantCulture)}"); } points.Add($"{ChartWidth.ToString(System.Globalization.CultureInfo.InvariantCulture)},{ChartHeight.ToString(System.Globalization.CultureInfo.InvariantCulture)}"); return string.Join(" ", points); } private (double X, double Y) GetCoordinate(int index, double value) { double xStep = ChartWidth / (Months.Length - 1); double x = index * xStep; double y = ChartHeight - ((value / MaxValue) * ChartHeight); // 10% padding so max values don't clip the top of the SVG box y = y * 0.90 + (ChartHeight * 0.10); return (x, y); } }