@page "/mobile" @using System.Collections.Generic @using System.Linq @using System.Threading.Tasks
@* Page Header Area *@

SAQA Mobile Companion

Empowering travellers with real-time itinerary updates, offline OCR expense capturing, and instant consultant chat

@* Alert / Feedback Notification Banner *@ @if (!string.IsNullOrEmpty(AlertFeedback)) {
@AlertFeedback
} @* Adoption & Capture Progress Cards *@
Active Mobile Users
@ActiveInstalls / 238
78% adoption across SAQA
Receipts Scanned (OCR)
@ReceiptsScannedCount
R @TotalValueScanned.ToString("N0") auto-reconciled
Mobile Bookings Share
34%
May 2026 reporting period
Avg Approval Turnaround
1h 12m
92% faster than email workflows
@* Core interactive layout split *@
@* Interactive Phone Bezel Simulator - Left Column (lg:col-span-5) *@
@* Front Camera Notch *@
@* Phone Inner Screen Canvas *@
@* Phone Status Bar *@
11:15 AM
@* Simulated Active Mobile Screen View *@
@* Screen Header *@
@ActiveTravellerInitials

@ActiveTraveller

SAQA corporate account
Online
@* Live View Contents based on Active Phone tab *@ @if (ActivePhoneTab == "profile") {
SAA Voyager Program
@LoyaltyMiles.ToString("N0") Miles
Gold status

Marriott Bonvoy Partner ID Linked

Verified Documents
ZA Passport Valid Oct 2029
United Kingdom Visa Valid Dec 2027
Schengen short-stay Awaiting renewal
Travel Preferences
Aisle Seat preferred Halal Meal Non-Smoking Floor Avis automatic group B
} else if (ActivePhoneTab == "expenses") {
Captured Overseas Receipts
OCR Sync active
@foreach (var exp in MockExpenses) {
@exp.Merchant
@exp.Category · @exp.Date
R @exp.ZarAmount.ToString("N2")
@exp.ForeignAmount
Reconciled
}
@if (IsScanning) {
Simulating Live Receipt Photo OCR Scan...

Auto-converting exchange rates to ZAR settlement metrics

}
} else if (ActivePhoneTab == "itinerary") {
Active Business Itinerary
Cape Town Engagement
Gate Open
Flight (Outbound)
SAA flight SA 311

Johannesburg (JNB) to Cape Town (CPT)

Boarding 05:30 AM
Seat Assigned 14C (Aisle)
Gate A12 (Terminal B)
Accommodation Check-In
Premier Hotel Waterfront

2 Nights confirmed lodging · PO linked

Voucher reference: HT7811 · Billed to SAQA Lodge Card

} else if (ActivePhoneTab == "chat") {
@foreach (var msg in ChatMessages) {
@msg.Sender · @msg.Timestamp
@msg.Text
} @if (IsTyping) {
Consultant typing...
}
}
@* Phone Bottom Navigation Bar *@
@* Traveller Profile Selector (Updates the phone UI dynamically) *@

Swapping the active traveller updates the profile sync settings and loyalties on the phone bezel above.

@* Mobile Capabilities Grid and Adopt Cards - Right Column (lg:col-span-7) *@

Dynamic Mobile Capabilities

Purpose-built workflows configured for SAQA corporate compliance

Always Syncing
@* Cap 1 *@

Profile & Doc Synchronization

Automatic sync of passport expiries, visas, and airline reward levels before tickets are processed.

@* Cap 2 *@

Receipt OCR Engine (Offline Ready)

Translates international receipts into ZAR using historical exchange conversion tables. Works in airplanes without Wi-Fi.

@* Cap 3 *@

Push Notification Alerts

Real-time alerts for gate changes, boarding statuses, flight delays, or global travel risk notifications.

@* Tile 4 *@

Approve On-the-Go

SAQA executives can approve pre-travel authorizations instantly inside the app.

Adoption and Usage Analysis

Continuous telemetry tracking of mobile app engagements

Active Installations
Across all corporate profiles
@ActiveInstalls of 238
May Receipts Scanned
Submissions cleared via OCR
@ReceiptsScannedCount
Mobile Self-Bookings
Executed via corporate tool
34% of all trips
Biometric POPIA Security compliance

Companion logs zero database footprints locally on device storage caches. All connection frames are sealed with AES-256 state channels.

@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 MockExpenses { get; set; } = new(); private List ChatMessages { get; set; } = new(); protected override void OnInitialized() { // Populate standard mock receipts matching the React Seed dataset MockExpenses = new List { 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 { 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(); } }