Files
tmtsaqatravelmanager/Tmt.SaqaTravel/Components/Pages/Vas.razor
T
2026-07-09 21:41:26 +02:00

769 lines
44 KiB
Plaintext

@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>
}
@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();
}
}