559 lines
30 KiB
Plaintext
559 lines
30 KiB
Plaintext
@page "/forex"
|
|
|
|
<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 {
|
|
// ===================== 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;
|
|
}
|
|
}
|
|
} |