Stable main pages
This commit is contained in:
@@ -1,7 +1,691 @@
|
||||
@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 {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user