Stable main pages

This commit is contained in:
2026-07-09 21:41:26 +02:00
parent 4daa2bfca4
commit 192772f0c2
15 changed files with 9171 additions and 190 deletions
+10 -1
View File
@@ -56,6 +56,15 @@
<span class="bg-[#00a3a1] text-white px-[7px] py-[1px] rounded-full text-[10px] font-bold">3</span> <span class="bg-[#00a3a1] text-white px-[7px] py-[1px] rounded-full text-[10px] font-bold">3</span>
</NavLink> </NavLink>
<!-- Trip Reports -->
<NavLink class="group flex items-center w-full px-3 py-2.5 text-[13px] font-medium rounded-lg text-[#b8c8df] hover:text-white hover:bg-white/5 transition-all duration-150 border-l-[3px] border-transparent [&.active]:bg-gradient-to-r [&.active]:from-[#00a3a12e] [&.active]:to-transparent [&.active]:text-white [&.active]:border-[#00a3a1] mb-0.5" href="trip-reports">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-[18px] h-[18px] mr-3 flex-shrink-0">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<path d="M14 2v6h6" /><path d="M16 13H8M16 17H8M10 9H8" />
</svg>
Trip Reports
</NavLink>
<!-- Financial Management --> <!-- Financial Management -->
<NavLink class="group flex items-center w-full px-3 py-2.5 text-[13px] font-medium rounded-lg text-[#b8c8df] hover:text-white hover:bg-white/5 transition-all duration-150 border-l-[3px] border-transparent [&.active]:bg-gradient-to-r [&.active]:from-[#00a3a12e] [&.active]:to-transparent [&.active]:text-white [&.active]:border-[#00a3a1] mb-0.5" href="financial"> <NavLink class="group flex items-center w-full px-3 py-2.5 text-[13px] font-medium rounded-lg text-[#b8c8df] hover:text-white hover:bg-white/5 transition-all duration-150 border-l-[3px] border-transparent [&.active]:bg-gradient-to-r [&.active]:from-[#00a3a12e] [&.active]:to-transparent [&.active]:text-white [&.active]:border-[#00a3a1] mb-0.5" href="financial">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-[18px] h-[18px] mr-3 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-[18px] h-[18px] mr-3 flex-shrink-0">
@@ -118,7 +127,7 @@
<div class="text-[10px] uppercase text-[#647892] tracking-[0.12em] px-3 pt-4 pb-2 font-semibold">Integrations</div> <div class="text-[10px] uppercase text-[#647892] tracking-[0.12em] px-3 pt-4 pb-2 font-semibold">Integrations</div>
<!-- SLA and Monthly Reports --> <!-- SLA and Monthly Reports -->
<NavLink class="group flex items-center w-full px-3 py-2.5 text-[13px] font-medium rounded-lg text-[#b8c8df] hover:text-white hover:bg-white/5 transition-all duration-150 border-l-[3px] border-transparent [&.active]:bg-gradient-to-r [&.active]:from-[#00a3a12e] [&.active]:to-transparent [&.active]:text-white [&.active]:border-[#00a3a1] mb-0.5" href="trip-reports"> <NavLink class="group flex items-center w-full px-3 py-2.5 text-[13px] font-medium rounded-lg text-[#b8c8df] hover:text-white hover:bg-white/5 transition-all duration-150 border-l-[3px] border-transparent [&.active]:bg-gradient-to-r [&.active]:from-[#00a3a12e] [&.active]:to-transparent [&.active]:text-white [&.active]:border-[#00a3a1] mb-0.5" href="sla-reports">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-[18px] h-[18px] mr-3 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-[18px] h-[18px] mr-3 flex-shrink-0">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /> <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<path d="M14 2v6h6" /> <path d="M14 2v6h6" />
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,571 @@
@page "/compliance" @page "/compliance"
<h3>Compliance</h3> @using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
@rendermode InteractiveServer
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
@* Page Header Area *@
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
<div class="text-left">
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">Compliance &amp; Policy Control</h2>
<p class="text-xs text-[#5a6577] mt-1">Audit travel approvals, verify National Treasury compliance, and review POPIA privacy safeguards</p>
</div>
<div class="flex gap-2">
<button @onclick="OpenPolicyLibraryModal"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer">
<!-- BookOpen SVG -->
<svg class="w-4 h-4 text-[#003b71]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.967 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
Policy Library
</button>
<button @onclick="TriggerFullComplianceAudit"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer">
<!-- Sparkles Shield SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
Run Compliance Audit
</button>
</div>
</div>
@* Alert / Feedback Notification Banner *@
@if (!string.IsNullOrEmpty(AlertFeedback))
{
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-sm text-[#1b8054] flex items-center gap-2.5 shadow-sm transition-all text-left">
<!-- Sparkles SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
<span class="font-medium">@AlertFeedback</span>
</div>
}
@* Scorecard Grid *@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Overall Compliance Rating</div>
<div class="text-2xl font-bold tracking-tight text-[#22a06b]">@OverallCompliance%</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>National Treasury compliant</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#00a3a1] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">POPIA Security Safeguards</div>
<div class="text-2xl font-bold tracking-tight text-[#00a3a1]">Fully Certified</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>ISO 27001 active framework</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#f4a300] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Bookings Outside Policy</div>
<div class="text-2xl font-bold tracking-tight text-[#f4a300]">@OutsidePolicyCount</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Requires CFO/Exec approval</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#dc2626] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Open Policy Anomalies</div>
<div class="text-2xl font-bold tracking-tight text-[#dc2626]">@AnomaliesCount</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#dc2626] font-semibold">
<span>Requires instant review</span>
</div>
</div>
</div>
@* Compliance split layout *@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
@* Card 1: Scorecard & metrics *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="mb-4">
<h3 class="text-[15px] font-bold">SAQA Travel Compliance Scorecard</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Live index measurements against SAQA policy rules and National Treasury Instruction No. 03</p>
</div>
<div class="space-y-4">
@* Metric 1 *@
<div class="space-y-1.5">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-[#1f2a3a]">POPIA Privacy Compliance</span>
<span class="font-bold text-[#22a06b]">100% compliant</span>
</div>
<p class="text-[10px] text-[#8a96a8]">AES 256 bit encryption on travel documents & traveller records at rest & in transit.</p>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#22a06b] h-1.5 rounded-full" style="width: 100%;"></div>
</div>
</div>
@* Metric 2 *@
<div class="space-y-1.5">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-[#1f2a3a]">Cabin Class Limits Adherence</span>
<span class="font-bold text-[#22a06b]">96% adherence</span>
</div>
<p class="text-[10px] text-[#8a96a8]">Economy limit enforced strictly. Exceptional premium cabins tracked & logged.</p>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#22a06b] h-1.5 rounded-full" style="width: 96%;"></div>
</div>
</div>
@* Metric 3 *@
<div class="space-y-1.5">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-[#1f2a3a]">Hotel Rate Matrix Compliance</span>
<span class="font-bold text-[#f4a300]">78% compliance</span>
</div>
<p class="text-[10px] text-[#8a96a8]">Ensures 3 quotes per booking. Handful of peak-season bookings bypassed limit matrix.</p>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#f4a300] h-1.5 rounded-full" style="width: 78%;"></div>
</div>
</div>
@* Metric 4 *@
<div class="space-y-1.5">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-[#1f2a3a]">After-Hours Support SLA Response</span>
<span class="font-bold text-[#00a3a1]">98% performance</span>
</div>
<p class="text-[10px] text-[#8a96a8]">Emergency consultant call backs resolved in less than 15 minutes.</p>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#00a3a1] h-1.5 rounded-full" style="width: 98%;"></div>
</div>
</div>
</div>
</div>
<div class="mt-6 pt-4 border-t border-[#e4eaf2] flex justify-between items-center text-xs text-[#8a96a8]">
<span>Sourced from GDS telemetry sync logs</span>
<span class="font-semibold text-[#1f2a3a]">Updated May 2026</span>
</div>
</div>
@* Card 2: Exceptions and Flags *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="flex items-center justify-between mb-4 pb-1 border-b border-[#e4eaf2]">
<div>
<h3 class="text-[15px] font-bold">Exceptions &amp; Flagged Bookings</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Real-time alerts requiring human justification &amp; executive approval</p>
</div>
<span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">Active Review</span>
</div>
<div class="space-y-3">
@foreach (var exc in ExceptionsList)
{
<div class="p-3 border rounded-lg flex items-start gap-3 transition-all hover:bg-[#fafcfe] @(exc.IsCritical ? "bg-[#fdeef0] border-[#f8b4b4]" : "bg-[#fff9ec] border-[#fbd897]")">
<div class="shrink-0 mt-0.5">
@if (exc.IsCritical)
{
<!-- Alert Triangle SVG -->
<svg class="w-5 h-5 text-[#dc2626]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
}
else
{
<!-- Info Shield SVG -->
<svg class="w-5 h-5 text-[#f4a300]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
}
</div>
<div class="flex-1">
<div class="flex justify-between items-baseline mb-1">
<h4 class="text-xs font-bold text-[#1f2a3a]">@exc.Title</h4>
<span class="text-[9px] font-mono text-[#8a96a8]">@exc.Date</span>
</div>
<p class="text-xs text-[#5a6577]">@exc.Description</p>
<p class="text-[10px] text-[#8a96a8] mt-1 font-medium italic">Justification: @exc.Justification</p>
<div class="mt-2.5 flex items-center justify-between border-t border-[#e4eaf2]/40 pt-2">
<span class="text-[10px] font-bold text-[#003b71]">Owner: @exc.Traveller</span>
<div class="flex gap-2">
@if (exc.Status == "Pending")
{
<button @onclick="() => AuthorizeException(exc.Id)"
class="px-2 py-0.5 bg-[#22a06b] hover:bg-[#1b8054] text-white text-[10px] font-bold rounded shadow-xs cursor-pointer">
Approve
</button>
<button @onclick="() => RejectException(exc.Id)"
class="px-2 py-0.5 bg-white hover:bg-gray-100 border border-gray-300 text-gray-700 text-[10px] font-bold rounded shadow-xs cursor-pointer">
Reject
</button>
}
else
{
<span class="px-2 py-0.5 rounded text-[10px] font-bold @(exc.Status == "Approved" ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700")">
@exc.Status
</span>
}
</div>
</div>
</div>
</div>
}
</div>
</div>
<div class="mt-4 pt-3 border-t border-[#e4eaf2] text-center">
<p class="text-[11px] text-[#8a96a8]">Flagged records automatically route alerts to executive dashboards upon booking completion.</p>
</div>
</div>
</div>
@* Compliance and Policy Library Grid Sections *@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 mb-6">
@* Security compliance cards - left side (lg:col-span-8) *@
<div class="lg:col-span-8 bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="flex items-center justify-between mb-4 border-b border-[#e4eaf2] pb-3">
<div>
<h3 class="text-base font-bold text-[#003b71]">Travel Data Privacy &amp; Cyber Safeguards</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">POPIA compliance rules and robust data protection standards configured across platforms</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">ISO 27001</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
@* Safeguard 1 *@
<div class="p-3.5 border border-[#e4eaf2] rounded-xl bg-[#f4f7fb]/40">
<div class="h-9 w-9 rounded-lg bg-[#e2eefb] text-[#0066b3] flex items-center justify-center mb-3">
<!-- Key SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] uppercase tracking-wide">AES 256 Data Encryption</h4>
<p class="text-[11px] text-[#5a6577] mt-1.5">All passports, reward accounts, and credit card frameworks are shielded securely using military-grade AES-256 standard protocols.</p>
</div>
@* Safeguard 2 *@
<div class="p-3.5 border border-[#e4eaf2] rounded-xl bg-[#f4f7fb]/40">
<div class="h-9 w-9 rounded-lg bg-[#e3f6ed] text-[#22a06b] flex items-center justify-center mb-3">
<!-- ShieldCheck SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] uppercase tracking-wide">POPIA Information Safeguards</h4>
<p class="text-[11px] text-[#5a6577] mt-1.5">Strict privacy protocols verified. Consent tracking is logged on every user profile activation, complying with South African privacy laws.</p>
</div>
@* Safeguard 3 *@
<div class="p-3.5 border border-[#e4eaf2] rounded-xl bg-[#f4f7fb]/40">
<div class="h-9 w-9 rounded-lg bg-[#e0f7f6] text-[#00a3a1] flex items-center justify-center mb-3">
<!-- History SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] uppercase tracking-wide">7-Year Audit Trail Retained</h4>
<p class="text-[11px] text-[#5a6577] mt-1.5">Every booking, approval, modifier, or exception justification is timestamped and archived safely for external AGSA auditing purposes.</p>
</div>
@* Safeguard 4 *@
<div class="p-3.5 border border-[#e4eaf2] rounded-xl bg-[#f4f7fb]/40">
<div class="h-9 w-9 rounded-lg bg-[#fff4dd] text-[#f4a300] flex items-center justify-center mb-3">
<!-- Locked SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] uppercase tracking-wide">ISO 27001 Certified Host</h4>
<p class="text-[11px] text-[#5a6577] mt-1.5">Data is hosted inside secure data centers. Monitored 24/7 with strict failover controls.</p>
</div>
</div>
</div>
@* Compliance reports generator - right side (lg:col-span-4) *@
<div class="lg:col-span-4 bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="mb-4">
<h3 class="text-[15px] font-bold">Standard SLA Report Outputs</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Export standard compliance matrices & cost containment breakdowns</p>
</div>
<div class="space-y-3">
@* Report row 1 *@
<div class="flex items-center justify-between p-2 rounded bg-[#f4f7fb] border border-[#e4eaf2]">
<div class="text-left">
<div class="text-xs font-bold text-[#1f2a3a]">National Treasury Matrix</div>
<span class="text-[10px] text-[#8a96a8]">Instruction 03 compliance logs</span>
</div>
<button @onclick='() => TriggerReportDownload("National Treasury Matrix")'
class="px-3 py-1 bg-white hover:bg-gray-50 text-xs font-bold border border-gray-300 text-[#1f2a3a] rounded transition">
Excel
</button>
</div>
@* Report row 2 *@
<div class="flex items-center justify-between p-2 rounded bg-[#f4f7fb] border border-[#e4eaf2]">
<div class="text-left">
<div class="text-xs font-bold text-[#1f2a3a]">POPIA Privacy Compliance Audit</div>
<span class="text-[10px] text-[#8a96a8]">Access log validation reports</span>
</div>
<button @onclick='() => TriggerReportDownload("POPIA Privacy Compliance Audit")'
class="px-3 py-1 bg-white hover:bg-gray-50 text-xs font-bold border border-gray-300 text-[#1f2a3a] rounded transition">
PDF
</button>
</div>
@* Report row 3 *@
<div class="flex items-center justify-between p-2 rounded bg-[#f4f7fb] border border-[#e4eaf2]">
<div class="text-left">
<div class="text-xs font-bold text-[#1f2a3a]">Executive Class Upgrade Log</div>
<span class="text-[10px] text-[#8a96a8]">Exemptions log with justifications</span>
</div>
<button @onclick='() => TriggerReportDownload("Executive Class Upgrade Log")'
class="px-3 py-1 bg-white hover:bg-gray-50 text-xs font-bold border border-gray-300 text-[#1f2a3a] rounded transition">
PDF
</button>
</div>
@* Report row 4 *@
<div class="flex items-center justify-between p-2 rounded bg-[#f4f7fb] border border-[#e4eaf2]">
<div class="text-left">
<div class="text-xs font-bold text-[#1f2a3a]">Standard Monthly SLA Review</div>
<span class="text-[10px] text-[#8a96a8]">Performance vs target response matrix</span>
</div>
<button @onclick='() => TriggerReportDownload("Standard Monthly SLA Review")'
class="px-3 py-1 bg-white hover:bg-gray-50 text-xs font-bold border border-gray-300 text-[#1f2a3a] rounded transition">
Excel
</button>
</div>
</div>
</div>
<div class="mt-4 pt-3.5 border-t border-[#e4eaf2]">
<p class="text-[10px] text-[#8a96a8]">Report metrics cover SAQA travel spend data for the May 2026 reporting cycle.</p>
</div>
</div>
</div>
</div>
@* ===================== MODAL DIALOGS ===================== *@
@* 1. Policy Library Reference Modal *@
@if (ShowPolicyLibraryModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-2xl w-full p-6 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div class="text-left">
<h3 class="text-base font-bold text-[#003b71]">SAQA Travel Policy Library</h3>
<p class="text-xs text-[#8a96a8]">Authorized compliance guidelines and cap matrices</p>
</div>
<button @onclick="ClosePolicyLibraryModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close X SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto space-y-4 pr-1 text-sm text-[#1f2a3a]">
<div class="p-3 bg-[#e2eefb] text-[#0066b3] text-xs font-medium rounded-lg flex gap-2">
<!-- Info SVG -->
<svg class="w-4 h-4 shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span>All bookings generated must comply with SAQA corporate governance policy and National Treasury frameworks. Exceptions require written CFO sign-off prior to ticketing.</span>
</div>
<div class="space-y-3">
<div class="p-3.5 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#003b71] text-xs uppercase mb-1">1. Airline Cabin Restrictions</div>
<p class="text-xs text-[#5a6577]">All local, regional, and international travel bookings are restricted to Economy Cabin flights. Premium Economy or Business classes require verified medical justifications from Human Resources, approved by the Chief Financial Officer.</p>
</div>
<div class="p-3.5 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#00a3a1] text-xs uppercase mb-1">2. Accommodation Rate Matrices</div>
<p class="text-xs text-[#5a6577]">Maximum nightly accommodation rates are aligned strictly with National Treasury Instruction No. 03 of 2016/17 (Cape Town maximum cap: R 1,900/night; Johannesburg: R 1,650/night). Minimum of 3 competitive quotes must be processed by the corporate booking tool prior to checkout.</p>
</div>
<div class="p-3.5 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#22a06b] text-xs uppercase mb-1">3. Ground Transportation Caps</div>
<p class="text-xs text-[#5a6577]">Car rental class is capped at Group B vehicles (e.g. automatic compact). Shuttles should be booked through verified pre-authorized local transfer vendors. Personal extension days differential billing is strictly monitored.</p>
</div>
<div class="p-3.5 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#f4a300] text-xs uppercase mb-1">4. Foreign Exchange &amp; Per Diems</div>
<p class="text-xs text-[#5a6577]">Daily travel allowances (per diems) are aligned with SARS guidelines for overseas destinations. Expenses must be OCR-submitted and synced within 7 working days post-trip completion.</p>
</div>
</div>
</div>
<div class="flex justify-end pt-4 border-t border-[#e4eaf2] mt-4">
<button @onclick="ClosePolicyLibraryModal"
class="px-4 py-2 rounded-lg text-sm font-semibold text-white bg-[#003b71] hover:bg-[#00264d] transition shadow-sm cursor-pointer">
Close Database
</button>
</div>
</div>
</div>
}
@code { @code {
// Exception class model representation
public class TravelException
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Date { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Justification { get; set; } = string.Empty;
public string Traveller { get; set; } = string.Empty;
public string Status { get; set; } = "Pending";
public bool IsCritical { get; set; }
}
// Core States
private int OverallCompliance { get; set; } = 94;
private int OutsidePolicyCount { get; set; } = 7;
private int AnomaliesCount { get; set; } = 2;
private string AlertFeedback { get; set; } = string.Empty;
// Modal control
private bool ShowPolicyLibraryModal { get; set; } = false;
// Database arrays
private List<TravelException> ExceptionsList { get; set; } = new();
protected override void OnInitialized()
{
// Populate pre-loaded Exceptions & Policy alerts matching GDS parameters
ExceptionsList = new List<TravelException>
{
new TravelException
{
Id = 1,
Title = "Cabin Class Upgrade (BA JNB to LHR)",
Date = "14 May 2026",
Description = "T. Maluleke booked Premium Economy cabin on British Airways JNB-LHR route.",
Justification = "Medical reasons verified by SAQA HR and confirmed by executive CFO. Approved cabin class exception.",
Traveller = "T. Maluleke",
Status = "Approved",
IsCritical = false
},
new TravelException
{
Id = 2,
Title = "Hotel Rate Bypassed Limits (Cape Town Engagement)",
Date = "22 May 2026",
Description = "J. Mokoena nightly rate processed at R 2,400 per night vs maximum NT Cap matrix of R 1,900.",
Justification = "Peak season business engagement at Waterfront. No alternative hotels available within standard NT limits on short notice.",
Traveller = "J. Mokoena",
Status = "Pending",
IsCritical = true
},
new TravelException
{
Id = 3,
Title = "Leisure Extension differential check",
Date = "20 May 2026",
Description = "S. Pillay added 2 non-working personal days to domestic Durban travel itinerary.",
Justification = "Weekend stay extension. Airfare price differential & weekend hotel lodging billed personally to coordinator account.",
Traveller = "S. Pillay",
Status = "Approved",
IsCritical = false
}
};
}
// Interactive Exception Approvals
private void AuthorizeException(int id)
{
var exc = ExceptionsList.FirstOrDefault(e => e.Id == id);
if (exc != null)
{
exc.Status = "Approved";
if (exc.IsCritical)
{
AnomaliesCount = System.Math.Max(0, AnomaliesCount - 1);
OverallCompliance = System.Math.Min(100, OverallCompliance + 2);
}
TriggerFeedback($"Exception justification authorized successfully for {exc.Traveller}. Compliance indices recalculated.");
}
}
private void RejectException(int id)
{
var exc = ExceptionsList.FirstOrDefault(e => e.Id == id);
if (exc != null)
{
exc.Status = "Rejected";
if (exc.IsCritical)
{
AnomaliesCount = System.Math.Max(0, AnomaliesCount - 1);
}
TriggerFeedback($"Exception justification rejected for {exc.Traveller}. Compliance violation flagged for PO settlement collection.");
}
}
// Compliance Audit trigger
private async Task TriggerFullComplianceAudit()
{
TriggerFeedback("Analyzing Sync Logs... Querying 142 Active GDS PNR entries for policy compliance.");
await Task.Delay(1800);
OverallCompliance = 96;
AnomaliesCount = ExceptionsList.Count(e => e.Status == "Pending");
OutsidePolicyCount = ExceptionsList.Count;
TriggerFeedback("Compliance Telemetry Scan Finished! 94% overall compliance rate recalculated across all May 2026 travel bookings. All flags archived.");
}
// Report Downloads simulation
private void TriggerReportDownload(string reportName)
{
TriggerFeedback($"Compiling dataset... {reportName} download queued. System auto-generating document structure.");
}
// Modal Helpers
private void OpenPolicyLibraryModal()
{
ShowPolicyLibraryModal = true;
}
private void ClosePolicyLibraryModal()
{
ShowPolicyLibraryModal = false;
}
// Feedback alert triggered toaster
private void TriggerFeedback(string message)
{
AlertFeedback = message;
_ = DismissAlertAfterDelay();
}
private async Task DismissAlertAfterDelay()
{
await Task.Delay(5000);
AlertFeedback = string.Empty;
StateHasChanged();
}
} }
@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1 class="text-3xl font-bold mb-4">Counter</h1>
<p role="status" class="mb-4">Current count: @currentCount</p>
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
+380 -1
View File
@@ -1,7 +1,386 @@
@page "/financial" @page "/financial"
<h3>Financial</h3> <div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
<!-- Page Header Area -->
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">Financial Management</h2>
<p class="text-xs text-[#5a6577] mt-1">Bill-back consolidation, lodge card reconciliation, virtual credit cards</p>
</div>
<div class="flex gap-2">
<button class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer">
Export to Excel
</button>
<button class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer">
Generate invoice
</button>
</div>
</div>
<!-- KPI Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Outstanding Invoices -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#0066b3]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-wider font-semibold mb-2">Outstanding Invoices</div>
<div class="text-2xl font-bold tracking-tight">R @OutstandingInvoices.ToString("N0")</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>14 invoices · ageing 0-30 days</span>
</div>
</div>
<!-- Lodge Card Balance -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#00a3a1]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-wider font-semibold mb-2">Lodge Card Balance</div>
<div class="text-2xl font-bold tracking-tight">R @LodgeCardBalance.ToString("N0")</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Reconciled to 24 May</span>
</div>
</div>
<!-- YTD Savings -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-wider font-semibold mb-2">YTD Savings</div>
<div class="text-2xl font-bold tracking-tight">@YtdSavingsText</div>
<div class="flex items-center gap-1.5 mt-2 text-xs">
<span class="text-[#22a06b] font-semibold">▲ 18.2%</span>
<span class="text-[#8a96a8]">vs. baseline</span>
</div>
</div>
<!-- No-Show Fees -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#f4a300]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-wider font-semibold mb-2">No-Show Fees</div>
<div class="text-2xl font-bold tracking-tight">R @NoShowFees.ToString("N0")</div>
<div class="flex items-center gap-1.5 mt-2 text-xs">
<span class="text-[#dc2626] font-semibold">▼ R 2,300</span>
<span class="text-[#8a96a8]">vs. last month</span>
</div>
</div>
</div>
<!-- First Data Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Spend vs Savings Responsive Bar Chart Card -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm flex flex-col justify-between">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold">Spend vs. Savings</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Cost containment performance</p>
</div>
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">On track</span>
</div>
<!-- Responsive Rendered SVG Bar Chart Layout -->
<div class="relative w-full h-[220px] mt-2">
<svg viewBox="0 0 600 220" class="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<!-- Y-Axis Gridlines & Ticks -->
<g stroke="#e4eaf2" stroke-width="1" stroke-dasharray="2,2">
<line x1="60" y1="20" x2="580" y2="20" />
<line x1="60" y1="60" x2="580" y2="60" />
<line x1="60" y1="100" x2="580" y2="100" />
<line x1="60" y1="140" x2="580" y2="140" />
<line x1="60" y1="180" x2="580" y2="180" />
</g>
<g font-family="Inter, sans-serif" font-size="11" fill="#8a96a8" text-anchor="end">
<text x="50" y="24">R2.5M</text>
<text x="50" y="64">R2.0M</text>
<text x="50" y="104">R1.5M</text>
<text x="50" y="144">R1.0M</text>
<text x="50" y="184">R500k</text>
</g>
<!-- Chart Bars: [Spend (Blue), Savings (Teal)] -->
<!-- Dec -->
<rect x="110" y="59" width="16" height="121" fill="#0066b3" rx="3" />
<rect x="130" y="162" width="16" height="18" fill="#00a3a1" rx="3" />
<!-- Jan -->
<rect x="190" y="47" width="16" height="133" fill="#0066b3" rx="3" />
<rect x="210" y="159" width="16" height="21" fill="#00a3a1" rx="3" />
<!-- Feb -->
<rect x="270" y="46" width="16" height="134" fill="#0066b3" rx="3" />
<rect x="290" y="157" width="16" height="23" fill="#00a3a1" rx="3" />
<!-- Mar -->
<rect x="350" y="28" width="16" height="152" fill="#0066b3" rx="3" />
<rect x="370" y="153" width="16" height="27" fill="#00a3a1" rx="3" />
<!-- Apr -->
<rect x="430" y="13" width="16" height="167" fill="#0066b3" rx="3" />
<rect x="450" y="149" width="16" height="31" fill="#00a3a1" rx="3" />
<!-- May -->
<rect x="510" y="4" width="16" height="176" fill="#0066b3" rx="3" />
<rect x="530" y="147" width="16" height="33" fill="#00a3a1" rx="3" />
<!-- X-Axis Labels -->
<g font-family="Inter, sans-serif" font-size="11" fill="#8a96a8" text-anchor="middle">
<text x="128" y="198">Dec</text>
<text x="208" y="198">Jan</text>
<text x="288" y="198">Feb</text>
<text x="368" y="198">Mar</text>
<text x="448" y="198">Apr</text>
<text x="528" y="198">May</text>
</g>
</svg>
</div>
<div class="flex justify-center gap-6 mt-4 text-xs font-semibold text-[#5a6577]">
<div class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-sm bg-[#0066b3]"></span> Total Spend
</div>
<div class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-sm bg-[#00a3a1]"></span> Savings Realised
</div>
</div>
</div>
<!-- Savings Breakdown Table Card -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm flex flex-col justify-between">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold">Savings Breakdown</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">How May savings of R 412,580 were realised</p>
</div>
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">+R 412k</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Category</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Open Rate</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Negotiated</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Saved</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">%</th>
</tr>
</thead>
<tbody class="text-sm">
@foreach (var item in SavingsData)
{
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
<td class="py-2.5 px-3">
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold @item.BadgeClass">
@item.Category
</span>
</td>
<td class="py-2.5 px-3">R @item.OpenRate.ToString("N0")</td>
<td class="py-2.5 px-3">R @item.NegotiatedRate.ToString("N0")</td>
<td class="py-2.5 px-3 text-[#22a06b] font-semibold">R @item.Saved.ToString("N0")</td>
<td class="py-2.5 px-3 text-[#5a6577]">@item.Percentage.ToString("F1")%</td>
</tr>
}
<tr class="bg-[#effaf3] font-bold">
<td colspan="3" class="py-3 px-3">Total saved this month</td>
<td class="py-3 px-3 text-[#22a06b]">R @TotalSavedThisMonth.ToString("N0")</td>
<td class="py-3 px-3">16.8%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Second Data Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Virtual Credit Card Card -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm flex flex-col justify-between">
<div class="mb-4">
<h3 class="text-[15px] font-bold">Virtual Credit Card</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Single-use, per-transaction</p>
</div>
<!-- Authentic Virtual Card Mock -->
<div class="bg-gradient-to-br from-[#003b71] via-[#0066b3] to-[#00a3a1] text-white rounded-2xl p-6 shadow-md relative overflow-hidden before:content-[''] before:absolute before:-right-10 before:-top-10 before:w-40 before:h-40 before:bg-gradient-to-br before:from-white/10 before:to-transparent before:rounded-full">
<div class="text-[11px] opacity-85 uppercase tracking-widest">SAQA · Single-use VCC</div>
<div class="font-mono text-lg tracking-[4px] my-5 font-semibold">4716 •••• •••• 8842</div>
<div class="flex justify-between items-end mt-3">
<div>
<span class="text-[10px] opacity-85 block uppercase tracking-wider">Cardholder</span>
<strong class="text-sm font-semibold">SAQA Travel Desk</strong>
</div>
<div>
<span class="text-[10px] opacity-85 block uppercase tracking-wider">Limit</span>
<strong class="text-sm font-semibold">R 3,500.00</strong>
</div>
<div>
<span class="text-[10px] opacity-85 block uppercase tracking-wider">Expires</span>
<strong class="text-sm font-semibold">02/06/26</strong>
</div>
</div>
</div>
<div class="flex gap-2 mt-5">
<button class="flex-1 py-2.5 px-4 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer text-center">
Issue new card
</button>
<button class="py-2.5 px-4 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer text-center">
View all (12)
</button>
</div>
</div>
<!-- Spend by Cost Centre Card -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm">
<div class="mb-4">
<h3 class="text-[15px] font-bold">Spend by Cost Centre</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Where the May spend went</p>
</div>
<div class="flex flex-col">
@foreach (var cc in CostCentreData)
{
<div class="flex items-center justify-between py-3 border-b border-[#e4eaf2] last:border-0 hover:bg-[#fafcfe] px-1 transition-colors rounded-lg">
<div>
<div class="text-[13px] font-medium">@cc.Name</div>
<div class="text-[11px] text-[#8a96a8]">@cc.Travellers travellers</div>
</div>
<strong class="text-sm font-bold">R @cc.Spend.ToString("N0")</strong>
</div>
}
</div>
</div>
</div>
<!-- Consolidated Invoices Card -->
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold">Consolidated Invoices</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Bill-back from suppliers · linked to PO &amp; Travel Authorisation</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">May 2026</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Invoice #</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Supplier</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Linked PO</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Items</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Total</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Status</th>
</tr>
</thead>
<tbody class="text-sm">
@foreach (var invoice in InvoicesData)
{
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
<td class="py-3 px-3 font-semibold">@invoice.InvoiceNum</td>
<td class="py-3 px-3">@invoice.Supplier</td>
<td class="py-3 px-3 text-[#5a6577]">@invoice.LinkedPo</td>
<td class="py-3 px-3 text-[#5a6577]">@invoice.Items</td>
<td class="py-3 px-3 font-medium">R @invoice.Total.ToString("N0")</td>
<td class="py-3 px-3">
<span class="inline-block px-2.5 py-0.5 rounded-full text-[11px] font-semibold @invoice.StatusBadgeClass">
@invoice.Status
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@code { @code {
// KPI Data Properties
private double OutstandingInvoices { get; set; } = 184920;
private double LodgeCardBalance { get; set; } = 312440;
private string YtdSavingsText { get; set; } = "R 1.94M";
private double NoShowFees { get; set; } = 8140;
private double TotalSavedThisMonth { get; set; } = 412580;
// Savings Breakdown Data Structure
private List<SavingsItem> SavingsData { get; set; } = new()
{
new SavingsItem("Air, domestic", 980400, 832340, 148060, 15.1, "bg-[#e2eefb] text-[#0066b3]"),
new SavingsItem("Air, international", 320200, 260920, 59280, 18.5, "bg-[#e2eefb] text-[#0066b3]"),
new SavingsItem("Accommodation", 845600, 722420, 123180, 14.6, "bg-[#f4f7fb] text-[#1f2a3a] border border-[#e4eaf2]"),
new SavingsItem("Car rental", 215800, 168180, 47620, 22.1, "bg-[#fff4dd] text-[#b06b00]"),
new SavingsItem("Shuttle and transfers", 92800, 75400, 17400, 18.8, "bg-[#f4f7fb] text-[#1f2a3a] border border-[#e4eaf2]"),
new SavingsItem("Service fees waived", 17040, 0, 17040, 100.0, "bg-[#e3f6ed] text-[#22a06b]")
};
// Cost Centre Data Structure
private List<CostCentreItem> CostCentreData { get; set; } = new()
{
new CostCentreItem("Office of the CEO", 12, 684200),
new CostCentreItem("Qualifications and Standards", 38, 542400),
new CostCentreItem("Verifications", 26, 418950),
new CostCentreItem("Research and Stats", 18, 312800),
new CostCentreItem("Corporate Services", 14, 248400),
new CostCentreItem("International Liaison", 6, 640560)
};
// Consolidated Invoices Data Structure
private List<InvoiceItem> InvoicesData { get; set; } = new()
{
new InvoiceItem("INV-2026-0541", "SAA", "PO-44219", "3 tickets", 12420, "Paid", "bg-[#e3f6ed] text-[#22a06b]"),
new InvoiceItem("INV-2026-0542", "Premier Hotels", "PO-44230", "5 nights", 9475, "Pending", "bg-[#fff4dd] text-[#b06b00]"),
new InvoiceItem("INV-2026-0543", "Avis", "PO-44245", "1 rental · 3 days", 1420, "Reconciled", "bg-[#e2eefb] text-[#0066b3]"),
new InvoiceItem("INV-2026-0544", "FlySafair", "PO-44250", "2 tickets", 3970, "Paid", "bg-[#e3f6ed] text-[#22a06b]"),
new InvoiceItem("INV-2026-0545", "British Airways", "PO-44261", "1 ticket (LHR)", 18940, "Disputed", "bg-[#fde5e8] text-[#dc2626]")
};
// Sub-classes for models
public class SavingsItem
{
public string Category { get; set; }
public double OpenRate { get; set; }
public double NegotiatedRate { get; set; }
public double Saved { get; set; }
public double Percentage { get; set; }
public string BadgeClass { get; set; }
public SavingsItem(string category, double openRate, double negotiatedRate, double saved, double percentage, string badgeClass)
{
Category = category;
OpenRate = openRate;
NegotiatedRate = negotiatedRate;
Saved = saved;
Percentage = percentage;
BadgeClass = badgeClass;
}
}
public class CostCentreItem
{
public string Name { get; set; }
public int Travellers { get; set; }
public double Spend { get; set; }
public CostCentreItem(string name, int travellers, double spend)
{
Name = name;
Travellers = travellers;
Spend = spend;
}
}
public class InvoiceItem
{
public string InvoiceNum { get; set; }
public string Supplier { get; set; }
public string LinkedPo { get; set; }
public string Items { get; set; }
public double Total { get; set; }
public string Status { get; set; }
public string StatusBadgeClass { get; set; }
public InvoiceItem(string invoiceNum, string supplier, string linkedPo, string items, double total, string status, string statusBadgeClass)
{
InvoiceNum = invoiceNum;
Supplier = supplier;
LinkedPo = linkedPo;
Items = items;
Total = total;
Status = status;
StatusBadgeClass = statusBadgeClass;
}
}
} }
+553 -1
View File
@@ -1,7 +1,559 @@
@page "/forex" @page "/forex"
<h3>Forex</h3> <div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
<!-- Page Header Area -->
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">Foreign Exchange and Expense Management</h2>
<p class="text-xs text-[#5a6577] mt-1">Order forex, load travel money cards, capture and reconcile foreign expenses</p>
</div>
<div class="flex gap-2">
<button class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer" @onclick="OpenForexOrderDialog">
Order forex
</button>
<button class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer" @onclick="OpenLoadCardDialog">
Load travel card
</button>
</div>
</div>
<!-- Alert / Feedback Notification Banner -->
@if (ShowAlertFeedback)
{
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-xs text-[#1b8054] flex items-center gap-2">
<svg class="w-4 h-4 text-[#22a06b] shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<polyline points="20 6 9 17 4 12" />
</svg>
<span>@AlertFeedbackText</span>
</div>
}
<!-- KPI Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Open Forex Orders -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#0066b3]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Open Forex Orders</div>
<div class="text-2xl font-bold tracking-tight">@OpenForexOrdersCount</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Awaiting collection or load</span>
</div>
</div>
<!-- Loaded on Travel Cards -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#00a3a1]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Loaded on Travel Cards</div>
<div class="text-2xl font-bold tracking-tight">USD @LoadedCardBalance.ToString("N0")</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>@ActiveCardsCount active travel cards</span>
</div>
</div>
<!-- Settled This Month -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Settled This Month</div>
<div class="text-2xl font-bold tracking-tight">R @SettledAmountZar.ToString("N0")</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Reconciled to lodge card</span>
</div>
</div>
<!-- Unreconciled Receipts -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#f4a300]">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Unreconciled Receipts</div>
<div class="text-2xl font-bold tracking-tight">@UnreconciledReceiptsCount</div>
<div class="flex items-center gap-1.5 mt-2 text-xs text-[#8a96a8]">
<span class="text-[#dc2626] font-medium">Awaiting traveller action</span>
</div>
</div>
</div>
<!-- First Data Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Live Forex Rates Card -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm flex flex-col justify-between">
<div>
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold">Live Forex Rates</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Updated @LastUpdatedTime.ToString("dd MMM yyyy, HH:mm") SAST</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">Bank rate</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Currency</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Buy</th>
<th className="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Sell</th>
<th className="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">1 Day</th>
<th className="text-right text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Action</th>
</tr>
</thead>
<tbody class="text-sm divide-y divide-[#e4eaf2]">
@foreach (var rate in ForexRates)
{
<tr class="hover:bg-[#fafcfe] transition-colors">
<td class="py-2.5 px-1 font-medium">
<span class="font-bold text-[#101827]">@rate.Code</span>
<span class="text-xs text-[#8a96a8] ml-1.5">@rate.Name</span>
</td>
<td class="py-2.5 px-1">R @rate.BuyRate.ToString("N2")</td>
<td class="py-2.5 px-1">R @rate.SellRate.ToString("N2")</td>
<td class="py-2.5 px-1 font-semibold">
@if (rate.DayChange >= 0)
{
<span class="text-[#22a06b]">▲ @rate.DayChange.ToString("N2")</span>
}
else
{
<span class="text-[#dc2626]">▼ @Math.Abs(rate.DayChange).ToString("N2")</span>
}
</td>
<td class="py-2.5 px-1 text-right">
<button @onclick="() => InitiateOrder(rate)" class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold text-[#1f2a3a] hover:bg-[#f4f7fb] border border-[#e4eaf2] transition cursor-pointer">
Order
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Currency Converter Tool Section -->
<div class="mt-5 pt-4 border-t border-[#e4eaf2]">
<h4 class="text-[11px] font-bold text-[#5a6577] uppercase tracking-wider mb-2.5">Quick ZAR Estimate Calculator</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
<div>
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Foreign Amount</label>
<input type="number" @bind="ConvertAmount" @bind:event="oninput" class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-white focus:outline-none focus:border-[#0066b3]" />
</div>
<div>
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Currency</label>
<select @bind="SelectedConvertCurrency" class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-white focus:outline-none focus:border-[#0066b3]">
@foreach (var rate in ForexRates)
{
<option value="@rate.Code">@rate.Code (@rate.Name)</option>
}
</select>
</div>
<div class="bg-[#f4f7fb] p-2 rounded-lg border border-[#e4eaf2] flex flex-col justify-center">
<span class="text-[9px] text-[#8a96a8] font-semibold uppercase">Estimated ZAR</span>
<span class="text-sm font-bold text-[#003b71]">R @CalculatedZarAmount.ToString("N2")</span>
</div>
</div>
</div>
</div>
<!-- Travel Money Card Card -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm flex flex-col justify-between">
<div>
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold">Travel Money Card</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Multi-currency, reloadable, traveller assigned</p>
</div>
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">Active</span>
</div>
<!-- Authentic Travel Card Container -->
<div class="relative overflow-hidden bg-gradient-to-br from-[#003b71] via-[#0066b3] to-[#00a3a1] text-white rounded-[14px] p-6 shadow-md @(IsCardFrozen ? "grayscale contrast-75 brightness-75 opacity-80 transition-all" : "transition-all")">
@if (IsCardFrozen)
{
<div class="absolute inset-0 bg-black/30 flex flex-col items-center justify-center z-10 backdrop-blur-[1px]">
<svg class="w-8 h-8 text-white mb-1.5 animate-bounce" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
<span class="text-xs font-bold tracking-wider uppercase">Frozen</span>
</div>
}
<div class="text-[11px] opacity-85 uppercase tracking-widest font-medium">SAQA Travel Money Card</div>
<div class="font-mono text-lg tracking-[3px] my-5 font-semibold">@MaskedCardNumber</div>
<div class="flex justify-between items-end mt-3">
<div>
<span class="text-[9px] opacity-75 uppercase block">Holder</span>
<strong class="text-sm font-semibold">@CardHolderName</strong>
</div>
<div class="flex gap-4">
<div>
<span class="text-[9px] opacity-75 uppercase block">USD Balance</span>
<strong class="text-sm font-semibold">$@CardUsdBalance.ToString("N0")</strong>
</div>
<div>
<span class="text-[9px] opacity-75 uppercase block">GBP Balance</span>
<strong class="text-sm font-semibold">£@CardGbpBalance.ToString("N0")</strong>
</div>
</div>
</div>
</div>
<!-- Status Action Rows -->
<div class="mt-5 space-y-1">
<div class="flex items-center justify-between py-3 border-b border-dashed border-[#e4eaf2]">
<div>
<div class="text-[13px] font-semibold">Top up USD</div>
<div class="text-[11px] text-[#8a96a8]">Auto-reload at $200</div>
</div>
<button @onclick="() => QuickReloadCard(200)" disabled="@IsCardFrozen" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] disabled:opacity-50 disabled:pointer-events-none transition cursor-pointer">
Add funds
</button>
</div>
<div class="flex items-center justify-between py-3 border-b border-dashed border-[#e4eaf2] last:border-0">
<div>
<div class="text-[13px] font-semibold">Card Freeze Control</div>
<div class="text-[11px] text-[#8a96a8]">instantly freeze or unfreeze this card 24/7</div>
</div>
<button @onclick="ToggleCardFreeze" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold border transition cursor-pointer @(IsCardFrozen ? "bg-[#22a06b] hover:bg-[#1b8054] text-white border-transparent" : "bg-white border-[#fde5e8] text-[#dc2626] hover:bg-red-50")">
@(IsCardFrozen ? "Unfreeze" : "Block")
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Foreign Expense Capture Table Card -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<div>
<h3 class="text-[15px] font-bold">Foreign Expense Capture and Reconciliation</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Receipts auto converted at transaction rate</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<input type="text" placeholder="Search traveler, merchant..." @bind="SearchQuery" @bind:event="oninput" class="px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs w-48 focus:outline-none focus:border-[#0066b3]" />
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">OCR enabled</span>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Date</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Traveller</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Merchant</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Foreign</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">ZAR</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Category</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Status</th>
<th class="text-right text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-3">Action</th>
</tr>
</thead>
<tbody class="text-sm">
@foreach (var expense in FilteredExpenses)
{
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
<td class="py-3 px-3">@expense.Date</td>
<td class="py-3 px-3 font-semibold text-[#101827]">@expense.Traveller</td>
<td class="py-3 px-3 text-[#5a6577]">@expense.Merchant</td>
<td class="py-3 px-3 font-semibold">@expense.ForeignCurrency @expense.ForeignAmount.ToString("N2")</td>
<td class="py-3 px-3 font-bold text-[#003b71]">R @expense.ZarAmount.ToString("N2")</td>
<td class="py-3 px-3 text-[#5a6577]">@expense.Category</td>
<td class="py-3 px-3">
<span class="inline-block px-2.5 py-0.5 rounded-full text-[11px] font-semibold @(expense.Status == "Reconciled" ? "bg-[#e3f6ed] text-[#22a06b]" : expense.Status == "Pending Receipt" ? "bg-[#fff4dd] text-[#b06b00]" : "bg-[#e2eefb] text-[#0066b3]")">
@expense.Status
</span>
</td>
<td class="py-3 px-3 text-right">
@if (expense.Status != "Reconciled")
{
<button @onclick="() => ReconcileExpense(expense)" class="px-2.5 py-1 bg-[#0066b3] hover:bg-[#003b71] text-white text-xs font-semibold rounded-lg shadow-sm transition cursor-pointer">
Reconcile
</button>
}
else
{
<span class="text-xs text-[#8a96a8] font-semibold py-1 px-2">Complete</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- ===================== INTERACTIVE MODAL DIALOGS ===================== -->
@if (ShowOrderDialog)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-bold text-[#003b71]">Order Foreign Currency</h3>
<button @onclick="CloseDialogs" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">✕</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Target Currency</label>
<select @bind="SelectedOrderCurrency" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white">
@foreach (var rate in ForexRates)
{
<option value="@rate.Code">@rate.Code (@rate.Name)</option>
}
</select>
</div>
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Order Amount</label>
<input type="number" @bind="OrderAmount" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white" />
</div>
<div class="p-3 bg-[#f4f7fb] rounded-lg border border-[#e4eaf2] text-xs text-[#5a6577] space-y-1">
<div class="flex justify-between">
<span>Exchange Rate:</span>
<span class="font-bold text-[#1f2a3a]">1 @SelectedOrderCurrency = @GetSellRate(SelectedOrderCurrency).ToString("N2") ZAR</span>
</div>
<div class="flex justify-between text-sm font-bold text-[#003b71] pt-1.5 border-t border-[#e4eaf2]">
<span>Estimated Cost:</span>
<span>R @((OrderAmount * GetSellRate(SelectedOrderCurrency)).ToString("N2"))</span>
</div>
</div>
<div class="flex gap-2 justify-end pt-2">
<button @onclick="CloseDialogs" class="px-4 py-2 rounded-lg text-sm font-semibold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Cancel</button>
<button @onclick="SubmitForexOrder" class="px-4 py-2 rounded-lg text-sm font-semibold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer">Confirm Order</button>
</div>
</div>
</div>
</div>
}
@if (ShowLoadDialog)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-bold text-[#003b71]">Load Travel Money Card</h3>
<button @onclick="CloseDialogs" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">✕</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Assign Traveller</label>
<input type="text" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-[#f4f7fb]" value="@CardHolderName" disabled />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Load USD ($)</label>
<input type="number" @bind="LoadUsdAmount" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white" />
</div>
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Load GBP (£)</label>
<input type="number" @bind="LoadGbpAmount" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white" />
</div>
</div>
<div class="flex gap-2 justify-end pt-2">
<button @onclick="CloseDialogs" class="px-4 py-2 rounded-lg text-sm font-semibold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Cancel</button>
<button @onclick="SubmitLoadCard" class="px-4 py-2 rounded-lg text-sm font-semibold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer">Load Card</button>
</div>
</div>
</div>
</div>
}
@code { @code {
// ===================== STATE & DATA PROPERTIES =====================
private int OpenForexOrdersCount { get; set; } = 4;
private int LoadedCardBalance { get; set; } = 3420;
private int ActiveCardsCount { get; set; } = 3;
private int SettledAmountZar { get; set; } = 184200;
private int UnreconciledReceiptsCount { get; set; } = 7;
private DateTime LastUpdatedTime { get; set; } = DateTime.Now;
private List<ForexRateModel> ForexRates { get; set; } = new()
{
new ForexRateModel("USD", "US Dollar", 18.42, 19.05, 0.12),
new ForexRateModel("EUR", "Euro", 19.98, 20.62, -0.04),
new ForexRateModel("GBP", "Pound", 23.34, 24.10, 0.18),
new ForexRateModel("AUD", "Aus Dollar", 12.05, 12.48, 0.03),
new ForexRateModel("BWP", "Pula", 1.31, 1.38, -0.01)
};
private List<ExpenseModel> TravellerExpenses { get; set; } = new()
{
new ExpenseModel("20 May", "T. Maluleke", "Heathrow Express", "£", 25.00, 602.50, "Ground transport", "Reconciled"),
new ExpenseModel("20 May", "T. Maluleke", "Pret a Manger", "£", 12.40, 298.84, "Meals", "Reconciled"),
new ExpenseModel("21 May", "A. Dlamini", "Hilton Nairobi", "$", 210.00, 3886.20, "Accommodation", "Pending Receipt"),
new ExpenseModel("22 May", "A. Dlamini", "Java House", "$", 14.50, 268.39, "Meals", "In review")
};
// Travel money card states
private string MaskedCardNumber { get; set; } = "5318 ●●●● ●●●● 2207";
private string CardHolderName { get; set; } = "T. Maluleke";
private int CardUsdBalance { get; set; } = 1200;
private int CardGbpBalance { get; set; } = 820;
private bool IsCardFrozen { get; set; } = false;
// Search query property
private string SearchQuery { get; set; } = string.Empty;
// Estimator property bindings
private double ConvertAmount { get; set; } = 100;
private string SelectedConvertCurrency { get; set; } = "USD";
// Dialog trigger states
private bool ShowOrderDialog { get; set; } = false;
private bool ShowLoadDialog { get; set; } = false;
private string SelectedOrderCurrency { get; set; } = "USD";
private double OrderAmount { get; set; } = 500;
private double LoadUsdAmount { get; set; } = 250;
private double LoadGbpAmount { get; set; } = 150;
// Interactive banner states
private bool ShowAlertFeedback { get; set; } = false;
private string AlertFeedbackText { get; set; } = string.Empty;
// Estimated calculator property
private double CalculatedZarAmount
{
get
{
var rate = ForexRates.FirstOrDefault(r => r.Code == SelectedConvertCurrency);
return rate != null ? ConvertAmount * rate.BuyRate : 0;
}
}
// Live search filtered results
private IEnumerable<ExpenseModel> FilteredExpenses
{
get
{
if (string.IsNullOrWhiteSpace(SearchQuery))
{
return TravellerExpenses;
}
return TravellerExpenses.Where(e =>
e.Merchant.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) ||
e.Traveller.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) ||
e.Category.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
);
}
}
// ===================== C# LOGIC ACTIONS =====================
private double GetSellRate(string currencyCode)
{
var rate = ForexRates.FirstOrDefault(r => r.Code == currencyCode);
return rate != null ? rate.SellRate : 0;
}
private void OpenForexOrderDialog()
{
SelectedOrderCurrency = "USD";
OrderAmount = 500;
ShowOrderDialog = true;
}
private void OpenLoadCardDialog()
{
LoadUsdAmount = 250;
LoadGbpAmount = 150;
ShowLoadDialog = true;
}
private void CloseDialogs()
{
ShowOrderDialog = false;
ShowLoadDialog = false;
}
private void InitiateOrder(ForexRateModel rate)
{
SelectedOrderCurrency = rate.Code;
OrderAmount = 200;
ShowOrderDialog = true;
}
private void SubmitForexOrder()
{
OpenForexOrdersCount++;
TriggerFeedback($"Order of {OrderAmount:N2} {SelectedOrderCurrency} confirmed for collection.");
CloseDialogs();
}
private void SubmitLoadCard()
{
CardUsdBalance += (int)LoadUsdAmount;
CardGbpBalance += (int)LoadGbpAmount;
LoadedCardBalance += (int)LoadUsdAmount;
TriggerFeedback($"Loaded ${LoadUsdAmount:N0} and £{LoadGbpAmount:N0} successfully onto Travel Card.");
CloseDialogs();
}
private void QuickReloadCard(int amount)
{
CardUsdBalance += amount;
LoadedCardBalance += amount;
TriggerFeedback($"Auto-reload triggered: successfully added ${amount} to USD wallet.");
}
private void ToggleCardFreeze()
{
IsCardFrozen = !IsCardFrozen;
var state = IsCardFrozen ? "FROZEN" : "UNFROZEN";
TriggerFeedback($"Travel money card has been successfully {state}.");
}
private void ReconcileExpense(ExpenseModel expense)
{
expense.Status = "Reconciled";
UnreconciledReceiptsCount = Math.Max(0, UnreconciledReceiptsCount - 1);
TriggerFeedback($"Expense at {expense.Merchant} successfully reconciled.");
}
private void TriggerFeedback(string message)
{
AlertFeedbackText = message;
ShowAlertFeedback = true;
StateHasChanged();
}
// ===================== DATA MODEL CLASSES =====================
public class ForexRateModel
{
public string Code { get; set; }
public string Name { get; set; }
public double BuyRate { get; set; }
public double SellRate { get; set; }
public double DayChange { get; set; }
public ForexRateModel(string code, string name, double buyRate, double sellRate, double dayChange)
{
Code = code;
Name = name;
BuyRate = buyRate;
SellRate = sellRate;
DayChange = dayChange;
}
}
public class ExpenseModel
{
public string Date { get; set; }
public string Traveller { get; set; }
public string Merchant { get; set; }
public string ForeignCurrency { get; set; }
public double ForeignAmount { get; set; }
public double ZarAmount { get; set; }
public string Category { get; set; }
public string Status { get; set; }
public ExpenseModel(string date, string traveller, string merchant, string foreignCurrency, double foreignAmount, double zarAmount, string category, string status)
{
Date = date;
Traveller = traveller;
Merchant = merchant;
ForeignCurrency = foreignCurrency;
ForeignAmount = foreignAmount;
ZarAmount = zarAmount;
Category = category;
Status = status;
}
}
} }
+597 -1
View File
@@ -1,7 +1,603 @@
@page "/gds-sync" @page "/gds-sync"
<h3>GdsSync</h3> @using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
@rendermode InteractiveServer
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
@* Page Header Area *@
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
<div class="text-left">
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">GDS Sync Console</h2>
<p class="text-xs text-[#5a6577] mt-1">Real-time Amadeus, Travelport, and Sabre telemetry synchronization gateway and GDS negotiated rates validator</p>
</div>
<div class="flex gap-2">
<button @onclick="OpenSyncLogsModal"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer">
<!-- Terminal SVG -->
<svg class="w-4 h-4 text-[#003b71]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
View Sync Logs
</button>
<button @onclick="TriggerForceResync"
disabled="@IsSyncing"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer disabled:bg-gray-400 disabled:cursor-not-allowed">
<!-- Sync / Arrow Path SVG -->
<svg class="w-4 h-4 @(IsSyncing ? "animate-spin" : "")" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
@(IsSyncing ? "Syncing GDS Nodes..." : "Force Resync Now")
</button>
</div>
</div>
@* Alert / Feedback Notification Banner *@
@if (!string.IsNullOrEmpty(AlertFeedback))
{
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-sm text-[#1b8054] flex items-center gap-2.5 shadow-sm transition-all text-left">
<!-- Check Circle SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
<span class="font-medium">@AlertFeedback</span>
</div>
}
@* GDS Status Core KPI Cards *@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Amadeus Node</div>
<div class="flex items-baseline gap-2">
<div class="text-2xl font-bold tracking-tight">Active / Live</div>
<span class="text-xs text-[#22a06b] font-semibold">100% Up</span>
</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Last sync @AmadeusLastSyncs ago</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Travelport Node</div>
<div class="flex items-baseline gap-2">
<div class="text-2xl font-bold tracking-tight">Active / Live</div>
<span class="text-xs text-[#22a06b] font-semibold">100% Up</span>
</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Last sync @TravelportLastSyncs ago</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Sabre Node</div>
<div class="flex items-baseline gap-2">
<div class="text-2xl font-bold tracking-tight">Active / Live</div>
<span class="text-xs text-[#22a06b] font-semibold">100% Up</span>
</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Last sync @SabreLastSyncs ago</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#f4a300] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">In-Flight PNR Queue</div>
<div class="text-2xl font-bold tracking-tight text-[#f4a300]">@PnrQueueCount</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Pending ticket printing checks</span>
</div>
</div>
</div>
@* Spend vs Savings Visual Indicators & GDS Providers Metrics split *@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 mb-6">
@* Spend vs Savings Metrics Panel - Left Column (lg:col-span-7) *@
<div class="lg:col-span-7 bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="flex items-center justify-between mb-4 border-b border-[#e4eaf2] pb-3">
<div>
<h3 class="text-[15px] font-bold">Spend vs. Savings Telemetry</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Negotiated corporate GDS fares against open published consumer tickets for May 2026</p>
</div>
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">+18.2% YTD Savings</span>
</div>
<div class="space-y-4">
<p class="text-xs text-[#5a6577]">Below metrics reflect the total savings realized directly from GDS nodes through TMT projects pre-negotiated corporate rates.</p>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-3.5 bg-[#f4f7fb] rounded-lg border border-[#e4eaf2]">
<span class="block text-[10px] text-[#8a96a8] uppercase font-bold mb-1">Open Published Cost</span>
<span class="text-lg font-bold text-[#1f2a3a]">R 2,662,890</span>
<span class="block text-[9px] text-gray-500 mt-1">Consumer benchmark rates</span>
</div>
<div class="p-3.5 bg-[#e2eefb] rounded-lg border border-[#b4d3f5]">
<span class="block text-[10px] text-[#0066b3] uppercase font-bold mb-1">Negotiated Corporate Cost</span>
<span class="text-lg font-bold text-[#0066b3]">R 2,250,310</span>
<span class="block text-[9px] text-[#0066b3] mt-1">SAQA contracted limits</span>
</div>
<div class="p-3.5 bg-[#effaf3] rounded-lg border border-[#a1dfc2]">
<span class="block text-[10px] text-[#22a06b] uppercase font-bold mb-1">Direct Savings Realized</span>
<span class="text-lg font-bold text-[#22a06b]">R @TotalSavingsThisMonth.ToString("N0")</span>
<span class="block text-[9px] text-[#22a06b] mt-1">May 2026 reporting index</span>
</div>
</div>
@* Visual Simulated Bar stack *@
<div class="space-y-1.5 pt-3">
<div class="flex justify-between items-center text-xs font-semibold">
<span>May Spend Budget Saved Rate</span>
<span class="text-[#22a06b]">15.5% Saved</span>
</div>
<div class="w-full bg-[#fde5e8] h-3.5 rounded-lg flex overflow-hidden">
<div class="bg-[#0066b3] h-full" style="width: 84.5%;" title="Negotiated Spend (84.5%)"></div>
<div class="bg-[#22a06b] h-full" style="width: 15.5%;" title="Savings (15.5%)"></div>
</div>
<div class="flex gap-4 text-[10px] text-gray-500">
<div class="flex items-center gap-1">
<span class="w-2.5 h-2.5 rounded-full bg-[#0066b3]"></span>
<span>Negotiated (R 2,250,310)</span>
</div>
<div class="flex items-center gap-1">
<span class="w-2.5 h-2.5 rounded-full bg-[#22a06b]"></span>
<span>Savings Realized (R @TotalSavingsThisMonth.ToString("N0"))</span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 pt-3.5 border-t border-[#e4eaf2] text-xs text-[#8a96a8] flex justify-between items-center">
<span>Database verified by ASATA / IATA standards</span>
<span class="font-semibold text-gray-700">Sync is operational</span>
</div>
</div>
@* Savings by GDS Provider progress list - Right Column (lg:col-span-5) *@
<div class="lg:col-span-5 bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="mb-4">
<h3 class="text-base font-bold text-[#003b71]">Savings by GDS Provider Node</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Contribution percentages realized across Amadeus, Travelport, and Sabre connection pools</p>
</div>
<div class="space-y-4">
@* Provider 1 *@
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<div>
<span class="font-bold text-[#1f2a3a]">Amadeus Node</span>
<span class="text-[10px] text-[#8a96a8] block">Domestic &amp; regional flights / local lodging</span>
</div>
<div class="text-right">
<span class="font-bold text-[#22a06b]">R @AmadeusSavings.ToString("N0")</span>
<span class="text-[10px] text-[#8a96a8] block">52.9% of total</span>
</div>
</div>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#00a3a1] h-1.5 rounded-full" style="width: 53%;"></div>
</div>
</div>
@* Provider 2 *@
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<div>
<span class="font-bold text-[#1f2a3a]">Travelport Node</span>
<span class="text-[10px] text-[#8a96a8] block">International airlines / premium class cabins</span>
</div>
<div class="text-right">
<span class="font-bold text-[#22a06b]">R @TravelportSavings.ToString("N0")</span>
<span class="text-[10px] text-[#8a96a8] block">30.1% of total</span>
</div>
</div>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#0066b3] h-1.5 rounded-full" style="width: 30%;"></div>
</div>
</div>
@* Provider 3 *@
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<div>
<span class="font-bold text-[#1f2a3a]">Sabre Node</span>
<span class="text-[10px] text-[#8a96a8] block">Hotel aggregates &amp; car rental structures</span>
</div>
<div class="text-right">
<span class="font-bold text-[#22a06b]">R @SabreSavings.ToString("N0")</span>
<span class="text-[10px] text-[#8a96a8] block">17.0% of total</span>
</div>
</div>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#f4a300] h-1.5 rounded-full" style="width: 17%;"></div>
</div>
</div>
@* Highlighted Box *@
<div class="p-3 bg-[#effaf3] rounded-lg border border-[#d2f1df] text-center">
<span class="block text-[10px] text-[#5a6577] uppercase font-bold">Total GDS Savings Verified</span>
<span class="text-xl font-black text-[#22a06b] mt-0.5 block">R @TotalSavingsThisMonth.ToString("N0")</span>
</div>
</div>
</div>
<div class="mt-4 text-[10px] text-[#8a96a8] leading-normal">
<span>Savings are calculated on GDS booking confirmation vs standard unrestricted booking baseline. Verified by continuous live audit checks.</span>
</div>
</div>
</div>
@* Live Supplier Feed Table & Sync Activity grid *@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 mb-6">
@* GDS Supplier Fares Feed - Left Column (lg:col-span-8) *@
<div class="lg:col-span-8 bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between mb-4 gap-2 border-b border-[#e4eaf2] pb-3">
<div>
<h3 class="text-base font-bold text-[#003b71]">Live Supplier Feed via GDS</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Contracted tariffs fetched directly from active connection handshakes</p>
</div>
@* Search Filter *@
<div class="flex items-center gap-2">
<span class="text-xs text-[#8a96a8] shrink-0 font-medium">GDS Provider:</span>
<select @bind="SelectedGdsFilter"
class="px-2 py-1 bg-[#f4f7fb] text-[#1f2a3a] border border-[#c8d3e1] rounded-lg text-xs font-semibold focus:outline-none focus:border-[#0066b3]">
<option value="All">All Providers</option>
<option value="Amadeus">Amadeus</option>
<option value="Travelport">Travelport</option>
<option value="Sabre">Sabre</option>
</select>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-left">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Supplier</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">GDS Node</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Type</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Open Rate</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Negotiated</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">ZAR Saved</th>
<th class="text-right text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Status</th>
</tr>
</thead>
<tbody class="text-sm">
@foreach (var feed in FilteredSupplierFeed)
{
<tr class="border-b border-[#e4eaf2] last:border-0 hover:bg-[#fafcfe] transition-colors">
<td class="py-2.5 px-1 font-bold text-[#101827]">@feed.SupplierName</td>
<td class="py-2.5 px-1">
<span class="inline-block px-2 py-0.5 rounded text-[10px] font-bold @(feed.GdsNode == "Amadeus" ? "bg-[#e0f7f6] text-[#00a3a1]" : feed.GdsNode == "Travelport" ? "bg-[#e2eefb] text-[#0066b3]" : "bg-[#fff4dd] text-[#b06b00]")">
@feed.GdsNode
</span>
</td>
<td class="py-2.5 px-1 text-gray-500 text-xs">@feed.CategoryType</td>
<td class="py-2.5 px-1 text-gray-600 font-mono text-xs">R @feed.OpenPrice.ToString("N0")</td>
<td class="py-2.5 px-1 font-bold text-[#101827] font-mono text-xs">R @feed.NegotiatedPrice.ToString("N0")</td>
<td class="py-2.5 px-1 text-[#22a06b] font-semibold text-xs font-mono">
R @feed.SavedValue.ToString("N0") (@feed.SavedPercentage%)
</td>
<td class="py-2.5 px-1 text-right">
<span class="inline-block px-2 py-0.5 rounded text-[9px] font-bold @(feed.Status == "Live" ? "bg-[#e3f6ed] text-[#22a06b]" : "bg-[#e2eefb] text-[#0066b3]")">
@feed.Status
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="mt-4 pt-3.5 border-t border-[#e4eaf2] text-xs text-[#8a96a8]">
<span>* Rates are updated dynamically every 30 seconds. Supplier cache lifetimes are managed according to commercial service agreements.</span>
</div>
</div>
@* Sync Activity & Health status indicators - Right Column (lg:col-span-4) *@
<div class="lg:col-span-4 space-y-6 text-left">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="mb-4">
<h3 class="text-base font-bold text-[#003b71]">Node Traffic Analysis</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Aggregated telemetry actions logged over last 24 hours</p>
</div>
<div class="space-y-3.5">
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2] text-xs">
<div>
<span class="font-bold text-[#1f2a3a] block">PNRs Generated</span>
<span class="text-[10px] text-[#8a96a8]">Across all connection tunnels</span>
</div>
<strong class="font-mono text-sm text-[#101827]">142</strong>
</div>
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2] text-xs">
<div>
<span class="font-bold text-[#1f2a3a] block">Fares &amp; Tariff Queries</span>
<span class="text-[10px] text-[#8a96a8]">Read operations processed</span>
</div>
<strong class="font-mono text-sm text-[#101827]">8,420</strong>
</div>
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2] text-xs">
<div>
<span class="font-bold text-[#1f2a3a] block">Confirmed Air &amp; Hotel Bookings</span>
<span class="text-[10px] text-[#8a96a8]">Ticketed in line with SAQA policies</span>
</div>
<strong class="font-mono text-sm text-[#22a06b]">118</strong>
</div>
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2] text-xs">
<div>
<span class="font-bold text-[#1f2a3a] block">Auto Refund / Voucher Queues</span>
<span class="text-[10px] text-[#8a96a8]">Cancellations processed smoothly</span>
</div>
<strong class="font-mono text-sm text-[#0066b3]">9</strong>
</div>
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2] text-xs">
<div>
<span class="font-bold text-[#1f2a3a] block">GDS Connection Failure Rate</span>
<span class="text-[10px] text-[#8a96a8]">Auto-retry failover threshold</span>
</div>
<strong class="font-mono text-sm text-[#22a06b]">0.04% (Exceptional)</strong>
</div>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="mb-4">
<h3 class="text-base font-bold text-[#003b71]">Telemetry Node Health</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Direct response times &amp; availability metrics</p>
</div>
<div class="space-y-4">
@* Bar 1 *@
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-gray-700">Amadeus API latency</span>
<span class="font-mono text-[#22a06b]">88ms (Avg)</span>
</div>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#22a06b] h-1.5 rounded-full" style="width: 88%;"></div>
</div>
</div>
@* Bar 2 *@
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-gray-700">Travelport API latency</span>
<span class="font-mono text-[#22a06b]">82ms (Avg)</span>
</div>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#22a06b] h-1.5 rounded-full" style="width: 82%;"></div>
</div>
</div>
@* Bar 3 *@
<div class="space-y-1">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-gray-700">Sabre API latency</span>
<span class="font-mono text-[#f4a300]">124ms (Avg)</span>
</div>
<div class="w-full bg-[#f4f7fb] h-1.5 rounded-full overflow-hidden">
<div class="bg-[#f4a300] h-1.5 rounded-full" style="width: 74%;"></div>
</div>
</div>
<div class="p-3 bg-[#effaf3] border border-[#d2f1df] rounded-lg text-xs text-[#1b8054] flex gap-2">
<!-- Shield SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
<div>
<span class="font-bold block">GDS Node Uptime: 99.98%</span>
<p class="text-[10px] mt-0.5 text-[#2b7250]">Automatic node failover setup ensures queries bypass any downtime immediately by fetching rates from auxiliary tunnels.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@* ===================== MODAL DIALOGS ===================== *@
@* 1. GDS Sync Logs Modal *@
@if (ShowSyncLogsModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-[#101827] rounded-xl border border-gray-800 shadow-2xl max-w-2xl w-full p-6 flex flex-col max-h-[90vh] text-left">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-gray-800">
<div>
<h3 class="text-base font-bold text-white">Live GDS Telemetry Sync Logs</h3>
<p class="text-xs text-gray-400 mt-0.5">Handshake queries and active transaction parameters</p>
</div>
<button @onclick="CloseSyncLogsModal" class="text-gray-400 hover:text-white cursor-pointer">
<!-- Close SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
@* Console style panel *@
<div class="flex-1 overflow-y-auto bg-black/50 border border-gray-900 rounded-lg p-4 font-mono text-xs text-green-400 space-y-2 max-h-[400px]">
@foreach (var log in SyncLogs)
{
<div class="flex gap-2.5 items-start">
<span class="text-gray-600 shrink-0">@log.Timestamp</span>
<span class="font-semibold @(log.Type == "Error" ? "text-red-400" : log.Type == "Success" ? "text-green-400" : "text-blue-300")">
[@log.Type]
</span>
<span class="text-gray-300">@log.Message</span>
</div>
}
</div>
<div class="flex justify-between items-center pt-4 border-t border-gray-800 mt-4">
<button @onclick="ClearSyncLogs" class="px-3.5 py-1.5 rounded-lg border border-gray-800 hover:border-gray-700 font-semibold text-xs text-gray-400 hover:text-white transition">
Clear Terminal Logs
</button>
<button @onclick="CloseSyncLogsModal" class="px-4 py-2 rounded-lg text-xs font-semibold text-[#1f2a3a] bg-white hover:bg-gray-100 transition shadow-sm cursor-pointer">
Close Terminal
</button>
</div>
</div>
</div>
}
@code { @code {
// Model Definitions
public class GdsSupplierFeed
{
public int Id { get; set; }
public string SupplierName { get; set; } = string.Empty;
public string GdsNode { get; set; } = string.Empty; // Amadeus, Travelport, Sabre
public string CategoryType { get; set; } = string.Empty;
public decimal OpenPrice { get; set; }
public decimal NegotiatedPrice { get; set; }
public decimal SavedValue => OpenPrice - NegotiatedPrice;
public int SavedPercentage => (int)System.Math.Round((SavedValue / OpenPrice) * 100);
public string Status { get; set; } = "Live";
}
public class GdsConsoleLog
{
public string Timestamp { get; set; } = string.Empty;
public string Type { get; set; } = "Info"; // Success, Warning, Error, Info
public string Message { get; set; } = string.Empty;
}
// Core States
private bool IsSyncing { get; set; } = false;
private string AlertFeedback { get; set; } = string.Empty;
private string SelectedGdsFilter { get; set; } = "All";
private int PnrQueueCount { get; set; } = 14;
// Last sync counters
private int AmadeusLastSyncs { get; set; } = 14;
private int TravelportLastSyncs { get; set; } = 41;
private int SabreLastSyncs { get; set; } = 22;
// Total aggregate calculations
private decimal TotalSavingsThisMonth => AmadeusSavings + TravelportSavings + SabreSavings;
private decimal AmadeusSavings { get; set; } = 218400M;
private decimal TravelportSavings { get; set; } = 124180M;
private decimal SabreSavings { get; set; } = 70000M;
// Modal Control
private bool ShowSyncLogsModal { get; set; } = false;
// Databases
private List<GdsSupplierFeed> SupplierFeeds { get; set; } = new();
private List<GdsConsoleLog> SyncLogs { get; set; } = new();
// Filtered Feed list based on selection
private IEnumerable<GdsSupplierFeed> FilteredSupplierFeed =>
SelectedGdsFilter == "All"
? SupplierFeeds
: SupplierFeeds.Where(f => f.GdsNode.Equals(SelectedGdsFilter, System.StringComparison.OrdinalIgnoreCase));
protected override void OnInitialized()
{
// Populate supplier contracted feeds matching dashboard data
SupplierFeeds = new List<GdsSupplierFeed>
{
new GdsSupplierFeed { Id = 1, SupplierName = "SAA (South African Airways)", GdsNode = "Amadeus", CategoryType = "Air, Domestic", OpenPrice = 3290M, NegotiatedPrice = 2890M, Status = "Live" },
new GdsSupplierFeed { Id = 2, SupplierName = "FlySafair Flights", GdsNode = "Amadeus", CategoryType = "Air, Low Cost", OpenPrice = 2180M, NegotiatedPrice = 1985M, Status = "Live" },
new GdsSupplierFeed { Id = 3, SupplierName = "British Airways Standard", GdsNode = "Travelport", CategoryType = "Air, International", OpenPrice = 21440M, NegotiatedPrice = 18940M, Status = "Live" },
new GdsSupplierFeed { Id = 4, SupplierName = "Emirates Corporate", GdsNode = "Travelport", CategoryType = "Air, International", OpenPrice = 28800M, NegotiatedPrice = 23100M, Status = "Live" },
new GdsSupplierFeed { Id = 5, SupplierName = "Premier Hotels Collection", GdsNode = "Sabre", CategoryType = "Accommodation", OpenPrice = 2200M, NegotiatedPrice = 1895M, Status = "Live" },
new GdsSupplierFeed { Id = 6, SupplierName = "City Lodge Hotels Corp", GdsNode = "Sabre", CategoryType = "Accommodation", OpenPrice = 1850M, NegotiatedPrice = 1520M, Status = "Live" },
new GdsSupplierFeed { Id = 7, SupplierName = "Avis Car Rental", GdsNode = "Sabre", CategoryType = "Car Rental", OpenPrice = 520M, NegotiatedPrice = 420M, Status = "Live" },
new GdsSupplierFeed { Id = 8, SupplierName = "Europcar SA Group", GdsNode = "Sabre", CategoryType = "Car Rental", OpenPrice = 580M, NegotiatedPrice = 460M, Status = "Cached, 2 min" }
};
// Populate initial sync terminal logger
SyncLogs = new List<GdsConsoleLog>
{
new GdsConsoleLog { Timestamp = "11:15:02 AM", Type = "Info", Message = "Opening communication socket with Amadeus JNB Node (196.34.12.8)..." },
new GdsConsoleLog { Timestamp = "11:15:03 AM", Type = "Success", Message = "Handshake authenticated successfully. Security key established." },
new GdsConsoleLog { Timestamp = "11:15:04 AM", Type = "Info", Message = "Fetching May 2026 PNR ticket confirmation logs. Parsing 142 records..." },
new GdsConsoleLog { Timestamp = "11:15:08 AM", Type = "Success", Message = "Amadeus sync process successfully closed. 14 open e-Tickets synced to active dashboard." },
new GdsConsoleLog { Timestamp = "11:15:10 AM", Type = "Info", Message = "Querying Sabre GDS hotel & transfer rates. Auto-refresh threshold enabled." },
new GdsConsoleLog { Timestamp = "11:15:12 AM", Type = "Warning", Message = "Europcar API socket returned lag index of 180ms. Auto-refresh fallback to cache enabled." }
};
}
// Force Resync Simulation Trigger
private async Task TriggerForceResync()
{
IsSyncing = true;
TriggerFeedback("Executing Force GDS Node Resync. Parsing active socket connections...");
// Simulating sync steps and logging parameters
SyncLogs.Add(new GdsConsoleLog { Timestamp = "Just Now", Type = "Info", Message = "USER SIGNAL: Initiating full cluster resynchronization..." });
StateHasChanged();
await Task.Delay(1200);
SyncLogs.Add(new GdsConsoleLog { Timestamp = "Just Now", Type = "Success", Message = "Amadeus connection flushed and pulled fresh corporate PNR sets." });
AmadeusLastSyncs = 1;
StateHasChanged();
await Task.Delay(800);
SyncLogs.Add(new GdsConsoleLog { Timestamp = "Just Now", Type = "Success", Message = "Travelport & Sabre nodes auto-recalibrated rates cache parameters." });
TravelportLastSyncs = 1;
SabreLastSyncs = 1;
PnrQueueCount = System.Math.Max(0, PnrQueueCount - 2);
IsSyncing = false;
TriggerFeedback("GDS Cluster Sync Complete! Telemetry matrices successfully mapped and negotiated rates parsed smoothly.");
StateHasChanged();
}
// Modal view triggers
private void OpenSyncLogsModal()
{
ShowSyncLogsModal = true;
}
private void CloseSyncLogsModal()
{
ShowSyncLogsModal = false;
}
private void ClearSyncLogs()
{
SyncLogs.Clear();
SyncLogs.Add(new GdsConsoleLog { Timestamp = "Terminal Purged", Type = "Info", Message = "Sync log interface successfully cleared by admin command." });
}
// Toast alarm toaster
private void TriggerFeedback(string message)
{
AlertFeedback = message;
_ = DismissAlertAfterDelay();
}
private async Task DismissAlertAfterDelay()
{
await Task.Delay(5000);
AlertFeedback = string.Empty;
StateHasChanged();
}
} }
+200 -62
View File
@@ -1,5 +1,9 @@
@page "/" @page "/"
@rendermode InteractiveServer @rendermode InteractiveServer
@using System
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
<PageTitle>Overview Dashboard</PageTitle> <PageTitle>Overview Dashboard</PageTitle>
@@ -19,7 +23,7 @@
</svg> </svg>
Export Export
</button> </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"> <button @onclick="@(() => OpenBookingModal())" 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 cursor-pointer">
<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"> <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="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" /> <line x1="5" y1="12" x2="19" y2="12" />
@@ -271,7 +275,7 @@
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<div> <div>
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Recent Bookings</h3> <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> <p class="text-[12px] text-[#8a96a8] mt-0.5">Last confirmed trips</p>
</div> </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> <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>
@@ -288,66 +292,59 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr class="hover:bg-[#fafcfe] transition-colors"> @foreach (var item in _recentBookings)
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]"> {
<div class="flex items-center gap-2.5"> <tr class="hover:bg-[#fafcfe] transition-colors">
<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> <td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
<div><strong class="font-bold">J. Mokoena</strong><span class="block text-[#8a96a8] text-[11px]">Executive</span></div> <div class="flex items-center gap-2.5">
</div> <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">@item.Initials</div>
</td> <div><strong class="font-bold">@item.Traveller</strong><span class="block text-[#8a96a8] text-[11px]">@item.Role</span></div>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">JNB → CPT, 02 Jun</td> </div>
<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>
<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]">@item.Details</td>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 3,240</td> <td class="p-3 border-b border-[#e4eaf2] text-[13px]">
</tr> @if (item.Type == "Flight")
<tr class="hover:bg-[#fafcfe] transition-colors"> {
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]"> <span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Flight</span>
<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> else if (item.Type == "Hotel")
<div><strong class="font-bold">N. Ndlovu</strong><span class="block text-[#8a96a8] text-[11px]">Manager</span></div> {
</div> <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] text-[#1f2a3a]">Premier Hotel OR Tambo</td> else if (item.Type == "Car")
<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> <span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Car</span>
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 1,895</td> }
</tr> else if (item.Type == "Shuttle")
<tr class="hover:bg-[#fafcfe] transition-colors"> {
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]"> <span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Shuttle</span>
<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> else
<div><strong class="font-bold">S. Pillay</strong><span class="block text-[#8a96a8] text-[11px]">Coordinator</span></div> {
</div> <span class="bg-[#f4f7fb] text-[#5a6577] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">@item.Type</span>
</td> }
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">Avis Group B, 3 days</td> </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]">
<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> @if (item.Status == "Confirmed")
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 1,420</td> {
</tr> <span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span>
<tr class="hover:bg-[#fafcfe] transition-colors"> }
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]"> else if (item.Status == "Pending")
<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> <span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Pending</span>
<div><strong class="font-bold">T. Maluleke</strong><span class="block text-[#8a96a8] text-[11px]">Director</span></div> }
</div> else if (item.Status == "Cancelled")
</td> {
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">JNB → LHR, 14 Jun</td> <span class="bg-[#fde5e8] text-[#c8102e] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Cancelled</span>
<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> else
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 18,940</td> {
</tr> <span class="bg-gray-100 text-gray-800 px-2.5 py-[3px] rounded-full text-[11px] font-semibold">@item.Status</span>
<tr class="hover:bg-[#fafcfe] transition-colors"> }
<td class="p-3 text-[13px] text-[#1f2a3a]"> </td>
<div class="flex items-center gap-2.5"> <td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R @item.Cost.ToString("N0")</td>
<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> </tr>
<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> </tbody>
</table> </table>
</div> </div>
@@ -402,6 +399,74 @@
</div> </div>
</div> </div>
@* Create Booking Modal *@
@if (_showNewBookingModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-[#003b71] flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-[#0066b3]">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
New Travel Booking
</h3>
<button @onclick="@(() => _showNewBookingModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Traveller Name</label>
<input type="text" @bind="_newTraveller" placeholder="e.g. J. Mokoena" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Role / Grade</label>
<select @bind="_newRole" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]">
<option value="Executive">Executive</option>
<option value="Director">Director</option>
<option value="Manager">Manager</option>
<option value="Officer">Officer</option>
<option value="Coordinator">Coordinator</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Booking Type</label>
<select @bind="_newType" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]">
<option value="Flight">Flight</option>
<option value="Hotel">Hotel</option>
<option value="Car">Car</option>
<option value="Shuttle">Shuttle</option>
</select>
</div>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Route / Stay Details</label>
<input type="text" @bind="_newDetails" placeholder="e.g. JNB → CPT, 18 Jun or Premier Hotel, 3 nights" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Booking Cost (ZAR)</label>
<input type="number" @bind="_newCost" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div class="p-3 bg-[#e2eefb] text-[#0066b3] border border-blue-100 rounded-lg text-[11px] leading-relaxed">
This booking will be automatically synced with the global GDS network and reconciled against the corporate Lodge Card parameters.
</div>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => _showNewBookingModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Discard</button>
<button @onclick="@(() => CreateBooking())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer shadow-xs">Generate Booking</button>
</div>
</div>
</div>
}
@code { @code {
private bool _isLoaded = false; private bool _isLoaded = false;
@@ -426,6 +491,79 @@
private double ChartWidth = 800; private double ChartWidth = 800;
private double ChartHeight = 220; 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<HomeBookingItem> _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) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
+685 -1
View File
@@ -1,7 +1,691 @@
@page "/mobile" @page "/mobile"
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
@* Page Header Area *@
<div class="flex flex-col md:flex-row md:items-end justify-between mb-6 gap-4">
<div class="text-left">
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">SAQA Mobile Companion</h2>
<p class="text-xs text-[#5a6577] mt-1">Empowering travellers with real-time itinerary updates, offline OCR expense capturing, and instant consultant chat</p>
</div>
<div class="flex gap-2">
<button @onclick='() => TriggerFeedback("Initiating SAQA Mobile App download for Apple iOS...")'
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer">
<!-- Apple SVG -->
<svg class="w-4 h-4 text-[#003b71]" fill="currentColor" viewBox="0 0 24 24">
<path d="M12.2 12.55c.6 0 1.22-.35 1.56-.95.34-.6.3-1.3-.1-1.74-.4-.44-1.07-.56-1.6-.26-.53.3-.84.93-.78 1.6.06.67.43 1.2.92 1.35z" />
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.49 1.29-.02 2.5.87 3.29.87.79 0 2.26-1.07 3.81-.91.65.07 2.47.3 3.64 2.01-.09.06-2.18 1.27-2.16 3.79.03 2.1 1.72 2.87 1.74 2.88-.02.05-.27.91-.93 1.86l-.01.11zM15.97 4.17c.66-.81 1.11-1.93.99-3.06-.96.04-2.13.64-2.82 1.45-.6.69-1.2 1.83-1.06 2.95 1.07.08 2.18-.51 2.89-1.34z" />
</svg>
App Store (iOS)
</button>
<button @onclick='() => TriggerFeedback("Redirecting to Google Play Store download portal...")'
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer">
<!-- Android SVG -->
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.52 14.5c.34-.35.5-.94.3-1.4l1.35-1.36c.4-.41.35-1.08-.1-1.44l-2.46-1.92-1.25 1.26c-.34-.35-.85-.45-1.3-.26L11.53 7.5c.44-.36.5-.98.15-1.4l-1.35-1.36c-.4-.41-1.07-.35-1.43.11L6.44 6.77l1.25 1.26c-.34.35-.49.94-.29 1.4L6.05 10.8c-.4.41-.35 1.08.1 1.44l2.46 1.92 1.25-1.26c.34.35.85.45 1.3.26l2.53 1.88c-.44.36-.5.98-.15 1.4l1.35 1.36c.4.41 1.07.35 1.43-.11l2.46-1.92-1.25-1.26z" />
</svg>
Google Play (Android)
</button>
</div>
</div>
@* Alert / Feedback Notification Banner *@
@if (!string.IsNullOrEmpty(AlertFeedback))
{
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-sm text-[#1b8054] flex items-center gap-2.5 shadow-sm transition-all text-left">
<!-- Sparkles SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b] animate-bounce" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 21L8.188 15.904L3 15L8.188 14.096L9 9L9.813 14.096L15 15L9.813 15.904Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.071 4.929a10 10 0 00-14.142 0M19.071 19.071a10 10 0 01-14.142 0" />
</svg>
<span class="font-medium">@AlertFeedback</span>
</div>
}
@* Adoption & Capture Progress Cards *@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#00a3a1] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Active Mobile Users</div>
<div class="text-2xl font-bold tracking-tight text-[#00a3a1]">@ActiveInstalls / 238</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>78% adoption across SAQA</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Receipts Scanned (OCR)</div>
<div class="text-2xl font-bold tracking-tight text-[#22a06b]">@ReceiptsScannedCount</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#22a06b] font-semibold">
<span>R @TotalValueScanned.ToString("N0") auto-reconciled</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#0066b3] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Mobile Bookings Share</div>
<div class="text-2xl font-bold tracking-tight">34%</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>May 2026 reporting period</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#dc2626] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Avg Approval Turnaround</div>
<div class="text-2xl font-bold tracking-tight text-[#dc2626]">1h 12m</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#dc2626] font-semibold">
<span>92% faster than email workflows</span>
</div>
</div>
</div>
@* Core interactive layout split *@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-6">
@* Interactive Phone Bezel Simulator - Left Column (lg:col-span-5) *@
<div class="lg:col-span-5 flex flex-col items-center">
<div class="w-full max-w-[340px] bg-[#1f2a3a] rounded-[42px] p-[12px] shadow-2xl border-4 border-[#c8d3e1] relative overflow-hidden">
@* Front Camera Notch *@
<div class="absolute top-4 left-1/2 -translate-x-1/2 w-28 h-5 bg-[#1f2a3a] rounded-full z-30 flex items-center justify-center gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-[#111] border border-[#222]"></div>
<div class="w-10 h-1 bg-[#222] rounded-full"></div>
</div>
@* Phone Inner Screen Canvas *@
<div class="bg-[#101827] rounded-[32px] w-full min-h-[580px] text-white flex flex-col justify-between overflow-hidden relative pt-7 select-none">
@* Phone Status Bar *@
<div class="absolute top-1 w-full px-6 flex justify-between items-center text-[10px] text-gray-400 font-medium z-20">
<span>11:15 AM</span>
<div class="flex items-center gap-1.5">
<!-- Signal SVG -->
<svg class="w-3 h-3 fill-current" viewBox="0 0 24 24"><path d="M2 22h20V2z" /></svg>
<!-- Wi-Fi SVG -->
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.288 15.038a5.25 5.25 0 017.424 0M5.106 11.856a9.75 9.75 0 0113.788 0M1.924 8.674a14.25 14.5 0 0120.152 0M12 18.75h.007v.008H12v-.008z" />
</svg>
<!-- Battery SVG -->
<div class="w-5 h-2.5 border border-gray-400 rounded-xs p-0.5 flex items-center">
<div class="w-3/4 h-full bg-green-500 rounded-2xs"></div>
</div>
</div>
</div>
@* Simulated Active Mobile Screen View *@
<div class="flex-1 overflow-y-auto px-4 py-3 flex flex-col justify-between">
@* Screen Header *@
<div class="flex items-center justify-between border-b border-gray-800 pb-2.5 mb-3">
<div class="flex items-center gap-2">
<div class="h-6 w-6 rounded-full bg-[#00a3a1] text-white flex items-center justify-center font-bold text-[10px]">
@ActiveTravellerInitials
</div>
<div class="text-left">
<h4 class="text-xs font-bold leading-none text-white">@ActiveTraveller</h4>
<span class="text-[9px] text-[#00a3a1]">SAQA corporate account</span>
</div>
</div>
<span class="text-[9px] bg-gray-800 px-1.5 py-0.5 rounded text-gray-300 font-semibold border border-gray-700">Online</span>
</div>
@* Live View Contents based on Active Phone tab *@
@if (ActivePhoneTab == "profile")
{
<div class="space-y-2.5 text-left animate-in fade-in duration-200">
<div class="bg-gray-900/80 rounded-xl p-3 border border-gray-800">
<div class="text-[9px] text-[#00a3a1] font-bold uppercase tracking-wider mb-1">SAA Voyager Program</div>
<div class="flex justify-between items-baseline">
<div class="text-lg font-bold text-white">@LoyaltyMiles.ToString("N0") <span class="text-xs font-normal text-gray-400">Miles</span></div>
<span class="text-[9px] bg-[#f4a300] text-[#1f2a3a] px-1.5 py-0.5 rounded font-bold uppercase">Gold status</span>
</div>
<p class="text-[10px] text-gray-400 mt-1">Marriott Bonvoy Partner ID Linked</p>
</div>
<div class="bg-gray-900/80 rounded-xl p-3 border border-gray-800">
<div class="text-[9px] text-gray-400 font-bold uppercase tracking-wider mb-1.5">Verified Documents</div>
<div class="space-y-1.5 text-xs">
<div class="flex justify-between items-center bg-gray-850 p-1.5 rounded">
<span class="text-gray-300">ZA Passport</span>
<span class="text-[10px] text-green-400 font-medium">Valid Oct 2029</span>
</div>
<div class="flex justify-between items-center bg-gray-850 p-1.5 rounded">
<span class="text-gray-300">United Kingdom Visa</span>
<span class="text-[10px] text-green-400 font-medium">Valid Dec 2027</span>
</div>
<div class="flex justify-between items-center bg-gray-850 p-1.5 rounded">
<span class="text-gray-300">Schengen short-stay</span>
<span class="text-[10px] text-yellow-500 font-medium">Awaiting renewal</span>
</div>
</div>
</div>
<div class="bg-gray-900/80 rounded-xl p-3 border border-gray-800">
<div class="text-[9px] text-gray-400 font-bold uppercase tracking-wider mb-1.5">Travel Preferences</div>
<div class="flex flex-wrap gap-1">
<span class="text-[9px] bg-gray-800 text-gray-300 px-2 py-0.5 rounded">Aisle Seat preferred</span>
<span class="text-[9px] bg-gray-800 text-gray-300 px-2 py-0.5 rounded">Halal Meal</span>
<span class="text-[10px] bg-gray-800 text-gray-300 px-2 py-0.5 rounded">Non-Smoking Floor</span>
<span class="text-[10px] bg-gray-800 text-gray-300 px-2.5 py-0.5 rounded">Avis automatic group B</span>
</div>
</div>
</div>
}
else if (ActivePhoneTab == "expenses")
{
<div class="space-y-3 text-left animate-in fade-in duration-200">
<div class="flex justify-between items-center">
<h5 class="text-xs font-bold text-gray-200">Captured Overseas Receipts</h5>
<span class="text-[9px] bg-green-500/20 text-green-400 border border-green-500/30 px-2 py-0.5 rounded-full font-bold">OCR Sync active</span>
</div>
<div class="space-y-2 max-h-[220px] overflow-y-auto pr-0.5">
@foreach (var exp in MockExpenses)
{
<div class="bg-gray-900 p-2.5 rounded-xl border border-gray-800 flex justify-between items-center">
<div>
<div class="text-xs font-bold text-white">@exp.Merchant</div>
<div class="text-[9px] text-gray-400">@exp.Category · @exp.Date</div>
<div class="text-[9px] text-[#00a3a1] mt-0.5 font-semibold">R @exp.ZarAmount.ToString("N2")</div>
</div>
<div class="text-right">
<div class="text-xs font-bold text-white">@exp.ForeignAmount</div>
<span class="text-[8px] bg-green-600 text-white px-1 rounded">Reconciled</span>
</div>
</div>
}
</div>
@if (IsScanning)
{
<div class="p-3 bg-blue-950/60 border border-blue-800/40 rounded-xl space-y-2 text-center animate-pulse">
<div class="text-xs font-bold text-blue-400">Simulating Live Receipt Photo OCR Scan...</div>
<div class="w-full bg-gray-800 h-1.5 rounded-full overflow-hidden">
<div class="bg-blue-500 h-1.5 rounded-full" style="width: 75%"></div>
</div>
<p class="text-[9px] text-gray-400">Auto-converting exchange rates to ZAR settlement metrics</p>
</div>
}
<button @onclick="TriggerMockOcrCapture"
disabled="@IsScanning"
class="w-full py-2 bg-[#00a3a1] hover:bg-[#008280] text-white text-xs font-bold rounded-lg transition-all shadow-md cursor-pointer text-center">
+ Snap New Receipt Photo
</button>
</div>
}
else if (ActivePhoneTab == "itinerary")
{
<div class="space-y-2.5 text-left animate-in fade-in duration-200">
<div class="flex justify-between items-center">
<h5 class="text-xs font-bold text-gray-200">Active Business Itinerary</h5>
<span class="text-[9px] text-[#f4a300] font-bold uppercase">Cape Town Engagement</span>
</div>
<div class="bg-[#003b71]/30 border border-[#0066b3]/30 rounded-xl p-3 relative">
<div class="absolute top-2.5 right-2.5 bg-green-600 text-white font-bold text-[8px] px-1.5 rounded-full uppercase tracking-wider">Gate Open</div>
<div class="text-[9px] text-gray-400 uppercase font-semibold">Flight (Outbound)</div>
<div class="text-sm font-bold text-white mt-1">SAA flight SA 311</div>
<p class="text-[10px] text-gray-300">Johannesburg (JNB) to Cape Town (CPT)</p>
<div class="mt-2.5 flex gap-4 text-xs font-semibold text-gray-300 border-t border-gray-800 pt-2">
<div>
<span class="block text-[8px] text-gray-500 uppercase">Boarding</span>
<span>05:30 AM</span>
</div>
<div>
<span class="block text-[8px] text-gray-500 uppercase">Seat Assigned</span>
<span>14C (Aisle)</span>
</div>
<div>
<span class="block text-[8px] text-gray-500 uppercase">Gate</span>
<span>A12 (Terminal B)</span>
</div>
</div>
</div>
<div class="bg-gray-900/80 border border-gray-800 rounded-xl p-3">
<div class="text-[9px] text-gray-400 uppercase font-semibold">Accommodation Check-In</div>
<div class="text-sm font-bold text-white mt-1">Premier Hotel Waterfront</div>
<p class="text-[10px] text-gray-400">2 Nights confirmed lodging · PO linked</p>
<p class="text-[9px] text-[#00a3a1] mt-1 font-semibold">Voucher reference: HT7811 · Billed to SAQA Lodge Card</p>
</div>
<button @onclick='() => TriggerFeedback("Virtual QR Boarding Pass generated on companion wallet.")'
class="w-full py-2 border border-gray-700 hover:bg-gray-850 text-gray-300 text-xs font-semibold rounded-lg transition-all text-center">
View Digital QR Boarding Pass
</button>
</div>
}
else if (ActivePhoneTab == "chat")
{
<div class="flex flex-col h-[280px] justify-between text-left animate-in fade-in duration-200">
<div class="flex-1 overflow-y-auto space-y-2 pr-0.5" id="chat-messages-container">
@foreach (var msg in ChatMessages)
{
<div class="flex flex-col @(msg.IsTraveller ? "items-end" : "items-start")">
<span class="text-[8px] text-gray-500 mb-0.5">@msg.Sender · @msg.Timestamp</span>
<div class="p-2 rounded-lg text-xs max-w-[85%] @(msg.IsTraveller ? "bg-[#0066b3] text-white rounded-br-none" : "bg-gray-800 text-gray-200 rounded-bl-none")">
@msg.Text
</div>
</div>
}
@if (IsTyping)
{
<div class="flex items-center gap-1 bg-gray-900 p-2 rounded-lg max-w-[140px] text-[10px] text-gray-400">
<span class="animate-bounce">●</span>
<span class="animate-bounce" style="animation-delay: 0.2s">●</span>
<span class="animate-bounce" style="animation-delay: 0.4s">●</span>
<span>Consultant typing...</span>
</div>
}
</div>
<div class="flex gap-1.5 mt-2 border-t border-gray-800 pt-2">
<input type="text"
placeholder="Ask travel consultant..."
@bind="NewChatMessage"
@onkeyup="HandleChatKeyPress"
class="flex-1 px-2.5 py-1.5 bg-gray-900 border border-gray-800 text-white rounded-lg text-xs outline-none focus:border-[#0066b3]" />
<button @onclick="SendChatMessage"
class="px-3 py-1.5 bg-[#0066b3] hover:bg-[#003b71] text-white rounded-lg text-xs font-bold transition-all shrink-0">
Send
</button>
</div>
</div>
}
</div>
@* Phone Bottom Navigation Bar *@
<div class="bg-[#101827] border-t border-gray-900 px-3 py-2 flex justify-around items-center rounded-b-[32px] text-gray-400">
<button @onclick='() => SetPhoneTab("profile")'
class="flex flex-col items-center gap-0.5 transition-colors cursor-pointer @(ActivePhoneTab == "profile" ? "text-[#00a3a1]" : "hover:text-white")">
<!-- User SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
<span class="text-[8px] font-semibold">Profile</span>
</button>
<button @onclick='() => SetPhoneTab("expenses")'
class="flex flex-col items-center gap-0.5 transition-colors cursor-pointer @(ActivePhoneTab == "expenses" ? "text-[#00a3a1]" : "hover:text-white")">
<!-- Receipt SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.75 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
<span class="text-[8px]">Expenses</span>
</button>
<button @onclick='() => SetPhoneTab("itinerary")'
class="flex flex-col items-center gap-0.5 cursor-pointer @(ActivePhoneTab == "itinerary" ? "text-[#00a3a1]" : "text-gray-400 hover:text-white")">
<!-- Plane SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
<span class="text-[8px] font-semibold">Itinerary</span>
</button>
<button @onclick='() => SetPhoneTab("chat")'
class="flex flex-col items-center gap-0.5 transition-colors cursor-pointer relative @(ActivePhoneTab == "chat" ? "text-[#00a3a1]" : "hover:text-white")">
@if (UnreadChatCount > 0)
{
<span class="absolute -top-1 -right-1.5 bg-red-600 text-white font-extrabold text-[7px] w-3 h-3 rounded-full flex items-center justify-center animate-bounce">@UnreadChatCount</span>
}
<!-- Chat SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span class="text-[8px]">Support</span>
</button>
</div>
</div>
</div>
@* Traveller Profile Selector (Updates the phone UI dynamically) *@
<div class="mt-4 w-full max-w-[340px] p-4 bg-white rounded-xl border border-[#e4eaf2] text-left">
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase tracking-wider mb-1">Simulate Traveller Device</label>
<select @onchange="OnTravellerDeviceChanged"
class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-[#f4f7fb] text-[#1f2a3a] font-semibold focus:outline-none">
<option value="J. Mokoena">J. Mokoena (Executive)</option>
<option value="T. Maluleke">T. Maluleke (Director)</option>
<option value="A. Dlamini">A. Dlamini (Senior Manager)</option>
<option value="S. Pillay">S. Pillay (Coordinator)</option>
</select>
<p class="text-[10px] text-gray-500 mt-2">Swapping the active traveller updates the profile sync settings and loyalties on the phone bezel above.</p>
</div>
</div>
@* Mobile Capabilities Grid and Adopt Cards - Right Column (lg:col-span-7) *@
<div class="lg:col-span-7 space-y-6">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="flex items-center justify-between mb-4 border-b border-[#e4eaf2] pb-3">
<div>
<h3 class="text-base font-bold text-[#003b71]">Dynamic Mobile Capabilities</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Purpose-built workflows configured for SAQA corporate compliance</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">Always Syncing</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
@* Cap 1 *@
<div class="p-3 border border-[#e4eaf2] rounded-lg bg-[#fafcfe] hover:border-[#00a3a1] transition-all">
<div class="h-8 w-8 rounded-lg bg-[#e0f7f6] text-[#00a3a1] flex items-center justify-center mb-2.5">
<!-- Document SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.75 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a]">Profile & Doc Synchronization</h4>
<p class="text-[11px] text-[#5a6577] mt-1">Automatic sync of passport expiries, visas, and airline reward levels before tickets are processed.</p>
</div>
@* Cap 2 *@
<div class="p-3 border border-[#e4eaf2] rounded-lg bg-[#fafcfe] hover:border-[#00a3a1] transition-all">
<div class="h-8 w-8 rounded-lg bg-[#e3f6ed] text-[#22a06b] flex items-center justify-center mb-2.5">
<!-- Camera SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a]">Receipt OCR Engine (Offline Ready)</h4>
<p class="text-[11px] text-[#5a6577] mt-1">Translates international receipts into ZAR using historical exchange conversion tables. Works in airplanes without Wi-Fi.</p>
</div>
@* Cap 3 *@
<div class="p-3 border border-[#e4eaf2] rounded-lg bg-[#fafcfe] hover:border-[#00a3a1] transition-all">
<div class="h-8 w-8 rounded-lg bg-[#e2eefb] text-[#0066b3] flex items-center justify-center mb-2.5">
<!-- Bell SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
</div>
<h4 class="text-sm font-bold text-[#1f2a3a]">Push Notification Alerts</h4>
<p class="text-xs text-[#8a96a8] mt-1">Real-time alerts for gate changes, boarding statuses, flight delays, or global travel risk notifications.</p>
</div>
@* Tile 4 *@
<div class="p-4 border border-[#e4eaf2] rounded-xl hover:border-[#0066b3] hover:bg-[#f4f8fc]/40 transition-all cursor-pointer group text-left">
<div class="h-10 w-10 rounded-lg bg-[#fff4dd] text-[#f4a300] flex items-center justify-center mb-3 group-hover:bg-[#f4a300] group-hover:text-white transition-colors">
<!-- CheckShield SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
</div>
<h4 class="text-sm font-bold text-[#1f2a3a]">Approve On-the-Go</h4>
<p class="text-xs text-[#8a96a8] mt-1">SAQA executives can approve pre-travel authorizations instantly inside the app.</p>
</div>
</div>
</div>
<div class="card bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="mb-4">
<h3 class="text-[15px] font-bold">Adoption and Usage Analysis</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Continuous telemetry tracking of mobile app engagements</p>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2]">
<div>
<div class="text-xs font-bold text-[#1f2a3a]">Active Installations</div>
<div class="text-[10px] text-[#8a96a8]">Across all corporate profiles</div>
</div>
<strong class="text-sm text-[#0066b3]">@ActiveInstalls of 238</strong>
</div>
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2]">
<div>
<div class="text-xs font-bold text-[#1f2a3a]">May Receipts Scanned</div>
<div class="text-[10px] text-[#8a96a8]">Submissions cleared via OCR</div>
</div>
<strong class="text-sm text-[#22a06b]">@ReceiptsScannedCount</strong>
</div>
<div class="flex items-center justify-between py-2 border-b border-dashed border-[#e4eaf2]">
<div>
<div class="text-xs font-bold text-[#1f2a3a]">Mobile Self-Bookings</div>
<div class="text-[10px] text-[#8a96a8]">Executed via corporate tool</div>
</div>
<strong class="text-sm text-[#003b71]">34% of all trips</strong>
</div>
<div class="p-3.5 bg-[#effaf3] border border-[#d2f1df] rounded-lg text-xs text-[#1b8054] flex gap-2">
<!-- ShieldCheck SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
<div>
<span class="font-bold block">Biometric POPIA Security compliance</span>
<p class="text-[11px] mt-0.5 text-[#2b7250]">Companion logs zero database footprints locally on device storage caches. All connection frames are sealed with AES-256 state channels.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h3>Mobile</h3>
@code { @code {
// Model Definitions
public class MockExpense
{
public string Date { get; set; } = string.Empty;
public string Merchant { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string ForeignAmount { get; set; } = string.Empty;
public decimal ZarAmount { get; set; }
}
public class ChatBubble
{
public string Sender { get; set; } = string.Empty;
public string Timestamp { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public bool IsTraveller { get; set; }
}
// Core states
private string ActivePhoneTab { get; set; } = "profile";
private int ActiveInstalls { get; set; } = 186;
private int ReceiptsScannedCount { get; set; } = 412;
private decimal TotalValueScanned { get; set; } = 184200M;
private string AlertFeedback { get; set; } = string.Empty;
// Active Traveller Details (Synchronized across UI metrics)
private string ActiveTraveller { get; set; } = "J. Mokoena";
private string ActiveTravellerInitials { get; set; } = "JM";
private int LoyaltyMiles { get; set; } = 84210;
// Live OCR Scanning State simulation
private bool IsScanning { get; set; } = false;
// Chat Interface Simulator states
private string NewChatMessage { get; set; } = string.Empty;
private bool IsTyping { get; set; } = false;
private int UnreadChatCount { get; set; } = 1;
// Mock Database Arrays
private List<MockExpense> MockExpenses { get; set; } = new();
private List<ChatBubble> ChatMessages { get; set; } = new();
protected override void OnInitialized()
{
// Populate standard mock receipts matching the React Seed dataset
MockExpenses = new List<MockExpense>
{
new MockExpense { Date = "20 May", Merchant = "Heathrow Express", Category = "Ground Transport", ForeignAmount = "£25.00", ZarAmount = 602.50M },
new MockExpense { Date = "20 May", Merchant = "Pret a Manger", Category = "Meals", ForeignAmount = "£12.40", ZarAmount = 298.84M },
new MockExpense { Date = "21 May", Merchant = "Hilton Nairobi", Category = "Accommodation", ForeignAmount = "$210.00", ZarAmount = 3886.20M },
new MockExpense { Date = "22 May", Merchant = "Java House", Category = "Meals", ForeignAmount = "$14.50", ZarAmount = 268.39M }
};
// Populate initial live consultant conversation
ChatMessages = new List<ChatBubble>
{
new ChatBubble { Sender = "System Bot", Timestamp = "09:12 AM", Text = "Your travel profile is pre-verified for the CPT trip.", IsTraveller = false },
new ChatBubble { Sender = "J. Mokoena", Timestamp = "09:14 AM", Text = "Hi, is my hotel voucher HT7811 checked with billing back?", IsTraveller = true },
new ChatBubble { Sender = "Consultant (Sarah)", Timestamp = "09:15 AM", Text = "Yes, Mr. Mokoena. The bill-back to SAQA lodge card has been settled with the Premier hotel desk.", IsTraveller = false }
};
}
private void SetPhoneTab(string tabName)
{
ActivePhoneTab = tabName;
if (tabName == "chat")
{
UnreadChatCount = 0;
}
}
// Handles traveller changes from selector
private void OnTravellerDeviceChanged(ChangeEventArgs e)
{
var selected = e.Value?.ToString() ?? "J. Mokoena";
ActiveTraveller = selected;
ActiveTravellerInitials = string.Concat(selected.Split(' ').Select(n => n[0])).ToUpper();
if (selected == "J. Mokoena")
{
LoyaltyMiles = 84210;
}
else if (selected == "T. Maluleke")
{
LoyaltyMiles = 112450;
}
else if (selected == "A. Dlamini")
{
LoyaltyMiles = 45120;
}
else
{
LoyaltyMiles = 12890;
}
// Regenerate contextual mock messages for the selected traveller
ChatMessages.Clear();
ChatMessages.Add(new ChatBubble { Sender = "Sarah (TMT Desk)", Timestamp = "11:00 AM", Text = $"Welcome to SAQA Support channel, {ActiveTraveller}. How may we assist you today?", IsTraveller = false });
TriggerFeedback($"Device synchronization profile swapped to {ActiveTraveller} successfully.");
}
// Live Simulated OCR Scanner trigger
private async Task TriggerMockOcrCapture()
{
IsScanning = true;
StateHasChanged();
// Simulated visual scan wait
await Task.Delay(2000);
// Generate contextual mock foreign receipt based on current random factors
var rand = new System.Random();
var currencies = new[] { ("€", 19.98M, "Uber Paris"), ("$", 18.42M, "Starbucks NY"), ("£", 23.34M, "Tesco London") };
var chosen = currencies[rand.Next(currencies.Length)];
decimal foreignVal = rand.Next(10, 45) + 0.99M;
decimal convertedVal = foreignVal * chosen.Item2;
var newExpense = new MockExpense
{
Date = "Today",
Merchant = chosen.Item3,
Category = "Business Travel Expense",
ForeignAmount = $"{chosen.Item1}{foreignVal.ToString("F2")}",
ZarAmount = System.Math.Round(convertedVal, 2)
};
MockExpenses.Insert(0, newExpense);
ReceiptsScannedCount++;
TotalValueScanned += convertedVal;
IsScanning = false;
TriggerFeedback($"Live OCR Scan Completed! Detected {newExpense.Merchant} receipt: {newExpense.ForeignAmount} -> Auto-converted to R {newExpense.ZarAmount.ToString("N2")} at bank rate.");
StateHasChanged();
}
// Chat Simulator actions
private void HandleChatKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
_ = SendChatMessage();
}
}
private async Task SendChatMessage()
{
if (string.IsNullOrWhiteSpace(NewChatMessage)) return;
var userMsg = new ChatBubble
{
Sender = ActiveTraveller,
Timestamp = "Just Now",
Text = NewChatMessage,
IsTraveller = true
};
ChatMessages.Add(userMsg);
var inputQuery = NewChatMessage;
NewChatMessage = string.Empty;
IsTyping = true;
StateHasChanged();
// Simulate typical support operator delay
await Task.Delay(1500);
string replyText = "Understood. Let me look up your SAQA corporate itinerary. I will respond to this query immediately via email or text alert.";
// Simple contextual mock chat responder
var lowered = inputQuery.ToLower();
if (lowered.Contains("hotel") || lowered.Contains("voucher"))
{
replyText = $"I can confirm your booking voucher HT7811 with billing settled on Lodge Card. Late check-in has been requested at the desk.";
}
else if (lowered.Contains("flight") || lowered.Contains("gate") || lowered.Contains("delay"))
{
replyText = $"Let me check SA 311. Gate A12 is current and boarding is still scheduled for 05:30 AM SAST. No delays are reported on the GDS.";
}
else if (lowered.Contains("claim") || lowered.Contains("expense") || lowered.Contains("ocr"))
{
replyText = "For all claims, simply snap a picture under the Expenses tab inside this app. Our offline OCR sync transforms it into a draft claim instantly.";
}
var botMsg = new ChatBubble
{
Sender = " Sarah (Travel Desk)",
Timestamp = "Just Now",
Text = replyText,
IsTraveller = false
};
ChatMessages.Add(botMsg);
IsTyping = false;
StateHasChanged();
}
// Feedback Alert helper
private void TriggerFeedback(string message)
{
AlertFeedback = message;
_ = DismissAlertAfterDelay();
}
private async Task DismissAlertAfterDelay()
{
await Task.Delay(5000);
AlertFeedback = string.Empty;
StateHasChanged();
}
} }
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,186 @@
@page "/trip-reports" @page "/trip-reports"
@rendermode InteractiveServer
<h3>TripReports</h3> <div class="p-6 lg:p-7 bg-[#f4f7fb] min-h-screen font-sans text-[#1f2a3a]">
<!-- PAGE HEADER -->
<div class="flex items-end justify-between mb-[22px] flex-wrap gap-3">
<div>
<h2 class="text-[22px] font-bold text-[#1f2a3a] tracking-tight">Trip Reports</h2>
<p class="text-[13px] text-[#5a6577] mt-1">System generated consolidated cost per trip, in line with NT Cost Containment reporting</p>
</div>
<div class="flex gap-2">
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-[#f4f7fb] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" /></svg>
Filter
</button>
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-transparent bg-[#0066b3] text-white hover:bg-[#003b71] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path 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 to Excel
</button>
</div>
</div>
<!-- MAIN TRIP REPORT CARD -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,.06),_0_1px_3px_rgba(15,23,42,.04)] mb-[22px]">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Trip TRP-2026-0488, J. Mokoena, Cape Town engagement</h3>
<p class="text-[12px] text-[#8a96a8] mt-[2px]">Travel Authorisation TA-44219, dated 02 to 04 Jun 2026</p>
</div>
<span class="bg-[#e3f6ed] text-[#22a06b] px-[10px] py-[3px] rounded-full text-[11px] font-semibold">All components consolidated</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[.06em] font-semibold px-3 py-[10px] whitespace-nowrap">Component</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[.06em] font-semibold px-3 py-[10px] whitespace-nowrap">Supplier</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[.06em] font-semibold px-3 py-[10px] whitespace-nowrap">Reference</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[.06em] font-semibold px-3 py-[10px] whitespace-nowrap">Date</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[.06em] font-semibold px-3 py-[10px] whitespace-nowrap">Negotiated</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[.06em] font-semibold px-3 py-[10px] whitespace-nowrap">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var item in ConsolidatedItems)
{
<tr class="hover:bg-[#fafcfe] border-b border-[#e4eaf2] last:border-b-0">
<td class="px-3 py-3 text-[13px] text-[#1f2a3a] align-middle">
<span class="@GetComponentBadgeClass(item.Type) px-[10px] py-[3px] rounded-full text-[11px] font-semibold">
@item.Component
</span>
</td>
<td class="px-3 py-3 text-[13px] text-[#1f2a3a] align-middle font-medium">@item.Supplier</td>
<td class="px-3 py-3 text-[13px] text-[#5a6577] align-middle font-mono">@item.Reference</td>
<td class="px-3 py-3 text-[13px] text-[#5a6577] align-middle">@item.Date</td>
<td class="px-3 py-3 text-[13px] text-[#5a6577] align-middle">@item.Negotiated</td>
<td class="px-3 py-3 text-[13px] text-[#1f2a3a] align-middle font-semibold">@item.Amount</td>
</tr>
}
<tr class="bg-[#f6fafe] border-t border-[#e4eaf2]">
<td colspan="5" class="px-3 py-3 text-[13px] text-[#1f2a3a] font-bold">Total trip cost</td>
<td class="px-3 py-3 text-[13px] text-[#1f2a3a] font-bold">R 12,960.00</td>
</tr>
<tr class="bg-[#effaf3] border-t border-[#e4eaf2]">
<td colspan="5" class="px-3 py-3 text-[13px] text-[#22a06b] font-medium">Savings against open rates</td>
<td class="px-3 py-3 text-[13px] text-[#22a06b] font-bold">R 1,840.00</td>
</tr>
</tbody>
</table>
</div>
<div class="flex gap-2 mt-4 flex-wrap">
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-transparent bg-[#0066b3] text-white hover:bg-[#003b71] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">Download PDF</button>
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-[#f4f7fb] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">Send to Finance</button>
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-[#f4f7fb] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">Attach to PO 44219</button>
</div>
</div>
<!-- MONTHLY AND SLA REPORTS GRID -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Standard Monthly Reports Card -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,.06),_0_1px_3px_rgba(15,23,42,.04)]">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Standard Monthly Reports</h3>
<p class="text-[12px] text-[#8a96a8] mt-[2px]">Auto-issued per NT Cost Containment template</p>
</div>
</div>
<div class="flex flex-col">
@foreach (var report in MonthlyReports)
{
<div class="flex items-center justify-between py-3 border-b border-dashed border-[#e4eaf2] last:border-b-0">
<div>
<div class="text-[13px] text-[#1f2a3a] font-medium">@report.Title</div>
<div class="text-[11px] text-[#8a96a8]">@report.Description</div>
</div>
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-[#f4f7fb] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">Download</button>
</div>
}
</div>
</div>
<!-- SLA Reports Card -->
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,.06),_0_1px_3px_rgba(15,23,42,.04)]">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold text-[#1f2a3a]">SLA Reports</h3>
<p class="text-[12px] text-[#8a96a8] mt-[2px]">Per the SAQA SLA template</p>
</div>
</div>
<div class="flex flex-col">
@foreach (var report in SlaReports)
{
<div class="flex items-center justify-between py-3 border-b border-dashed border-[#e4eaf2] last:border-b-0">
<div>
<div class="text-[13px] text-[#1f2a3a] font-medium">@report.Title</div>
<div class="text-[11px] text-[#8a96a8]">@report.Description</div>
</div>
<button class="inline-flex items-center gap-[6px] px-[14px] py-2 rounded-lg text-[13px] font-semibold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-[#f4f7fb] transition-all duration-150 cursor-pointer shadow-[0_1px_2px_rgba(15,23,42,.06)]">Download</button>
</div>
}
</div>
</div>
</div>
</div>
@code { @code {
public class ReportItem
{
public string Component { get; set; } = string.Empty;
public string Supplier { get; set; } = string.Empty;
public string Reference { get; set; } = string.Empty;
public string Date { get; set; } = string.Empty;
public string Negotiated { get; set; } = string.Empty;
public string Amount { get; set; } = string.Empty;
public string Type { get; set; } = "default";
}
public class ReportInfo
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
private List<ReportItem> ConsolidatedItems = new()
{
new ReportItem { Component = "Flight (out)", Supplier = "SAA, SA 311", Reference = "AB23PQ", Date = "02 Jun", Negotiated = "Yes, 12 percent off", Amount = "R 2,890.00", Type = "blue" },
new ReportItem { Component = "Flight (return)", Supplier = "SAA, SA 322", Reference = "AB23PQ", Date = "04 Jun", Negotiated = "Yes", Amount = "R 2,640.00", Type = "blue" },
new ReportItem { Component = "Hotel", Supplier = "Premier Hotel Waterfront", Reference = "HT7811", Date = "02 to 04 Jun, 2 nights", Negotiated = "Govt rate", Amount = "R 3,790.00", Type = "default" },
new ReportItem { Component = "Car rental", Supplier = "Avis, Group B", Reference = "AV1140", Date = "02 to 04 Jun, 3 days", Negotiated = "Yes", Amount = "R 1,420.00", Type = "gold" },
new ReportItem { Component = "Shuttle", Supplier = "SA Express Shuttle", Reference = "SX0921", Date = "02 Jun", Negotiated = "No", Amount = "R 380.00", Type = "default" },
new ReportItem { Component = "Daily allowance", Supplier = "SAQA per diem policy", Reference = "PD-019", Date = "3 days", Negotiated = "n/a", Amount = "R 1,560.00", Type = "default" },
new ReportItem { Component = "Service fee", Supplier = "TMT Projects", Reference = "SF-2026-0488", Date = "02 Jun", Negotiated = "Flat", Amount = "R 280.00", Type = "green" }
};
private List<ReportInfo> MonthlyReports = new()
{
new ReportInfo { Title = "Cost per trip report", Description = "Consolidated, all components, all travellers" },
new ReportInfo { Title = "Spend by category", Description = "Air, accommodation, ground, other" },
new ReportInfo { Title = "Traveller activity", Description = "Per cost centre and per traveller" }
};
private List<ReportInfo> SlaReports = new()
{
new ReportInfo { Title = "After hours report", Description = "All out of hours support events" },
new ReportInfo { Title = "Consultant productivity", Description = "Bookings handled per consultant" },
new ReportInfo { Title = "Refund and open voucher log", Description = "Status of all open items" }
};
private string GetComponentBadgeClass(string type)
{
return type switch
{
"blue" => "bg-[#e2eefb] text-[#0066b3]",
"green" => "bg-[#e3f6ed] text-[#22a06b]",
"gold" => "bg-[#fff4dd] text-[#b06b00]",
_ => "bg-[#e8f4f4] text-[#00a3a1]"
};
}
} }
+762 -1
View File
@@ -1,7 +1,768 @@
@page "/vas" @page "/vas"
@rendermode InteractiveServer
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
@* Page Header Area *@
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
<div class="text-left">
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">Value-Added Services (VAS)</h2>
<p class="text-xs text-[#5a6577] mt-1">Global destination intelligence, live travel risk feeds, and premium VIP traveller concierge support</p>
</div>
<div class="flex gap-2">
<button @onclick="OpenVipBookingModal"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer">
<!-- Sparkles Star SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.98 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
</svg>
Book VIP Lounge &amp; Concierge
</button>
</div>
</div>
@* Alert / Feedback Notification Banner *@
@if (!string.IsNullOrEmpty(AlertFeedback))
{
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-sm text-[#1b8054] flex items-center gap-2.5 shadow-sm transition-all text-left">
<!-- Alert Icon SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
<span class="font-medium">@AlertFeedback</span>
</div>
}
@* Quick intelligence overview widgets *@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
@foreach (var dest in IntelligentDestinations)
{
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left flex flex-col justify-between transition-all hover:shadow-md relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full @(dest.RiskLevel == "High" ? "before:bg-[#dc2626]" : dest.RiskLevel == "Moderate" ? "before:bg-[#f4a300]" : "before:bg-[#22a06b]")">
<div>
<div class="flex justify-between items-start mb-3">
<div class="text-left">
<h3 class="text-[16px] font-bold text-[#1f2a3a]">@dest.City, @dest.CountryCode</h3>
<p class="text-[11px] text-[#8a96a8] mt-0.5">Traveller: @dest.Traveller</p>
</div>
<span class="text-3xl">@dest.WeatherEmoji</span>
</div>
<div class="flex items-center gap-3 mb-4">
<div class="text-2xl font-bold tracking-tight text-[#003b71]">@dest.Temperature°C</div>
<div class="text-xs text-[#5a6577]">
<span class="font-semibold block text-gray-800">@dest.WeatherCondition</span>
<span>Local time: @dest.LocalTime</span>
</div>
</div>
<div class="space-y-2 border-t border-[#e4eaf2] pt-3 text-xs">
<div class="flex justify-between items-center py-1">
<span class="text-[#8a96a8]">Visa Requirement:</span>
<span class="px-2 py-0.5 rounded text-[10px] font-bold @(dest.VisaStatus == "eVisa Required" ? "bg-[#fff4dd] text-[#b06b00]" : dest.VisaStatus == "Standard Visa" ? "bg-[#fde5e8] text-[#dc2626]" : "bg-[#e3f6ed] text-[#22a06b]")">
@dest.VisaStatus
</span>
</div>
<div class="flex justify-between items-center py-1">
<span class="text-[#8a96a8]">Health Warning:</span>
<span class="font-medium text-[#1f2a3a]">@dest.HealthWarning</span>
</div>
<div class="flex justify-between items-center py-1">
<span class="text-[#8a96a8]">Global Safety Risk:</span>
<span class="font-bold uppercase text-[11px] @(dest.RiskLevel == "High" ? "text-[#dc2626]" : dest.RiskLevel == "Moderate" ? "text-[#b06b00]" : "text-[#22a06b]")">
@dest.RiskLevel
</span>
</div>
</div>
</div>
<div class="mt-4 pt-3 border-t border-[#e4eaf2] flex gap-2">
<button @onclick="() => ExploreDestinationInDepth(dest)"
class="w-full text-center py-1.5 bg-[#f4f7fb] hover:bg-[#e2eefb] text-[#0066b3] text-xs font-bold rounded transition">
Explore Destination Guide
</button>
</div>
</div>
}
</div>
@* Risk Advisories and Services Splitting grid *@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 mb-6">
@* Live Travel Advisories Feed - Left Column (lg:col-span-7) *@
<div class="lg:col-span-7 bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left flex flex-col justify-between">
<div>
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div>
<h3 class="text-base font-bold text-[#003b71]">Global Safety &amp; Risk Feed</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Real-time alerts synced through TMT global risk metrics and WMO meteorology bulletins</p>
</div>
<div class="flex gap-1">
<button @onclick='() => SetAlertFilter("All")'
class="px-2 py-0.5 rounded text-[10px] font-bold transition @(SelectedAlertFilter == "All" ? "bg-[#0066b3] text-white" : "bg-[#f4f7fb] text-gray-500 hover:bg-gray-200")">
All
</button>
<button @onclick='() => SetAlertFilter("Critical")'
class="px-2 py-0.5 rounded text-[10px] font-bold transition @(SelectedAlertFilter == "Critical" ? "bg-[#dc2626] text-white" : "bg-[#f4f7fb] text-gray-500 hover:bg-gray-200")">
High Risk
</button>
<button @onclick='() => SetAlertFilter("Info")'
class="px-2 py-0.5 rounded text-[10px] font-bold transition @(SelectedAlertFilter == "Info" ? "bg-[#00a3a1] text-white" : "bg-[#f4f7fb] text-gray-500 hover:bg-gray-200")">
Advisory
</button>
</div>
</div>
<div class="space-y-3.5 max-h-[360px] overflow-y-auto pr-1">
@foreach (var alert in FilteredRiskAlerts)
{
<div class="p-3 border rounded-xl transition-all hover:bg-[#fafcfe] flex items-start gap-3 @(alert.Severity == "Critical" ? "bg-[#fdeef0] border-[#f8b4b4]" : alert.Severity == "Warning" ? "bg-[#fff9ec] border-[#fbd897]" : "bg-[#f3f8fd] border-[#b4d3f5]")">
<div class="shrink-0 mt-1">
@if (alert.Severity == "Critical")
{
<!-- Critical Red Alert SVG -->
<svg class="w-5 h-5 text-[#dc2626]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
}
else if (alert.Severity == "Warning")
{
<!-- Warning Orange SVG -->
<svg class="w-5 h-5 text-[#f4a300]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
}
else
{
<!-- Info Blue SVG -->
<svg class="w-5 h-5 text-[#0066b3]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 111.063.852l-.708 2.836a.75.75 0 001.063.852l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
}
</div>
<div class="flex-1 text-xs">
<div class="flex justify-between items-baseline mb-0.5">
<h4 class="font-bold text-[#101827]">@alert.Title</h4>
<span class="text-[9px] font-mono text-gray-500">@alert.PublishedTime</span>
</div>
<p class="text-[#5a6577] mt-1">@alert.Description</p>
<div class="mt-2 flex items-center justify-between border-t border-gray-200/50 pt-2 text-[10px]">
<span class="font-bold text-[#003b71]">Category: @alert.Category</span>
<button @onclick="() => SimulateNotifyTravellers(alert)"
class="px-2 py-0.5 bg-white hover:bg-gray-100 border border-gray-300 text-gray-700 font-bold rounded shadow-xs">
Notify Affected Travellers
</button>
</div>
</div>
</div>
}
</div>
</div>
<div class="mt-4 pt-3.5 border-t border-[#e4eaf2] text-center text-xs text-gray-500">
<span>Alert definitions are updated dynamically via real-time international GDS PNR telemetry streams.</span>
</div>
</div>
@* Value-added Services At a Glance - Right Column (lg:col-span-5) *@
<div class="lg:col-span-5 space-y-6">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="mb-4">
<h3 class="text-base font-bold text-[#003b71]">Always-On Concierge Services</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Select a capability to simulate corporate booking and guidelines</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
@* Tile 1: Airport VIP Lounge *@
<div @onclick="OpenVipBookingModal"
class="p-3 border border-[#e4eaf2] rounded-xl hover:border-[#0066b3] hover:bg-[#f4f8fc]/40 transition-all cursor-pointer group">
<div class="h-8 w-8 rounded-lg bg-[#e2eefb] text-[#0066b3] flex items-center justify-center mb-2.5 group-hover:bg-[#0066b3] group-hover:text-white transition-colors">
<!-- Star SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.98 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] group-hover:text-[#0066b3] transition-colors">Airport Lounges</h4>
<p class="text-[10px] text-[#8a96a8] mt-0.5">Pre-book SLOW &amp; international Bidvest airport lounges.</p>
</div>
@* Tile 2: Shuttle Transfer Calcs *@
<div @onclick="OpenShuttleModal"
class="p-3 border border-[#e4eaf2] rounded-xl hover:border-[#00a3a1] hover:bg-[#f2faf9]/50 transition-all cursor-pointer group">
<div class="h-8 w-8 rounded-lg bg-[#e0f7f6] text-[#00a3a1] flex items-center justify-center mb-2.5 group-hover:bg-[#00a3a1] group-hover:text-white transition-colors">
<!-- Truck/Car SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.129-1.125V11.25M12 9V5.25a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 5.25v2.25m9-1.5h.008v.008H13.5V6zm2.25 2.25h.008v.008H15.75V8.25zm2.25 2.25h.008v.008H18V10.5z" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] group-hover:text-[#00a3a1] transition-colors">Shuttle Transfers</h4>
<p class="text-[10px] text-[#8a96a8] mt-0.5">Calculate airport transfers, shuttles &amp; express trains.</p>
</div>
@* Tile 3: Places of Interest *@
<div @onclick="OpenCultureModal"
class="p-3 border border-[#e4eaf2] rounded-xl hover:border-[#22a06b] hover:bg-[#edfbf4]/50 transition-all cursor-pointer group">
<div class="h-8 w-8 rounded-lg bg-[#e3f6ed] text-[#22a06b] flex items-center justify-center mb-2.5 group-hover:bg-[#22a06b] group-hover:text-white transition-colors">
<!-- Compass/Globe SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 2.25v2.25m0 15v2.25M4.5 12h2.25M17.25 12H21" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] group-hover:text-[#22a06b] transition-colors">Culture Guides</h4>
<p class="text-[10px] text-[#8a96a8] mt-0.5">Corporate travel orientations &amp; localized city guides.</p>
</div>
@* Tile 4: Health Warnings Tracker *@
<div @onclick="OpenHealthTrackerModal"
class="p-3 border border-[#e4eaf2] rounded-xl hover:border-[#f4a300] hover:bg-[#fffbf2]/50 transition-all cursor-pointer group">
<div class="h-8 w-8 rounded-lg bg-[#fff4dd] text-[#f4a300] flex items-center justify-center mb-2.5 group-hover:bg-[#f4a300] group-hover:text-white transition-colors">
<!-- Shield Exclamation SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
</div>
<h4 class="text-xs font-bold text-[#1f2a3a] group-hover:text-[#f4a300] transition-colors">Health &amp; Vaccines</h4>
<p class="text-[10px] text-[#8a96a8] mt-0.5">WHO, CDC Yellow Fever certificate lookup checker.</p>
</div>
</div>
</div>
@* Travel Help Desk Assistance Box *@
<div class="p-4 bg-gradient-to-br from-[#003b71] to-[#0066b3] text-white rounded-xl text-left space-y-3.5 shadow-lg relative overflow-hidden">
<div class="absolute -right-8 -bottom-8 w-24 h-24 bg-white/5 rounded-full"></div>
<div>
<span class="text-[9px] bg-white/20 text-[#00d4d1] px-2 py-0.5 rounded-full font-bold uppercase tracking-wider">Premium Support</span>
<h3 class="text-sm font-bold mt-1.5">24/7 Global Emergency Assistance</h3>
<p class="text-[11px] text-blue-100 mt-1">If a traveller gets stranded, requires flight changes, or encounters medical events, our emergency assistance room handles all fast-tracks instantly.</p>
</div>
<div class="flex items-center justify-between text-xs pt-1 border-t border-white/10">
<div>
<span class="block text-[9px] text-blue-200">Local Help-desk</span>
<span class="font-mono font-semibold">0800 TMT HELP</span>
</div>
<div>
<span class="block text-[9px] text-blue-200">International Hotline</span>
<span class="font-mono font-semibold">+27 82 555 4400</span>
</div>
</div>
</div>
</div>
</div>
</div>
@* ===================== MODAL DIALOGS ===================== *@
@* 1. Book VIP Lounge & Concierge Modal *@
@if (ShowVipBookingModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 text-left">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div>
<h3 class="text-base font-bold text-[#003b71]">Book Premium VIP Services</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Fast-track security and airport lounge clearance for SAQA travellers</p>
</div>
<button @onclick="CloseVipBookingModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4 text-xs">
<div>
<label class="block font-bold text-gray-700 uppercase mb-1">Select Traveller</label>
<select @bind="VipSelectedTraveller" class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-[#f4f7fb]">
<option value="J. Mokoena">J. Mokoena (Executive)</option>
<option value="T. Maluleke">T. Maluleke (Director)</option>
<option value="A. Dlamini">A. Dlamini (Senior Manager)</option>
<option value="S. Pillay">S. Pillay (Coordinator)</option>
</select>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block font-bold text-gray-700 uppercase mb-1">Departure Airport</label>
<select @bind="VipSelectedAirport" class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-[#f4f7fb]">
<option value="OR Tambo (JNB)">OR Tambo JNB</option>
<option value="Cape Town (CPT)">Cape Town CPT</option>
<option value="King Shaka (DUR)">King Shaka DUR</option>
<option value="Heathrow (LHR)">London Heathrow LHR</option>
</select>
</div>
<div>
<label class="block font-bold text-gray-700 uppercase mb-1">Service Level</label>
<select @bind="VipServiceLevel" class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-[#f4f7fb]">
<option value="Airport Lounge Access">Airport Lounge Access Only</option>
<option value="Fast-track + Lounge">Fast-track + Lounge Clearance</option>
<option value="Full Concierge Meet &amp; Greet">Full VIP Meet &amp; Greet Meetup</option>
</select>
</div>
</div>
<div>
<label class="block font-bold text-gray-700 uppercase mb-1">Flight Departure Date</label>
<input type="date" @bind="VipFlightDate" class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-[#f4f7fb]" />
</div>
<div class="p-3 bg-[#e2eefb] text-[#0066b3] rounded-lg">
<p class="font-bold flex items-center gap-1.5 mb-1">
<!-- Info SVG -->
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 16v-4m0-4h.01" />
</svg>
SAQA Travel Policy Check
</p>
<p>Airport lounge clearances are automatically covered for executive travellers. Other staff require line-manager travel authorization justification files linked to their PO.</p>
</div>
<div class="flex gap-2 justify-end pt-3 border-t border-[#e4eaf2]">
<button @onclick="CloseVipBookingModal" class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 font-semibold text-gray-700">
Cancel
</button>
<button @onclick="ProcessVipBooking" class="px-4 py-2 bg-[#0066b3] hover:bg-[#003b71] text-white font-semibold rounded-lg shadow-sm">
Issue Concierge Pass
</button>
</div>
</div>
</div>
</div>
}
@* 2. Public Transport & Shuttle Calculator Modal *@
@if (ShowShuttleModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 text-left">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div>
<h3 class="text-base font-bold text-[#003b71]">Airport Shuttle Transfer Calculator</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Calculate corporate transfers vs. public options at destination</p>
</div>
<button @onclick="CloseShuttleModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4 text-xs">
<div>
<label class="block font-bold text-gray-700 uppercase mb-1">Select Destination City</label>
<select @bind="SelectedCalcCity" class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-[#f4f7fb]">
<option value="London">London (Heathrow LHR)</option>
<option value="Nairobi">Nairobi (Jomo Kenyatta NBO)</option>
<option value="Cape Town">Cape Town (CPT)</option>
</select>
</div>
@{
var options = GetTransferOptions(SelectedCalcCity);
}
<div class="space-y-2 border border-dashed border-[#e4eaf2] p-3 rounded-lg bg-[#fafcfe]">
<div class="font-bold text-[#003b71] mb-2">Cost Comparison Matrix (converted to ZAR)</div>
@foreach (var opt in options)
{
<div class="flex justify-between items-center py-1.5 border-b border-gray-100 last:border-none">
<div>
<span class="block font-semibold text-gray-800">@opt.Name</span>
<span class="text-[9px] text-[#8a96a8]">Time: @opt.Duration</span>
</div>
<div class="text-right">
<span class="block font-bold text-[#1f2a3a]">R @opt.ZarCost.ToString("N0")</span>
<span class="text-[9px] px-1 rounded bg-[#e2eefb] text-[#0066b3]">@opt.Type</span>
</div>
</div>
}
</div>
<div class="p-3 bg-[#e2eefb] text-[#0066b3] rounded-lg">
<p class="font-semibold block">TMT Recommendations:</p>
<p class="mt-0.5">Our preferred corporate shuttle transfers provide meet &amp; greet in the terminal arrivals hall, reducing delays and offering bill-back directly to the corporate travel card.</p>
</div>
<div class="flex gap-2 justify-end pt-3 border-t border-[#e4eaf2]">
<button @onclick="CloseShuttleModal" class="px-4 py-2 bg-[#0066b3] text-white hover:bg-[#003b71] font-semibold rounded-lg shadow-sm">
Close Calculator
</button>
</div>
</div>
</div>
</div>
}
@* 3. Culture Guidelines Modal *@
@if (ShowCultureModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 text-left">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div>
<h3 class="text-base font-bold text-[#003b71]">Travel Orientation &amp; Cultural Guidelines</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Empowering corporate travellers with destination intelligence and local advice</p>
</div>
<button @onclick="CloseCultureModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#1f2a3a] overflow-y-auto max-h-[400px]">
<div class="space-y-3">
<div class="p-3.5 border border-[#e4eaf2] rounded-lg bg-[#fafcfe]">
<div class="font-bold text-[#003b71] text-xs uppercase mb-1">United Kingdom (London Orientation)</div>
<p class="text-xs text-[#5a6577]">Always keep to the left on tube escalators to avoid delays. Mobile contactless is widely accepted across all public transport. Standard tipping is 10-12.5% on meals if service charge is not already included. Safe and well-lit walking spaces throughout.</p>
</div>
<div class="p-3.5 border border-[#e4eaf2] rounded-lg bg-[#fafcfe]">
<div class="font-bold text-[#00a3a1] text-xs uppercase mb-1">Kenya (Nairobi Orientation)</div>
<p class="text-xs text-[#5a6577]">M-Pesa mobile money is universal; ask the help-desk on how to set it up prior to landing. Avoid carrying excess physical cash or wearing flashy items. Pre-arranged hotel transport or Uber is highly recommended for late-night airport transfers.</p>
</div>
<div class="p-3.5 border border-[#e4eaf2] rounded-lg bg-[#fafcfe]">
<div class="font-bold text-[#22a06b] text-xs uppercase mb-1">South Africa (Cape Town Orientation)</div>
<p class="text-xs text-[#5a6577]">Local microclimate weather shifts can occur quickly; packing lightweight warm layers is ideal. Use pre-arranged airport transfer shuttles rather than flagging roadside cabs. High alert on security awareness in localized central sectors after hours.</p>
</div>
</div>
</div>
<div class="flex justify-end pt-4 border-t border-[#e4eaf2]">
<button @onclick="CloseCultureModal" class="px-4 py-2 bg-[#003b71] hover:bg-[#00264d] text-white font-semibold rounded-lg shadow-sm">
Close Orientation
</button>
</div>
</div>
</div>
}
@* 4. Health Warnings Tracker & Vaccine Requirements *@
@if (ShowHealthTrackerModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 text-left">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div>
<h3 class="text-base font-bold text-[#003b71]">Health &amp; Vaccine Checklist Portal</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Pre-travel health screening for SAQA corporate compliance</p>
</div>
<button @onclick="CloseHealthTrackerModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4 text-xs">
<div class="p-3 bg-[#effaf3] text-[#1b8054] rounded-lg flex gap-2">
<!-- ShieldCheck SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
</svg>
<div>
<span class="font-bold block">International Yellow Card</span>
<p class="text-[11px] mt-0.5">SAQA travellers returning from East Africa, West Africa or South America require a physical Yellow Fever vaccine certificate logged on checkout.</p>
</div>
</div>
<div class="space-y-3">
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#dc2626] mb-1">Nairobi, Kenya</div>
<p class="text-gray-600">Yellow Fever vaccination certificate mandatory upon landing. Malaria prophylaxis is strongly advised for excursions outside urban Nairobi.</p>
</div>
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#0066b3] mb-1">United Kingdom &amp; Schengen Area</div>
<p class="text-gray-600">No special vaccine cards or malaria restrictions required. General healthcare reciprocal frameworks apply for travel insurance files.</p>
</div>
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#f4a300] mb-1">United Arab Emirates (Dubai)</div>
<p class="text-gray-600">Ensure any special prescription medicine is logged on official SAQA Support channels beforehand, as UAE customs maintains strict medical framework checklists.</p>
</div>
</div>
</div>
<div class="flex justify-end pt-4 border-t border-[#e4eaf2] mt-4">
<button @onclick="CloseHealthTrackerModal" class="px-4 py-2 bg-[#003b71] hover:bg-[#00264d] text-white font-semibold rounded-lg shadow-sm">
Close Tracker
</button>
</div>
</div>
</div>
}
<h3>Vas</h3>
@code { @code {
// Model Definitions
public class DestinationGuide
{
public string City { get; set; } = string.Empty;
public string CountryCode { get; set; } = string.Empty;
public string Traveller { get; set; } = string.Empty;
public string WeatherEmoji { get; set; } = string.Empty;
public int Temperature { get; set; }
public string WeatherCondition { get; set; } = string.Empty;
public string LocalTime { get; set; } = string.Empty;
public string VisaStatus { get; set; } = string.Empty;
public string HealthWarning { get; set; } = string.Empty;
public string RiskLevel { get; set; } = string.Empty;
}
public class GlobalRiskAlert
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string PublishedTime { get; set; } = string.Empty;
public string Severity { get; set; } = "Info"; // Critical, Warning, Info
public string Category { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
public class AirportTransferOpt
{
public string Name { get; set; } = string.Empty;
public string Duration { get; set; } = string.Empty;
public decimal ZarCost { get; set; }
public string Type { get; set; } = string.Empty; // Train, Shuttle, Cab
}
// Core States
private string AlertFeedback { get; set; } = string.Empty;
private string SelectedAlertFilter { get; set; } = "All";
// Modals Control
private bool ShowVipBookingModal { get; set; } = false;
private bool ShowShuttleModal { get; set; } = false;
private bool ShowCultureModal { get; set; } = false;
private bool ShowHealthTrackerModal { get; set; } = false;
// Vip booking form states
private string VipSelectedTraveller { get; set; } = "J. Mokoena";
private string VipSelectedAirport { get; set; } = "OR Tambo (JNB)";
private string VipServiceLevel { get; set; } = "Fast-track + Lounge";
private DateTime VipFlightDate { get; set; } = new DateTime(2026, 6, 2);
// Shuttle calculator states
private string SelectedCalcCity { get; set; } = "London";
// Databases
private List<DestinationGuide> IntelligentDestinations { get; set; } = new();
private List<GlobalRiskAlert> SafetyAlerts { get; set; } = new();
// Filtered Risk Alerts derived properties
private IEnumerable<GlobalRiskAlert> FilteredRiskAlerts =>
SelectedAlertFilter == "All"
? SafetyAlerts
: SelectedAlertFilter == "Critical"
? SafetyAlerts.Where(a => a.Severity == "Critical")
: SafetyAlerts.Where(a => a.Severity == "Warning" || a.Severity == "Info");
protected override void OnInitialized()
{
// Populate standard intelligence trackers matching React seed database
IntelligentDestinations = new List<DestinationGuide>
{
new DestinationGuide
{
City = "London",
CountryCode = "UK",
Traveller = "T. Maluleke",
WeatherEmoji = "☁️",
Temperature = 14,
WeatherCondition = "Light drizzle, windy",
LocalTime = "10:15 AM BST",
VisaStatus = "Standard Visa",
HealthWarning = "None active",
RiskLevel = "Low"
},
new DestinationGuide
{
City = "Nairobi",
CountryCode = "KE",
Traveller = "A. Dlamini",
WeatherEmoji = "⛅",
Temperature = 22,
WeatherCondition = "Partly Cloudy",
LocalTime = "12:15 PM EAT",
VisaStatus = "eVisa Required",
HealthWarning = "Yellow Fever Mandatory",
RiskLevel = "Moderate"
},
new DestinationGuide
{
City = "Cape Town",
CountryCode = "ZA",
Traveller = "J. Mokoena",
WeatherEmoji = "🌧️",
Temperature = 12,
WeatherCondition = "Heavy rain, cold front",
LocalTime = "11:15 AM SAST",
VisaStatus = "Not Applicable",
HealthWarning = "None",
RiskLevel = "Low"
}
};
// Populate global advisories feed
SafetyAlerts = new List<GlobalRiskAlert>
{
new GlobalRiskAlert
{
Id = 1,
Title = "Planned Ground Crew Strike action (London Heathrow)",
PublishedTime = "22 May 2026, 09:14 AM",
Severity = "Critical",
Category = "Labor Dispute",
Description = "A 48-hour ground handler strike is notified for Heathrow Airport Terminal 2 & 5. Expect airline rescheduling, baggage delays, and boarding queues. SAQA travellers on the BA route have been contacted for pre-arranged lounge transfers."
},
new GlobalRiskAlert
{
Id = 2,
Title = "Tropical Cyclone advisory (Mozambique Channel)",
PublishedTime = "23 May 2026, 08:30 AM",
Severity = "Warning",
Category = "Severe Weather",
Description = "High-velocity tropical system tracking near Beira. Staff scheduled for Maputo or regional liaison should avoid non-essential regional flights until June 2."
},
new GlobalRiskAlert
{
Id = 3,
Title = "High Capacity Notice: SLOW Lounge OR Tambo JNB",
PublishedTime = "24 May 2026, 07:15 AM",
Severity = "Info",
Category = "Facility Advisory",
Description = "SLOW lounge is reporting high peak queues between 06:00 and 09:00 SAST daily. Executive travellers are advised to pre-book their slots using this companion app to guarantee priority entry."
}
};
}
// Interactive Filters
private void SetAlertFilter(string filter)
{
SelectedAlertFilter = filter;
}
// Destinations action guides
private void ExploreDestinationInDepth(DestinationGuide dest)
{
TriggerFeedback($"Loaded complete Orientation Portfolio for {dest.City} ({dest.CountryCode}). Sending localized safety briefs to {dest.Traveller}'s device.");
}
private void SimulateNotifyTravellers(GlobalRiskAlert alert)
{
TriggerFeedback($"Notification sent successfully! Pushed critical alert '{alert.Title}' as an SMS and inside the Companion Wallet of active SAQA travellers.");
}
// Vip booking processors
private void ProcessVipBooking()
{
TriggerFeedback($"VIP Service Request successfully processed! Authorized '{VipServiceLevel}' for {VipSelectedTraveller} departing {VipSelectedAirport} on {VipFlightDate.ToString("yyyy-MM-dd")}. Billed to SAQA Lodge Card.");
ShowVipBookingModal = false;
}
// Airport transfer calculator helper
private List<AirportTransferOpt> GetTransferOptions(string city)
{
if (city == "London")
{
return new List<AirportTransferOpt>
{
new AirportTransferOpt { Name = "Heathrow Express (Train)", Duration = "15 mins", ZarCost = 600M, Type = "Train" },
new AirportTransferOpt { Name = "Elizabeth Line Tube", Duration = "35 mins", ZarCost = 310M, Type = "Train" },
new AirportTransferOpt { Name = "Corporate Partner Shuttle", Duration = "45 mins", ZarCost = 1450M, Type = "Shuttle" }
};
}
else if (city == "Nairobi")
{
return new List<AirportTransferOpt>
{
new AirportTransferOpt { Name = "Pre-arranged Hotel Shuttle", Duration = "25 mins", ZarCost = 450M, Type = "Shuttle" },
new AirportTransferOpt { Name = "Local Rideshare App (Uber)", Duration = "30 mins", ZarCost = 280M, Type = "Cab" }
};
}
else
{
return new List<AirportTransferOpt>
{
new AirportTransferOpt { Name = "SA Express Shuttle Service", Duration = "20 mins", ZarCost = 380M, Type = "Shuttle" },
new AirportTransferOpt { Name = "MyCiTi Airport Bus 110", Duration = "30 mins", ZarCost = 120M, Type = "Shuttle" },
new AirportTransferOpt { Name = "Airport authorized Private Cab", Duration = "25 mins", ZarCost = 320M, Type = "Cab" }
};
}
}
// Modals visibility Helpers
private void OpenVipBookingModal()
{
ShowVipBookingModal = true;
}
private void CloseVipBookingModal()
{
ShowVipBookingModal = false;
}
private void OpenShuttleModal()
{
ShowShuttleModal = true;
}
private void CloseShuttleModal()
{
ShowShuttleModal = false;
}
private void OpenCultureModal()
{
ShowCultureModal = true;
}
private void CloseCultureModal()
{
ShowCultureModal = false;
}
private void OpenHealthTrackerModal()
{
ShowHealthTrackerModal = true;
}
private void CloseHealthTrackerModal()
{
ShowHealthTrackerModal = false;
}
// Toast triggered alert toaster
private void TriggerFeedback(string message)
{
AlertFeedback = message;
_ = DismissAlertAfterDelay();
}
private async Task DismissAlertAfterDelay()
{
await Task.Delay(5000);
AlertFeedback = string.Empty;
StateHasChanged();
}
} }
File diff suppressed because it is too large Load Diff
@@ -1,66 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1 class="text-3xl font-bold mb-4">Weather</h1>
<p class="mb-6 text-gray-700">This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p class="italic text-gray-500">Loading...</p>
}
else
{
<div class="overflow-x-auto shadow-sm ring-1 ring-black ring-opacity-5 rounded-lg bg-white">
<table class="min-w-full text-left border-collapse">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 border-b border-gray-300 text-sm font-semibold text-gray-900">Date</th>
<th scope="col" class="px-6 py-3 border-b border-gray-300 text-sm font-semibold text-gray-900" aria-label="Temperature in Celsius">Temp. (C)</th>
<th scope="col" class="px-6 py-3 border-b border-gray-300 text-sm font-semibold text-gray-900" aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th scope="col" class="px-6 py-3 border-b border-gray-300 text-sm font-semibold text-gray-900">Summary</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach (var forecast in forecasts)
{
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap">@forecast.Date.ToShortDateString()</td>
<td class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap">@forecast.TemperatureC</td>
<td class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap">@forecast.TemperatureF</td>
<td class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap">@forecast.Summary</td>
</tr>
}
</tbody>
</table>
</div>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
+1586 -29
View File
File diff suppressed because it is too large Load Diff