Files
tmtsaqatravelmanager/Tmt.SaqaTravel/Components/Pages/Home.razor
T
2026-07-09 18:46:23 +02:00

501 lines
33 KiB
Plaintext

@page "/"
@rendermode InteractiveServer
<PageTitle>Overview Dashboard</PageTitle>
<div class="max-w-[1600px] mx-auto pb-8">
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-end mb-[22px] gap-3">
<div>
<h2 class="text-[22px] font-bold text-[#1f2a3a] tracking-tight outline-none focus:outline-none select-none" tabindex="-1">Travel Overview</h2>
<p class="text-[13px] text-[#5a6577] mt-1">Consolidated single source of truth, May 2026 reporting period</p>
</div>
<div class="flex gap-2">
<button class="flex items-center gap-1.5 px-[14px] py-2 bg-white border border-[#e4eaf2] text-[#1f2a3a] rounded-lg text-[13px] font-semibold hover:bg-[#f4f7fb] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Export
</button>
<button class="flex items-center gap-1.5 px-[14px] py-2 bg-[#0066b3] text-white rounded-lg text-[13px] font-semibold hover:bg-[#003b71] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Booking
</button>
</div>
</div>
<!-- KPI Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Spend Card -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] overflow-hidden relative group p-[18px]">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-[#0066b3] transition-all duration-500 group-hover:w-1.5"></div>
<div class="transition-all duration-700 ease-out" style="opacity: @(_isLoaded ? "1" : "0"); transform: translateY(@(_isLoaded ? "0" : "10px"));">
<p class="text-[11px] font-semibold text-[#5a6577] tracking-[0.08em] uppercase mb-2.5">Total Travel Spend</p>
<div class="text-[26px] font-bold text-[#1f2a3a] tracking-tight leading-none">R @(_kpiSpend.ToString("N0"))</div>
<div class="flex items-center gap-1.5 mt-2 text-[12px]">
<span class="text-[#22a06b] font-semibold">▲ 4.8%</span>
<span class="text-[#8a96a8]">vs. April 2026</span>
</div>
</div>
</div>
<!-- Savings Card -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] overflow-hidden relative group p-[18px]">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-[#00a3a1] transition-all duration-500 group-hover:w-1.5"></div>
<div class="transition-all duration-700 ease-out delay-100" style="opacity: @(_isLoaded ? "1" : "0"); transform: translateY(@(_isLoaded ? "0" : "10px"));">
<p class="text-[11px] font-semibold text-[#5a6577] tracking-[0.08em] uppercase mb-2.5">Negotiated Savings</p>
<div class="text-[26px] font-bold text-[#1f2a3a] tracking-tight leading-none">R @(_kpiSavings.ToString("N0"))</div>
<div class="flex items-center gap-1.5 mt-2 text-[12px]">
<span class="text-[#22a06b] font-semibold">▲ 14.5%</span>
<span class="text-[#8a96a8]">discounted fares</span>
</div>
</div>
</div>
<!-- Travellers Card -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] overflow-hidden relative group p-[18px]">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-[#22a06b] transition-all duration-500 group-hover:w-1.5"></div>
<div class="transition-all duration-700 ease-out delay-200" style="opacity: @(_isLoaded ? "1" : "0"); transform: translateY(@(_isLoaded ? "0" : "10px"));">
<p class="text-[11px] font-semibold text-[#5a6577] tracking-[0.08em] uppercase mb-2.5">Active Travellers</p>
<div class="text-[26px] font-bold text-[#1f2a3a] tracking-tight leading-none">@(_kpiTravellers)</div>
<div class="flex items-center gap-1.5 mt-2 text-[12px]">
<span class="text-[#22a06b] font-semibold">▲ 12</span>
<span class="text-[#8a96a8]">this month</span>
</div>
</div>
</div>
<!-- Out of Policy Card -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] overflow-hidden relative group p-[18px]">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-[#f4a300] transition-all duration-500 group-hover:w-1.5"></div>
<div class="transition-all duration-700 ease-out delay-300" style="opacity: @(_isLoaded ? "1" : "0"); transform: translateY(@(_isLoaded ? "0" : "10px"));">
<p class="text-[11px] font-semibold text-[#5a6577] tracking-[0.08em] uppercase mb-2.5">Bookings Outside Policy</p>
<div class="text-[26px] font-bold text-[#1f2a3a] tracking-tight leading-none">@(_kpiPolicy)</div>
<div class="flex items-center gap-1.5 mt-2 text-[12px]">
<span class="text-[#dc2626] font-semibold">▼ 3</span>
<span class="text-[#8a96a8]">vs. last month</span>
</div>
</div>
</div>
</div>
<!-- Main Grid Row 1 (Charts) -->
<div class="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4 mb-[22px]">
<!-- DYNAMIC SPEND TREND CHART -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] p-5 flex flex-col">
<div class="flex justify-between items-center mb-4">
<div>
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Monthly Spend Trend</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">Air, accommodation and ground transport over 6 months</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Live from GDS</span>
</div>
<div class="relative flex-1 w-full min-h-[260px] flex items-end pt-4">
<!-- Y-Axis Labels -->
<div class="absolute left-0 top-0 bottom-8 flex flex-col justify-between text-[11px] text-[#8a96a8] pr-2 pb-2 z-10 transition-opacity duration-700" style="opacity: @(_isLoaded ? "1" : "0")">
<span>R @((MaxValue).ToString("N0"))k</span>
<span>R @((MaxValue * 0.75).ToString("N0"))k</span>
<span>R @((MaxValue * 0.5).ToString("N0"))k</span>
<span>R @((MaxValue * 0.25).ToString("N0"))k</span>
<span>R 0k</span>
</div>
<!-- Grid Lines -->
<div class="absolute inset-0 ml-12 flex flex-col justify-between pb-8">
<div class="border-b border-[#eef2f8] w-full h-0"></div>
<div class="border-b border-[#eef2f8] w-full h-0"></div>
<div class="border-b border-[#eef2f8] w-full h-0"></div>
<div class="border-b border-[#eef2f8] w-full h-0"></div>
<div class="border-b border-[#eef2f8] w-full h-0"></div>
</div>
<!-- Dynamic SVG Chart -->
<div class="absolute inset-0 ml-12 pb-8">
<svg class="h-full w-full overflow-visible" preserveAspectRatio="none" viewBox="0 0 @ChartWidth @ChartHeight">
<defs>
<linearGradient id="airGradient" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#0066b3" stop-opacity="0.15" />
<stop offset="100%" stop-color="#0066b3" stop-opacity="0" />
</linearGradient>
<linearGradient id="accGradient" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#00a3a1" stop-opacity="0.15" />
<stop offset="100%" stop-color="#00a3a1" stop-opacity="0" />
</linearGradient>
<linearGradient id="groundGradient" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#f4a300" stop-opacity="0.15" />
<stop offset="100%" stop-color="#f4a300" stop-opacity="0" />
</linearGradient>
</defs>
<!-- Animating Group (Draws from left to right) -->
<g class="transition-all duration-[1500ms] ease-in-out" style="clip-path: inset(0 @(_isLoaded ? "0%" : "100%") 0 0);">
<!-- Area Fills -->
<polygon fill="url(#airGradient)" points="@GetPolygonPoints(AirData)" />
<polygon fill="url(#accGradient)" points="@GetPolygonPoints(AccData)" />
<polygon fill="url(#groundGradient)" points="@GetPolygonPoints(GroundData)" />
<!-- Data Lines -->
<polyline fill="none" stroke="#f4a300" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="@GetPolylinePoints(GroundData)" />
<polyline fill="none" stroke="#00a3a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="@GetPolylinePoints(AccData)" />
<polyline fill="none" stroke="#0066b3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="@GetPolylinePoints(AirData)" />
</g>
<!-- Interactive Data Points (Popping in synced with the line drawing) -->
@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;";
<g class="group cursor-pointer transition-all duration-500 ease-out" style="opacity: @(_isLoaded ? "1" : "0"); transform: scale(@(_isLoaded ? "1" : "0")); transform-origin: @(groundCoord.X)px @(groundCoord.Y)px; @delayStyle">
<circle cx="@groundCoord.X" cy="@groundCoord.Y" r="4" fill="white" stroke="#f4a300" stroke-width="2" class="transition-all duration-200 group-hover:r-5" />
<text x="@groundCoord.X" y="@(groundCoord.Y - 10)" text-anchor="middle" fill="#1f2a3a" font-size="11" font-family="Inter" font-weight="600" class="opacity-0 group-hover:opacity-100 transition-opacity">R @GroundData[i]k</text>
</g>
<g class="group cursor-pointer transition-all duration-500 ease-out" style="opacity: @(_isLoaded ? "1" : "0"); transform: scale(@(_isLoaded ? "1" : "0")); transform-origin: @(accCoord.X)px @(accCoord.Y)px; @delayStyle">
<circle cx="@accCoord.X" cy="@accCoord.Y" r="4" fill="white" stroke="#00a3a1" stroke-width="2" class="transition-all duration-200 group-hover:r-5" />
<text x="@accCoord.X" y="@(accCoord.Y - 10)" text-anchor="middle" fill="#1f2a3a" font-size="11" font-family="Inter" font-weight="600" class="opacity-0 group-hover:opacity-100 transition-opacity">R @AccData[i]k</text>
</g>
<g class="group cursor-pointer transition-all duration-500 ease-out" style="opacity: @(_isLoaded ? "1" : "0"); transform: scale(@(_isLoaded ? "1" : "0")); transform-origin: @(airCoord.X)px @(airCoord.Y)px; @delayStyle">
<circle cx="@airCoord.X" cy="@airCoord.Y" r="4" fill="white" stroke="#0066b3" stroke-width="2" class="transition-all duration-200 group-hover:r-5" />
<text x="@airCoord.X" y="@(airCoord.Y - 10)" text-anchor="middle" fill="#1f2a3a" font-size="11" font-family="Inter" font-weight="600" class="opacity-0 group-hover:opacity-100 transition-opacity">R @AirData[i]k</text>
</g>
}
</svg>
</div>
<!-- X-Axis Labels -->
<div class="absolute bottom-0 left-12 right-0 flex justify-between text-[11px] text-[#8a96a8] transition-opacity duration-1000 delay-300" style="opacity: @(_isLoaded ? "1" : "0")">
@foreach (var month in Months)
{
<span class="w-8 text-center -ml-4">@month</span>
}
</div>
</div>
<!-- Legend -->
<div class="flex justify-center gap-6 mt-4 transition-opacity duration-1000 delay-500" style="opacity: @(_isLoaded ? "1" : "0")">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-[#0066b3]"></span>
<span class="text-[11px] text-[#5a6577]">Air</span>
</div>
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-[#00a3a1]"></span>
<span class="text-[11px] text-[#5a6577]">Accommodation</span>
</div>
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-[#f4a300]"></span>
<span class="text-[11px] text-[#5a6577]">Ground</span>
</div>
</div>
</div>
<!-- Spend by Category -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] p-5">
<div class="mb-5">
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Spend by Category</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">May 2026</p>
</div>
<div class="space-y-4 pt-2">
<div>
<div class="flex justify-between text-[12px] mb-1">
<span class="text-[#1f2a3a]">Air Travel</span>
<span class="text-[#5a6577] font-semibold transition-opacity duration-500 delay-[500ms]" style="opacity: @(_isLoaded ? "1" : "0")">R 1.24M</span>
</div>
<div class="w-full bg-[#f4f7fb] rounded-full h-1.5">
<div class="bg-[#0066b3] h-1.5 rounded-full transition-all duration-[1500ms] ease-out" style="width: @(_isLoaded ? "50%" : "0%")"></div>
</div>
</div>
<div>
<div class="flex justify-between text-[12px] mb-1">
<span class="text-[#1f2a3a]">Accommodation</span>
<span class="text-[#5a6577] font-semibold transition-opacity duration-500 delay-[600ms]" style="opacity: @(_isLoaded ? "1" : "0")">R 720k</span>
</div>
<div class="w-full bg-[#f4f7fb] rounded-full h-1.5">
<div class="bg-[#00a3a1] h-1.5 rounded-full transition-all duration-[1500ms] ease-out delay-[100ms]" style="width: @(_isLoaded ? "30%" : "0%")"></div>
</div>
</div>
<div>
<div class="flex justify-between text-[12px] mb-1">
<span class="text-[#1f2a3a]">Car Rental</span>
<span class="text-[#5a6577] font-semibold transition-opacity duration-500 delay-[700ms]" style="opacity: @(_isLoaded ? "1" : "0")">R 290k</span>
</div>
<div class="w-full bg-[#f4f7fb] rounded-full h-1.5">
<div class="bg-[#f4a300] h-1.5 rounded-full transition-all duration-[1500ms] ease-out delay-[200ms]" style="width: @(_isLoaded ? "12%" : "0%")"></div>
</div>
</div>
<div>
<div class="flex justify-between text-[12px] mb-1">
<span class="text-[#1f2a3a]">Shuttle</span>
<span class="text-[#5a6577] font-semibold transition-opacity duration-500 delay-[800ms]" style="opacity: @(_isLoaded ? "1" : "0")">R 110k</span>
</div>
<div class="w-full bg-[#f4f7fb] rounded-full h-1.5">
<div class="bg-[#003b71] h-1.5 rounded-full transition-all duration-[1500ms] ease-out delay-[300ms]" style="width: @(_isLoaded ? "5%" : "0%")"></div>
</div>
</div>
<div>
<div class="flex justify-between text-[12px] mb-1">
<span class="text-[#1f2a3a]">Other</span>
<span class="text-[#5a6577] font-semibold transition-opacity duration-500 delay-[900ms]" style="opacity: @(_isLoaded ? "1" : "0")">R 80k</span>
</div>
<div class="w-full bg-[#f4f7fb] rounded-full h-1.5">
<div class="bg-[#c0d4e6] h-1.5 rounded-full transition-all duration-[1500ms] ease-out delay-[400ms]" style="width: @(_isLoaded ? "3%" : "0%")"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Grid Row 2 (Tables) -->
<div class="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4">
<!-- Recent Bookings Table -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] p-5 flex flex-col transition-opacity duration-700 delay-300" style="opacity: @(_isLoaded ? "1" : "0")">
<div class="flex justify-between items-center mb-4">
<div>
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Recent Bookings</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">Last 5 confirmed trips</p>
</div>
<button class="px-[14px] py-2 bg-white border border-[#e4eaf2] text-[#1f2a3a] rounded-lg text-[13px] font-semibold hover:bg-[#f4f7fb] transition-colors">View all</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse min-w-[600px]">
<thead>
<tr>
<th class="text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold p-3 border-b border-[#e4eaf2] whitespace-nowrap">Traveller</th>
<th class="text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold p-3 border-b border-[#e4eaf2] whitespace-nowrap">Route / Stay</th>
<th class="text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold p-3 border-b border-[#e4eaf2] whitespace-nowrap">Type</th>
<th class="text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold p-3 border-b border-[#e4eaf2] whitespace-nowrap">Status</th>
<th class="text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold p-3 border-b border-[#e4eaf2] whitespace-nowrap">Cost</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-[#fafcfe] transition-colors">
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">JM</div>
<div><strong class="font-bold">J. Mokoena</strong><span class="block text-[#8a96a8] text-[11px]">Executive</span></div>
</div>
</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">JNB → CPT, 02 Jun</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Flight</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 3,240</td>
</tr>
<tr class="hover:bg-[#fafcfe] transition-colors">
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">NN</div>
<div><strong class="font-bold">N. Ndlovu</strong><span class="block text-[#8a96a8] text-[11px]">Manager</span></div>
</div>
</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">Premier Hotel OR Tambo</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Hotel</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 1,895</td>
</tr>
<tr class="hover:bg-[#fafcfe] transition-colors">
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">SP</div>
<div><strong class="font-bold">S. Pillay</strong><span class="block text-[#8a96a8] text-[11px]">Coordinator</span></div>
</div>
</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">Avis Group B, 3 days</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Car</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Pending</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 1,420</td>
</tr>
<tr class="hover:bg-[#fafcfe] transition-colors">
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">TM</div>
<div><strong class="font-bold">T. Maluleke</strong><span class="block text-[#8a96a8] text-[11px]">Director</span></div>
</div>
</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">JNB → LHR, 14 Jun</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Flight</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span></td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 18,940</td>
</tr>
<tr class="hover:bg-[#fafcfe] transition-colors">
<td class="p-3 text-[13px] text-[#1f2a3a]">
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">LK</div>
<div><strong class="font-bold">L. Khumalo</strong><span class="block text-[#8a96a8] text-[11px]">Officer</span></div>
</div>
</td>
<td class="p-3 text-[13px] text-[#1f2a3a]">Shuttle: Sandton ⇄ OR Tambo</td>
<td class="p-3 text-[13px]"><span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Shuttle</span></td>
<td class="p-3 text-[13px]"><span class="bg-[#fde5e8] text-[#c8102e] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Cancelled</span></td>
<td class="p-3 text-[13px] text-[#1f2a3a]">R 380</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Top Destinations -->
<div class="bg-white rounded-[10px] shadow-[0_1px_2px_rgba(15,23,42,.06)] border border-[#e4eaf2] p-5 flex flex-col transition-opacity duration-700 delay-300" style="opacity: @(_isLoaded ? "1" : "0")">
<div class="mb-4">
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Top Destinations</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">By spend, this month</p>
</div>
<div class="flex flex-col">
<div class="flex items-center justify-between py-2.5 border-b border-dashed border-[#e4eaf2]">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#00a3a1] to-[#0066b3] text-white flex items-center justify-center text-[12px] font-bold">CT</div>
<div><strong class="block text-[13px] text-[#1f2a3a]">Cape Town</strong><span class="text-[11px] text-[#8a96a8]">42 trips · ZA</span></div>
</div>
<div class="font-bold text-[#003b71] text-[13px]">R 612,400</div>
</div>
<div class="flex items-center justify-between py-2.5 border-b border-dashed border-[#e4eaf2]">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#00a3a1] to-[#0066b3] text-white flex items-center justify-center text-[12px] font-bold">DBN</div>
<div><strong class="block text-[13px] text-[#1f2a3a]">Durban</strong><span class="text-[11px] text-[#8a96a8]">28 trips · ZA</span></div>
</div>
<div class="font-bold text-[#003b71] text-[13px]">R 318,750</div>
</div>
<div class="flex items-center justify-between py-2.5 border-b border-dashed border-[#e4eaf2]">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#00a3a1] to-[#0066b3] text-white flex items-center justify-center text-[12px] font-bold">PE</div>
<div><strong class="block text-[13px] text-[#1f2a3a]">Gqeberha</strong><span class="text-[11px] text-[#8a96a8]">19 trips · ZA</span></div>
</div>
<div class="font-bold text-[#003b71] text-[13px]">R 214,300</div>
</div>
<div class="flex items-center justify-between py-2.5 border-b border-dashed border-[#e4eaf2]">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#00a3a1] to-[#0066b3] text-white flex items-center justify-center text-[12px] font-bold">LDN</div>
<div><strong class="block text-[13px] text-[#1f2a3a]">London</strong><span class="text-[11px] text-[#8a96a8]">4 trips · UK</span></div>
</div>
<div class="font-bold text-[#003b71] text-[13px]">R 188,900</div>
</div>
<div class="flex items-center justify-between py-2.5">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-[#00a3a1] to-[#0066b3] text-white flex items-center justify-center text-[12px] font-bold">NBO</div>
<div><strong class="block text-[13px] text-[#1f2a3a]">Nairobi</strong><span class="text-[11px] text-[#8a96a8]">6 trips · KE</span></div>
</div>
<div class="font-bold text-[#003b71] text-[13px]">R 142,100</div>
</div>
</div>
</div>
</div>
</div>
@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;
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<string>();
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<string>();
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);
}
}