@page "/forex"

Foreign Exchange and Expense Management

Order forex, load travel money cards, capture and reconcile foreign expenses

@if (ShowAlertFeedback) {
@AlertFeedbackText
}
Open Forex Orders
@OpenForexOrdersCount
Awaiting collection or load
Loaded on Travel Cards
USD @LoadedCardBalance.ToString("N0")
@ActiveCardsCount active travel cards
Settled This Month
R @SettledAmountZar.ToString("N0")
Reconciled to lodge card
Unreconciled Receipts
@UnreconciledReceiptsCount
Awaiting traveller action

Live Forex Rates

Updated @LastUpdatedTime.ToString("dd MMM yyyy, HH:mm") SAST

Bank rate
@foreach (var rate in ForexRates) { }
Currency Buy Sell 1 Day Action
@rate.Code @rate.Name R @rate.BuyRate.ToString("N2") R @rate.SellRate.ToString("N2") @if (rate.DayChange >= 0) { ▲ @rate.DayChange.ToString("N2") } else { ▼ @Math.Abs(rate.DayChange).ToString("N2") }

Quick ZAR Estimate Calculator

Estimated ZAR R @CalculatedZarAmount.ToString("N2")

Travel Money Card

Multi-currency, reloadable, traveller assigned

Active
@if (IsCardFrozen) {
Frozen
}
SAQA Travel Money Card
@MaskedCardNumber
Holder @CardHolderName
USD Balance $@CardUsdBalance.ToString("N0")
GBP Balance £@CardGbpBalance.ToString("N0")
Top up USD
Auto-reload at $200
Card Freeze Control
instantly freeze or unfreeze this card 24/7

Foreign Expense Capture and Reconciliation

Receipts auto converted at transaction rate

OCR enabled
@foreach (var expense in FilteredExpenses) { }
Date Traveller Merchant Foreign ZAR Category Status Action
@expense.Date @expense.Traveller @expense.Merchant @expense.ForeignCurrency @expense.ForeignAmount.ToString("N2") R @expense.ZarAmount.ToString("N2") @expense.Category @expense.Status @if (expense.Status != "Reconciled") { } else { Complete }
@if (ShowOrderDialog) {

Order Foreign Currency

Exchange Rate: 1 @SelectedOrderCurrency = @GetSellRate(SelectedOrderCurrency).ToString("N2") ZAR
Estimated Cost: R @((OrderAmount * GetSellRate(SelectedOrderCurrency)).ToString("N2"))
} @if (ShowLoadDialog) {

Load Travel Money Card

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