Stable site design and layout

This commit is contained in:
2026-07-09 18:46:23 +02:00
parent e7defebed4
commit 4daa2bfca4
16 changed files with 1556 additions and 492 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
@page "/booking"
<h3>Booking</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/compliance"
<h3>Compliance</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/contact"
<h3>Contact</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/financial"
<h3>Financial</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/forex"
<h3>Forex</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/gds-sync"
<h3>GdsSync</h3>
@code {
}
+497 -3
View File
@@ -1,7 +1,501 @@
@page "/" @page "/"
@rendermode InteractiveServer
<PageTitle>Home</PageTitle> <PageTitle>Overview Dashboard</PageTitle>
<h1 class="text-3xl font-bold mb-4">Hello, world!</h1> <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>
<p>Welcome to your new app.</p> <!-- 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);
}
}
@@ -0,0 +1,7 @@
@page "/mobile"
<h3>Mobile</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/profile"
<h3>Profile</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/settings"
<h3>Settings</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/trip-reports"
<h3>TripReports</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/vas"
<h3>Vas</h3>
@code {
}
@@ -0,0 +1,7 @@
@page "/visa"
<h3>Visa</h3>
@code {
}
+760 -366
View File
File diff suppressed because it is too large Load Diff