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

1346 lines
80 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/booking"
@using System
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
@rendermode InteractiveServer
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
@* Page Head *@
<div class="flex items-end justify-between mb-6 flex-wrap gap-4">
<div>
<h2 class="text-2xl font-bold text-[#1f2a3a] tracking-tight">Online GDS Booking Console</h2>
<p class="text-xs text-[#5a6577] mt-1">GDS-integrated · Amadeus, Travelport &amp; Sabre · e-ticketing &amp; vouchers</p>
</div>
<div class="flex gap-2">
<button @onclick="@(() => ToggleMyBookingsFilter())"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-semibold border @(ShowMyBookingsOnly ? "bg-[#0066b3] text-white border-[#0066b3]" : "bg-white border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#f4f7fb]") transition-all cursor-pointer">
<!-- User Profile 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>
@(ShowMyBookingsOnly ? "Showing My Bookings" : "All Corporate Bookings")
</button>
<button @onclick="@(() => OpenNewTripModal())"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-semibold border border-transparent bg-[#0066b3] text-white hover:bg-[#003b71] transition-all cursor-pointer shadow-xs">
<!-- Plus 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="M12 4.5v15m7.5-7.5h-15" />
</svg>
New Custom Trip
</button>
</div>
</div>
@* Notification Feedback Banner *@
@if (!string.IsNullOrEmpty(AlertFeedback))
{
<div class="mb-6 p-4 rounded-xl border border-emerald-200 bg-emerald-50 text-emerald-800 flex items-center justify-between shadow-xs animate-fade-in text-xs font-semibold">
<div class="flex items-center gap-2">
<!-- Check Circle SVG -->
<svg class="w-5 h-5 text-emerald-600 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>@AlertFeedback</span>
</div>
<button @onclick="@(() => AlertFeedback = string.Empty)" class="text-emerald-500 hover:text-emerald-700 cursor-pointer">
<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 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
}
@* Financial & Booking KPI Dashboard *@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 text-left">
<div class="bg-white border border-[#e4eaf2] rounded-xl p-4 shadow-xs flex items-center gap-4">
<div class="p-3 rounded-lg bg-[#e2eefb] text-[#0066b3]">
<!-- CreditCard SVG -->
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
</svg>
</div>
<div>
<span class="text-[10px] text-[#8a96a8] font-bold uppercase tracking-wider block">Lodge Card Balance</span>
<strong class="text-lg font-extrabold text-[#1f2a3a]">R @LodgeCardBalance.ToString("N0")</strong>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-xl p-4 shadow-xs flex items-center gap-4">
<div class="p-3 rounded-lg bg-[#e3f6ed] text-[#22a06b]">
<!-- ShoppingBag SVG -->
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg>
</div>
<div>
<span class="text-[10px] text-[#8a96a8] font-bold uppercase tracking-wider block">Active Bookings Value</span>
<strong class="text-lg font-extrabold text-[#1f2a3a]">R @GetActiveBookingsValue().ToString("N0")</strong>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-xl p-4 shadow-xs flex items-center gap-4">
<div class="p-3 rounded-lg bg-[#fff4dd] text-[#b06b00]">
<!-- Ticket SVG -->
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-12v12m9-12H3a1.5 1.5 0 00-1.5 1.5v12A1.5 1.5 0 003 19.5h18a1.5 1.5 0 001.5-1.5v-12A1.5 1.5 0 0021 6h-4.5z" />
</svg>
</div>
<div>
<span class="text-[10px] text-[#8a96a8] font-bold uppercase tracking-wider block">Unused e-Ticket Credits</span>
<strong class="text-lg font-extrabold text-[#1f2a3a]">R @GetUnusedTicketsValue().ToString("N0")</strong>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-xl p-4 shadow-xs flex items-center gap-4">
<div class="p-3 rounded-lg bg-[#fdeef0] text-[#dc2626]">
<!-- ClipboardDocumentCheck SVG -->
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.03 0 1.9.693 2.166 1.638m-7.377 2.24l-.407-.145m-3.59 3.39c-.765-.311-1.296-1.07-1.313-1.907a48.254 48.254 0 01-1.025-.138" />
</svg>
</div>
<div>
<span class="text-[10px] text-[#8a96a8] font-bold uppercase tracking-wider block">Approval Backlog</span>
<strong class="text-lg font-extrabold text-[#1f2a3a]">@tripRequests.Count(t => t.Status == "Pending") Awaiting</strong>
</div>
</div>
</div>
@* Search Card *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_1px_3px_rgba(15,23,42,0.04)] mb-[18px]">
@* Tabs *@
<div class="flex gap-1 border-b border-[#e4eaf2] mb-[18px]">
<button @onclick='() => SetTab("flights")'
class="px-4 py-2.5 text-[13px] font-semibold flex items-center gap-1.5 border-b-2 -mb-px transition-colors cursor-pointer @(activeTab == "flights" ? "text-[#0066b3] border-[#0066b3]" : "text-[#5a6577] border-transparent hover:text-[#1f2a3a]")">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4"><path d="M17.8 19.2L16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2 3.4 7.6l5.6 3-4 4-3-1L1 14l4 2 2 4 1.4-1.4-1-3 4-4 3 5.6 1.4-1.4z" /></svg>
Flights
</button>
<button @onclick='() => SetTab("hotels")'
class="px-4 py-2.5 text-[13px] font-semibold flex items-center gap-1.5 border-b-2 -mb-px transition-colors cursor-pointer @(activeTab == "hotels" ? "text-[#0066b3] border-[#0066b3]" : "text-[#5a6577] border-transparent hover:text-[#1f2a3a]")">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4"><path d="M3 21V8a2 2 0 012-2h14a2 2 0 012 2v13" /><path d="M9 21V12h6v9" /><path d="M3 21h18" /></svg>
Hotels
</button>
<button @onclick='() => SetTab("cars")'
class="px-4 py-2.5 text-[13px] font-semibold flex items-center gap-1.5 border-b-2 -mb-px transition-colors cursor-pointer @(activeTab == "cars" ? "text-[#0066b3] border-[#0066b3]" : "text-[#5a6577] border-transparent hover:text-[#1f2a3a]")">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4"><path d="M14 16H9m10 0h2v-3.4a2 2 0 00-.5-1.3L18 8H6L3.5 11.3a2 2 0 00-.5 1.3V16h2" /><circle cx="7" cy="17" r="2" /><circle cx="17" cy="17" r="2" /></svg>
Cars &amp; Shuttle
</button>
</div>
@* Form Row *@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_1fr_auto] gap-3 items-end text-left">
@if (activeTab == "flights")
{
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">From</label>
<input type="text" @bind="FlightFrom" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">To</label>
<input type="text" @bind="FlightTo" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Depart</label>
<input type="date" @bind="FlightDate" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Cabin</label>
<select @bind="FlightCabin" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors">
<option value="Economy (Policy)">Economy (Policy)</option>
<option value="Business (Exec only)">Business (Exec only)</option>
<option value="Premium Economy">Premium Economy</option>
</select>
</div>
}
else if (activeTab == "hotels")
{
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Destination / City</label>
<input type="text" @bind="HotelLocation" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Check-In</label>
<input type="date" @bind="HotelCheckIn" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Check-Out</label>
<input type="date" @bind="HotelCheckOut" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Occupancy</label>
<select @bind="HotelGuests" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors">
<option value="1 Guest">1 Guest</option>
<option value="2 Guests">2 Guests</option>
<option value="3+ Guests (Group)">3+ Guests (Group)</option>
</select>
</div>
}
else
{
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Pick-up Station</label>
<input type="text" @bind="CarLocation" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Pick-up Date</label>
<input type="date" @bind="CarPickUp" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Return Date</label>
<input type="date" @bind="CarReturn" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors" />
</div>
<div>
<label class="block text-[11px] text-[#5a6577] font-semibold mb-1.5 uppercase tracking-[0.04em]">Vehicle Tier</label>
<select @bind="CarClass" class="w-full px-3 py-[9px] border border-[#c8d3e1] rounded-lg text-[13px] text-[#1f2a3a] bg-white outline-none focus:border-[#0066b3] transition-colors">
<option value="Compact / Hatchback">Compact / Hatchback (Policy)</option>
<option value="Sedan / Comfort">Sedan / Comfort</option>
<option value="SUV / Premium">SUV / Premium</option>
</select>
</div>
}
<button @onclick="@(() => TriggerSearch())"
class="inline-flex items-center justify-center gap-1.5 px-6 py-[9px] rounded-lg text-[13px] font-semibold border border-transparent bg-[#0066b3] text-white hover:bg-[#003b71] transition-all h-[39px] cursor-pointer shadow-sm">
<!-- Search SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.637 10.637z" />
</svg>
Search
</button>
</div>
</div>
@* Simulated GDS Node Loader / Spinner *@
@if (IsSearching)
{
<div class="bg-white border border-[#e4eaf2] rounded-xl p-8 mb-[22px] flex flex-col items-center justify-center text-center shadow-xs animate-pulse">
<div class="w-10 h-10 border-4 border-t-transparent border-[#0066b3] rounded-full animate-spin mb-4"></div>
<strong class="text-sm font-bold text-[#1f2a3a]">Interrogating GDS Telemetry Nodes...</strong>
<p class="text-xs text-[#8a96a8] mt-1">Fetching pre-negotiated corporate rates from Amadeus, Sabre and Travelport databases</p>
</div>
}
else
{
@* Grid 3: Results *@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-[22px] text-left">
@foreach (var result in GetActiveSearchResults())
{
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_1px_3px_rgba(15,23,42,0.04)] flex flex-col justify-between hover:border-[#0066b3] transition-all">
<div>
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[14px] font-extrabold text-[#1f2a3a]">@result.Title</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">@result.Subtitle</p>
</div>
<span class="inline-flex px-2.5 py-[3px] rounded-full text-[10px] font-bold uppercase tracking-wider @result.BadgeClass">@result.BadgeText</span>
</div>
</div>
<div class="flex justify-between items-end mt-4 pt-4 border-t border-dashed border-[#e4eaf2]">
<div>
<div class="text-[20px] font-extrabold text-[#1f2a3a] tracking-tight">R @result.Price.ToString("N0")</div>
<span class="text-[11px] text-[#8a96a8]">@result.HighlightText</span>
</div>
<button @onclick="@(() => OpenBookingModal(result))"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold border border-transparent bg-[#0066b3] text-white hover:bg-[#003b71] transition-all cursor-pointer shadow-xs">
Book
</button>
</div>
</div>
}
</div>
}
@* Grid 2: Delivery & Unused e-Tickets *@
<div class="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4 mb-[22px] text-left">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_1px_3px_rgba(15,23,42,0.04)] 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-[15px] font-bold text-[#1f2a3a]">Unused e-Tickets</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">Tracked &amp; refund-eligible corporate flight stock</p>
</div>
<span class="inline-flex bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">@unusedTickets.Count(t => !t.IsRefunded && !t.IsReused) open</span>
</div>
<div class="space-y-1">
@foreach (var ticket in unusedTickets)
{
<div class="flex items-center justify-between py-3 border-b border-dashed border-[#e4eaf2] last:border-none transition-all">
<div>
<div class="text-[13px] text-[#1f2a3a] font-medium flex items-center gap-2">
@ticket.TicketDetails for @ticket.Passenger
@if (ticket.IsRefunded)
{
<span class="bg-[#e3f6ed] text-[#22a06b] px-2 py-0.5 rounded text-[9px] font-bold uppercase">Refunded</span>
}
else if (ticket.IsReused)
{
<span class="bg-[#e2eefb] text-[#0066b3] px-2 py-0.5 rounded text-[9px] font-bold uppercase">Reused</span>
}
</div>
<div class="text-[11px] text-[#8a96a8]">Expires @ticket.ExpiryDate · Face Value: <strong class="text-[#003b71]">R @ticket.Price.ToString("N0")</strong></div>
</div>
<div class="flex gap-1.5">
@if (!ticket.IsRefunded && !ticket.IsReused)
{
<button @onclick="@(() => OpenRefundModal(ticket))"
class="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-bold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-all cursor-pointer">
Refund
</button>
<button @onclick="@(() => OpenReuseModal(ticket))"
class="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-bold border border-[#e4eaf2] bg-white text-[#0066b3] hover:bg-[#e2eefb] transition-all cursor-pointer">
Reuse
</button>
}
</div>
</div>
}
</div>
</div>
<div class="p-3 bg-[#e2eefb] text-[#0066b3] rounded-lg text-xs font-medium flex items-center gap-2 mt-4">
<svg class="w-4 h-4 shrink-0" 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 0z" />
</svg>
<span>Refunds are directly re-credited to SAQA's master Lodge Card balance instantly upon approval.</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_1px_3px_rgba(15,23,42,0.04)] 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-[15px] font-bold text-[#1f2a3a]">Delivery &amp; Notifications</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">E-tickets, SMS alerts &amp; hotel vouchers</p>
</div>
<span class="inline-flex bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Active Feed</span>
</div>
<div class="space-y-3 max-h-[360px] overflow-y-auto pr-1">
@foreach (var log in deliveryLogs)
{
<div class="flex gap-3 p-3 rounded-[10px] border-l-4 @log.TypeClass @log.BgClass">
<div class="w-7 h-7 rounded-full bg-white/70 flex items-center justify-center shrink-0">
@if (log.Icon == "sms")
{
<!-- DevicePhoneMobile SVG -->
<svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
}
else if (log.Icon == "email")
{
<!-- Envelope SVG -->
<svg class="w-4 h-4 text-sky-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
</svg>
}
else
{
<!-- Warning SVG -->
<svg class="w-4 h-4 text-amber-600" 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>
}
</div>
<div class="text-xs">
<h4 class="font-bold text-[#1f2a3a]">@log.Title</h4>
<p class="text-[#5a6577] mt-0.5 leading-snug">@log.Content</p>
<span class="text-[9px] text-[#8a96a8] font-mono mt-1 block">@log.Timestamp</span>
</div>
</div>
}
</div>
</div>
<button @onclick="@(() => TriggerDeliveryTest())"
class="mt-4 w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold rounded-lg text-xs transition cursor-pointer">
Simulate Direct GDS Node Ping
</button>
</div>
</div>
@* Grid 2: Approval & Cancellations *@
<div class="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4 mb-[22px] text-left">
@* Approvals Table *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_1px_3px_rgba(15,23,42,0.04)] 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-[15px] font-bold text-[#1f2a3a]">Travel Approval Queue</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">Manager sign-off required before GDS auto-ticketing</p>
</div>
<span class="inline-flex bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">@tripRequests.Count(t => t.Status == "Pending") awaiting</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-[0.06em] font-semibold px-3 py-2.5 whitespace-nowrap">Traveller</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold px-3 py-2.5 whitespace-nowrap">Trip</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold px-3 py-2.5 whitespace-nowrap">Cost</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold px-3 py-2.5 whitespace-nowrap">Approver</th>
<th class="text-right text-[11px] text-[#8a96a8] uppercase tracking-[0.06em] font-semibold px-3 py-2.5 whitespace-nowrap">Action</th>
</tr>
</thead>
<tbody>
@foreach (var trip in tripRequests)
{
<tr class="transition-colors group hover:bg-[#fafcfe] border-b border-[#e4eaf2] last:border-none">
<td class="p-3 text-[13px] text-[#1f2a3a] align-middle">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-[10px] text-white font-bold bg-gradient-to-br from-[#0066b3] to-[#00a3a1]">@trip.Initials</div>
<div>
<strong class="font-bold block text-[13px]">@trip.Name</strong>
<small class="block text-[#8a96a8] text-[11px]">@trip.Role</small>
</div>
</div>
</td>
<td class="p-3 text-[13px] text-[#1f2a3a] align-middle">@trip.TripDetails</td>
<td class="p-3 text-[13px] text-[#1f2a3a] font-bold text-[#003b71] align-middle">@trip.Cost</td>
<td class="p-3 text-[13px] text-[#1f2a3a] align-middle">
<span class="inline-flex bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[10px] font-bold uppercase">@trip.ApproverRole</span>
</td>
<td class="p-3 text-[13px] text-[#1f2a3a] align-middle text-right">
@if (trip.Status == "Pending")
{
<button @onclick="@(() => OpenApprovalModal(trip))"
class="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-bold border border-transparent bg-[#0066b3] text-white hover:bg-[#003b71] cursor-pointer transition-all">
Review Request
</button>
}
else if (trip.Status == "Approved")
{
<span class="inline-flex bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[10px] font-bold uppercase tracking-wider">Approved</span>
}
else
{
<span class="inline-flex bg-[#fde5e8] text-[#dc2626] px-2.5 py-[3px] rounded-full text-[10px] font-bold uppercase tracking-wider">Declined</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@* Manage Bookings *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_1px_3px_rgba(15,23,42,0.04)] 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-[15px] font-bold text-[#1f2a3a]">Active Corporate Bookings</h3>
<p class="text-[12px] text-[#8a96a8] mt-0.5">Amend, cancel &amp; track refunds</p>
</div>
<span class="inline-flex bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Live Feed</span>
</div>
<div class="space-y-1">
@foreach (var booking in GetFilteredBookings())
{
<div class="flex items-center justify-between py-3 border-b border-dashed border-[#e4eaf2] last:border-none transition-opacity duration-300 @(booking.IsCancelled ? "opacity-55" : "")">
<div>
<div class="text-[13px] text-[#1f2a3a] font-medium flex items-center gap-2">
@booking.Title
@if (booking.IsCancelled)
{
<span class="inline-flex bg-[#fde5e8] text-[#dc2626] px-2.5 py-[3px] rounded-full text-[10px] font-bold uppercase tracking-wider">Cancelled</span>
}
</div>
<div class="text-[11px] text-[#8a96a8]">@booking.Subtitle · <strong>R @booking.Price.ToString("N0")</strong></div>
</div>
<div class="flex gap-1.5 shrink-0">
@if (booking.IsCancelled)
{
<span class="inline-flex bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[10px] font-bold uppercase">Refund Queued</span>
}
else
{
<button @onclick="@(() => OpenModifyModal(booking))"
class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-bold border border-[#e4eaf2] bg-white text-[#1f2a3a] hover:bg-[#f4f7fb] transition-all cursor-pointer">
Modify
</button>
<button @onclick="@(() => OpenCancelConfirmModal(booking))"
class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-bold border border-[#fde5e8] bg-white text-[#dc2626] hover:bg-red-50 transition-all cursor-pointer">
Cancel
</button>
}
</div>
</div>
}
</div>
</div>
<div class="flex gap-3 p-3.5 rounded-[10px] border-l-4 border-[#22a06b] bg-[#effaf3] mt-4 text-[#22a06b]">
<div class="w-8 h-8 rounded-full bg-white/70 flex items-center justify-center shrink-0">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4"><polyline points="20 6 9 17 4 12" /></svg>
</div>
<div>
<h4 class="text-[13px] font-semibold mb-0.5 text-[#1f2a3a]">Automated voucher</h4>
<p class="text-[12px] text-[#5a6577] leading-[1.45]">Vouchers are auto-sent to hotel, traveller and back-office instantly on finalization.</p>
</div>
</div>
</div>
</div>
</div>
@* ===================== MODAL DIALOGS ===================== *@
@* 1. Booking Confirmation Modal *@
@if (showBookingModal && selectedSearchResult != null)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-lg w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<div>
<span class="text-[10px] bg-[#e2eefb] text-[#0066b3] px-2 py-0.5 rounded font-bold uppercase tracking-wider block w-fit mb-1">GDS Transaction Gateway</span>
<h3 class="text-base font-bold text-[#003b71]">Confirm GDS Lodge Booking</h3>
</div>
<button @onclick="@(() => showBookingModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<div class="p-3.5 bg-slate-50 border border-[#e4eaf2] rounded-lg">
<div class="text-[10px] uppercase font-bold tracking-wider text-[#8a96a8]">Selected Offering</div>
<div class="text-sm font-bold text-[#1f2a3a] mt-1">@selectedSearchResult.Title</div>
<div class="text-xs text-[#5a6577] mt-0.5">@selectedSearchResult.Subtitle</div>
<div class="flex justify-between items-baseline mt-2.5 pt-2 border-t border-dashed border-[#c8d3e1]">
<span class="font-semibold text-[#1f2a3a]">Cost Center Rate:</span>
<span class="text-lg font-black text-[#0066b3]">R @selectedSearchResult.Price.ToString("N0")</span>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Assign Traveller</label>
<select @bind="selectedTraveller" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] font-semibold outline-none focus:border-[#0066b3]">
<option value="J. Mokoena">J. Mokoena (Executive)</option>
<option value="T. Maluleke">T. Maluleke (Director)</option>
<option value="A. Dlamini">A. Dlamini (Coordinator)</option>
<option value="N. Ndlovu">N. Ndlovu (Officer)</option>
<option value="S. Pillay">S. Pillay (Assoc)</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Cost Center</label>
<select @bind="selectedCostCentre" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] font-semibold outline-none focus:border-[#0066b3]">
<option value="Executive Administration">Exec Admin (C01)</option>
<option value="Corporate Research Division">Corporate Research (C03)</option>
<option value="Finance &amp; Operations Support">Finance &amp; Ops (C08)</option>
<option value="Standard Operations Division">Std Ops (C12)</option>
</select>
</div>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Special Instructions &amp; Luggage</label>
<input type="text" @bind="bookingSpecialNotes" placeholder="e.g. Wheelchair support, aisle seat preferred, early breakfast" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div class="p-3 bg-[#effaf3] text-[#22a06b] border border-emerald-100 rounded-lg space-y-1">
<strong class="font-bold flex items-center gap-1.5 uppercase tracking-wider text-[10px]">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4" /></svg>
Corporate Policy Verification
</strong>
<p class="text-[11px] leading-relaxed">This travel option fits comfortably within SAQA National Treasury Instruction 03 guidelines and budget thresholds. Lodge card is pre-authorized.</p>
</div>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => showBookingModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Cancel</button>
<button @onclick="@(() => ConfirmBooking())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#22a06b] hover:bg-[#1b8054] cursor-pointer shadow-xs">Authorize GDS Ticket</button>
</div>
</div>
</div>
}
@* 2. Refund Confirmation Modal *@
@if (showRefundModal && selectedTicket != null)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-red-700 flex items-center gap-1.5">
<svg class="w-5 h-5 shrink-0" 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>
Confirm e-Ticket Refund
</h3>
<button @onclick="@(() => showRefundModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-3.5 text-xs text-[#5a6577]">
<p>Are you sure you want to request an immediate GDS automated refund for this unused e-ticket?</p>
<div class="p-3 bg-red-50 border border-red-100 rounded-lg">
<div class="font-bold text-[#1f2a3a]">@selectedTicket.TicketDetails</div>
<div class="text-[11px] mt-0.5">Traveller: @selectedTicket.Passenger · Expiry: @selectedTicket.ExpiryDate</div>
<div class="font-bold text-red-700 mt-1.5 text-sm">Refund Amount: R @selectedTicket.Price.ToString("N0")</div>
</div>
<p class="text-[11px] text-gray-400">Note: Airline administrative fees of R150 will be auto-settled via GDS system. The net refund will reflect on the lodge card instantly.</p>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => showRefundModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Abort</button>
<button @onclick="@(() => ConfirmRefund())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-red-600 hover:bg-red-700 cursor-pointer shadow-xs">Process GDS Refund</button>
</div>
</div>
</div>
}
@* 3. Reuse Credit Confirmation Modal *@
@if (showReuseModal && selectedTicket != null)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-[#003b71] flex items-center gap-1.5">
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.656 48.656 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7C4.547 9.547 4.5 10.768 4.5 12s.047 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.092-1.209.138-2.43.138-3.662z" /><path stroke-linecap="round" stroke-linejoin="round" d="M9 10.5l3 3 3-3" /></svg>
Reassign Unused ticket credit
</h3>
<button @onclick="@(() => showReuseModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<p>You are reassigning <strong class="text-slate-800">R @selectedTicket.Price.ToString("N0")</strong> of unused ticket credit from @selectedTicket.Passenger.</p>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Target Traveller for reuse</label>
<select @bind="reusedTargetPassenger" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] font-semibold outline-none focus:border-[#0066b3]">
<option value="J. Mokoena">J. Mokoena (Executive)</option>
<option value="T. Maluleke">T. Maluleke (Director)</option>
<option value="A. Dlamini">A. Dlamini (Coordinator)</option>
<option value="N. Ndlovu">N. Ndlovu (Officer)</option>
<option value="S. Pillay">S. Pillay (Assoc)</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Target Flight route</label>
<select @bind="reusedTargetRoute" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] font-semibold outline-none focus:border-[#0066b3]">
<option value="JNB to CPT (02 Jun)">Johannesburg to Cape Town</option>
<option value="JNB to PLZ (15 Jun)">Johannesburg to Port Elizabeth</option>
<option value="JNB to DUR (22 Jun)">Johannesburg to Durban</option>
</select>
</div>
<div class="p-3 bg-[#effaf3] text-[#22a06b] border border-emerald-100 rounded-lg text-[11px]">
This credit will be applied directly to the booking fee, reducing the new trip's cash settlement requirement. No extra cash outflows.
</div>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => showReuseModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Abort</button>
<button @onclick="@(() => ConfirmReuse())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer shadow-xs">Apply Credit Now</button>
</div>
</div>
</div>
}
@* 4. Modify Active Booking Modal *@
@if (showModifyModal && selectedBooking != null)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-[#003b71]">Amend Corporate Booking</h3>
<button @onclick="@(() => showModifyModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<div class="p-3 bg-slate-50 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#1f2a3a]">@selectedBooking.Title</div>
<div class="text-[11px] text-[#8a96a8] mt-0.5">Current: @selectedBooking.Subtitle</div>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">New Route / Details</label>
<input type="text" @bind="modifyNewDetails" placeholder="e.g. Confirmed, 04 Jun, ref AB23PQ" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<p class="text-[11px] text-[#8a96a8]">Note: Modifications are verified against the original GDS fare category rules. No-fee changes are guaranteed for executive tiers.</p>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => showModifyModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Abort</button>
<button @onclick="@(() => ConfirmModify())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer shadow-xs">Save Amendments</button>
</div>
</div>
</div>
}
@* 5. Cancel Booking Confirmation Modal *@
@if (showCancelConfirmModal && selectedBooking != null)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-[#dc2626]">Cancel GDS Booking</h3>
<button @onclick="@(() => showCancelConfirmModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<p>Are you sure you want to cancel this booking and trigger the refund sequence?</p>
<div class="p-3 bg-red-50 border border-red-100 rounded-lg text-xs">
<strong class="text-slate-800">@selectedBooking.Title</strong>
<div class="text-[11px] text-[#8a96a8] mt-0.5">@selectedBooking.Subtitle</div>
<div class="text-sm font-bold text-red-600 mt-1.5">Value: R @selectedBooking.Price.ToString("N0")</div>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Reason for Cancellation</label>
<select @bind="cancellationReason" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]">
<option value="Meeting rescheduled by external stakeholders">Meeting rescheduled by external stakeholders</option>
<option value="Health / Personal emergency">Health / Personal emergency</option>
<option value="Project scope reduction / Trip redundant">Project scope reduction / Trip redundant</option>
<option value="Alternative transport selected">Alternative transport selected</option>
</select>
</div>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => showCancelConfirmModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Keep Booking</button>
<button @onclick="@(() => ConfirmCancel())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-red-600 hover:bg-red-700 cursor-pointer shadow-xs">Cancel &amp; Refund</button>
</div>
</div>
</div>
}
@* 6. Custom New Trip Booking Modal *@
@if (showNewTripModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-[#003b71]">Create Manual Corporate Booking</h3>
<button @onclick="@(() => showNewTripModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Booking Category</label>
<select @bind="customCategory" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]">
<option value="Flight">Corporate Flight</option>
<option value="Hotel">Hotel Accommodation</option>
<option value="Car Rental">Car / Shuttle Rental</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Title &amp; Reference (e.g. CemAir Flight 121)</label>
<input type="text" @bind="customTitle" placeholder="e.g. CemAir Flight 121, S. Pillay" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Subtitle / Details (e.g. Confirmed, 10 Jun)</label>
<input type="text" @bind="customSubtitle" placeholder="e.g. Confirmed, 10 to 12 Jun, ref CE3341" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Price (ZAR)</label>
<input type="number" @bind="customPrice" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Lodge Authorized</label>
<select class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-[#effaf3] text-[#22a06b] font-bold" disabled>
<option>Pre-authorized</option>
</select>
</div>
</div>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => showNewTripModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Cancel</button>
<button @onclick="@(() => ConfirmNewTrip())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer shadow-xs">Issue Booking</button>
</div>
</div>
</div>
}
@* 7. Travel Approval Queue Detail Modal *@
@if (showApprovalModal && selectedApproval != null)
{
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
<h3 class="text-base font-bold text-[#003b71]">Review Travel Authorization</h3>
<button @onclick="@(() => showApprovalModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-4 text-xs text-[#5a6577]">
<div class="flex items-center gap-3 p-3 bg-[#f4f7fb] rounded-xl border border-[#e4eaf2]">
<div class="w-10 h-10 rounded-full flex items-center justify-center text-xs text-white font-bold bg-gradient-to-br from-[#0066b3] to-[#00a3a1]">@selectedApproval.Initials</div>
<div>
<strong class="text-sm font-bold text-[#1f2a3a]">@selectedApproval.Name</strong>
<div class="text-[11px] text-[#8a96a8]">@selectedApproval.Role · @selectedApproval.ApproverRole Check</div>
</div>
</div>
<div class="space-y-2 border border-[#e4eaf2] p-3.5 rounded-lg">
<div class="flex justify-between border-b border-[#f4f7fb] pb-1.5">
<span class="text-[#8a96a8]">Trip Route:</span>
<span class="font-bold text-[#1f2a3a]">@selectedApproval.TripDetails</span>
</div>
<div class="flex justify-between border-b border-[#f4f7fb] pb-1.5">
<span class="text-[#8a96a8]">Cost Quote:</span>
<span class="font-bold text-red-600">@selectedApproval.Cost</span>
</div>
<div class="flex justify-between">
<span class="text-[#8a96a8]">Policy Match:</span>
<span class="font-bold text-[#22a06b]">✓ National Treasury compliant</span>
</div>
</div>
<div>
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Approval Comments / Decline Reason</label>
<textarea @bind="approvalComments" placeholder="Enter optional comments here..." rows="2" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]"></textarea>
</div>
</div>
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
<button @onclick="@(() => ActionTripRequest(selectedApproval, "Declined"))" class="px-3 py-2 rounded-lg text-xs font-bold text-[#dc2626] bg-red-50 hover:bg-red-100 cursor-pointer">Decline Request</button>
<button @onclick="@(() => ActionTripRequest(selectedApproval, "Approved"))" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#22a06b] hover:bg-[#1b8054] cursor-pointer shadow-xs">Approve &amp; Ticket</button>
</div>
</div>
</div>
}
@code {
// --- Tabs State ---
private string activeTab = "flights";
private void SetTab(string tab)
{
activeTab = tab;
// Automatically ping GDS when tab changes for liveness
IsSearching = true;
_ = Task.Delay(400).ContinueWith(_ => { IsSearching = false; InvokeAsync(StateHasChanged); });
}
// --- Master Card & Financials State ---
private decimal LodgeCardBalance { get; set; } = 312440M;
// --- Telemetry Alerts / Success logs ---
private string AlertFeedback { get; set; } = string.Empty;
// --- My Bookings view filter ---
private bool ShowMyBookingsOnly { get; set; } = false;
private void ToggleMyBookingsFilter()
{
ShowMyBookingsOnly = !ShowMyBookingsOnly;
if (ShowMyBookingsOnly)
{
AlertFeedback = "Filtered queue for your corporate profile.";
}
else
{
AlertFeedback = "Displaying all corporate passenger GDS itineraries.";
}
}
// --- Form Bindings ---
private string FlightFrom { get; set; } = "Johannesburg (JNB)";
private string FlightTo { get; set; } = "Cape Town (CPT)";
private DateTime FlightDate { get; set; } = new DateTime(2026, 6, 2);
private string FlightCabin { get; set; } = "Economy (Policy)";
private string HotelLocation { get; set; } = "Sandton, Johannesburg";
private DateTime HotelCheckIn { get; set; } = new DateTime(2026, 6, 2);
private DateTime HotelCheckOut { get; set; } = new DateTime(2026, 6, 5);
private string HotelGuests { get; set; } = "1 Guest";
private string CarLocation { get; set; } = "O.R. Tambo International Airport (JNB)";
private DateTime CarPickUp { get; set; } = new DateTime(2026, 6, 2);
private DateTime CarReturn { get; set; } = new DateTime(2026, 6, 5);
private string CarClass { get; set; } = "Compact / Hatchback";
private bool IsSearching { get; set; } = false;
// --- Search Logic ---
private void TriggerSearch()
{
IsSearching = true;
// Mimic instant GDS query
_ = Task.Delay(800).ContinueWith(_ =>
{
IsSearching = false;
AlertFeedback = $"Refreshed corporate GDS query for your choice of {activeTab.ToUpper()} with 3 preferred supplier nodes.";
InvokeAsync(StateHasChanged);
});
}
// --- Models ---
public class SearchResultItem
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public decimal Price { get; set; }
public string BadgeText { get; set; } = string.Empty;
public string BadgeClass { get; set; } = string.Empty;
public string HighlightText { get; set; } = string.Empty;
public string TabType { get; set; } = string.Empty; // "flights", "hotels", "cars"
}
public class TripRequest
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Initials { get; set; } = "";
public string Name { get; set; } = "";
public string Role { get; set; } = "";
public string TripDetails { get; set; } = "";
public string Cost { get; set; } = "";
public string ApproverRole { get; set; } = "";
public string Status { get; set; } = "Pending";
}
public class BookingItem
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; } = "";
public string Subtitle { get; set; } = "";
public decimal Price { get; set; }
public string Passenger { get; set; } = "";
public bool IsCancelled { get; set; }
}
public class UnusedTicketItem
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string TicketDetails { get; set; } = "";
public string ExpiryDate { get; set; } = "";
public string Passenger { get; set; } = "";
public decimal Price { get; set; }
public bool IsRefunded { get; set; }
public bool IsReused { get; set; }
}
public class DeliveryNotification
{
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public string Icon { get; set; } = ""; // "sms", "email", "warning"
public string TypeClass { get; set; } = "";
public string BgClass { get; set; } = "";
public string Timestamp { get; set; } = "";
}
// --- Database Collections ---
private List<SearchResultItem> allSearchResults = new()
{
// Flights
new() { Id = "F1", Title = "SAA Flight SA 311", Subtitle = "JNB 06:00 → CPT 08:10", Price = 2890M, BadgeText = "Best Value", BadgeClass = "bg-[#e3f6ed] text-[#22a06b]", HighlightText = "Negotiated fare · -12%", TabType = "flights" },
new() { Id = "F2", Title = "FlySafair Flight FA 121", Subtitle = "JNB 07:15 → CPT 09:20", Price = 1985M, BadgeText = "LCC", BadgeClass = "bg-[#e2eefb] text-[#0066b3]", HighlightText = "No checked bag · +R200 ext", TabType = "flights" },
new() { Id = "F3", Title = "British Airways Flight BA 6433", Subtitle = "JNB 08:00 → CPT 10:05", Price = 3410M, BadgeText = "Full Service", BadgeClass = "bg-[#e8f4f4] text-[#00a3a1]", HighlightText = "23kg bag · lounge access", TabType = "flights" },
// Hotels
new() { Id = "H1", Title = "Premier Hotel OR Tambo", Subtitle = "Executive Room · Sandton District", Price = 1850M, BadgeText = "Preferred", BadgeClass = "bg-[#e3f6ed] text-[#22a06b]", HighlightText = "Corporate discount · -15%", TabType = "hotels" },
new() { Id = "H2", Title = "The Westin Cape Town", Subtitle = "Deluxe Suite · Convention Centre", Price = 3950M, BadgeText = "Exec Choice", BadgeClass = "bg-[#e2eefb] text-[#0066b3]", HighlightText = "Spa and lounge access included", TabType = "hotels" },
new() { Id = "H3", Title = "Southern Sun Waterfront", Subtitle = "Standard Executive Double · CBD", Price = 2410M, BadgeText = "Standard", BadgeClass = "bg-[#e8f4f4] text-[#00a3a1]", HighlightText = "Free Wi-Fi · Buffet Breakfast", TabType = "hotels" },
// Cars
new() { Id = "C1", Title = "Avis Car Rental - VW Polo", Subtitle = "Manual hatchback · Airport Terminal", Price = 450M, BadgeText = "Best Value", BadgeClass = "bg-[#e3f6ed] text-[#22a06b]", HighlightText = "Unlimited mileage · Air-Con", TabType = "cars" },
new() { Id = "C2", Title = "Hertz - Toyota RAV4", Subtitle = "Automatic Midsize SUV · City Depot", Price = 950M, BadgeText = "SUV Tier", BadgeClass = "bg-[#e2eefb] text-[#0066b3]", HighlightText = "Premium Collision Cover", TabType = "cars" },
new() { Id = "C3", Title = "Europcar - VW Caravelle", Subtitle = "8-Seater Executive Passenger Van", Price = 1890M, BadgeText = "Shuttle Class", BadgeClass = "bg-[#e8f4f4] text-[#00a3a1]", HighlightText = "Corporate driver package", TabType = "cars" }
};
private List<SearchResultItem> GetActiveSearchResults()
{
return allSearchResults.Where(r => r.TabType == activeTab).ToList();
}
private List<TripRequest> tripRequests = new()
{
new() { Initials = "JM", Name = "J. Mokoena", Role = "Executive", TripDetails = "JNB to CPT, 02 Jun", Cost = "R 3,240", ApproverRole = "CFO", Status = "Pending" },
new() { Initials = "SP", Name = "S. Pillay", Role = "Coordinator", TripDetails = "Avis car, 3 days", Cost = "R 1,420", ApproverRole = "Line Manager", Status = "Pending" },
new() { Initials = "TM", Name = "T. Maluleke", Role = "Director", TripDetails = "JNB to LHR, 14 Jun", Cost = "R 18,940", ApproverRole = "CEO", Status = "Pending" }
};
private List<BookingItem> activeBookings = new()
{
new() { Title = "SA 311, J. Mokoena", Subtitle = "Confirmed, 02 Jun, ref AB23PQ", Price = 2890M, Passenger = "J. Mokoena", IsCancelled = false },
new() { Title = "Premier Hotel, N. Ndlovu", Subtitle = "Confirmed, 03 to 05 Jun, ref HT7732", Price = 3700M, Passenger = "N. Ndlovu", IsCancelled = false },
new() { Title = "Avis car, S. Pillay", Subtitle = "Pending approval, 04 to 06 Jun", Price = 1350M, Passenger = "S. Pillay", IsCancelled = false }
};
private List<BookingItem> GetFilteredBookings()
{
if (ShowMyBookingsOnly)
{
return activeBookings.Where(b => b.Passenger == "T. Maluleke").ToList();
}
return activeBookings;
}
private List<UnusedTicketItem> unusedTickets = new()
{
new() { TicketDetails = "SA 311", Passenger = "J. Mokoena", Price = 2890M, ExpiryDate = "21 Jul 2026", IsRefunded = false, IsReused = false },
new() { TicketDetails = "BA 6433", Passenger = "T. Maluleke", Price = 3410M, ExpiryDate = "05 Sep 2026", IsRefunded = false, IsReused = false },
new() { TicketDetails = "FA 121", Passenger = "N. Ndlovu", Price = 1985M, ExpiryDate = "18 Aug 2026", IsRefunded = false, IsReused = false }
};
private List<DeliveryNotification> deliveryLogs = new()
{
new() { Title = "SMS delivered", Content = "e-Ticket sent to J. Mokoena · +27 82 ●●● 4421", Icon = "sms", TypeClass = "border-[#22a06b] text-[#22a06b]", BgClass = "bg-[#effaf3]", Timestamp = "09:14 UTC" },
new() { Title = "Email voucher sent", Content = "Premier Hotel OR Tambo voucher to n.ndlovu@@saqa.org.za", Icon = "email", TypeClass = "border-[#0066b3] text-[#0066b3]", BgClass = "bg-[#f3f8fd]", Timestamp = "08:55 UTC" },
new() { Title = "Baggage policy alert", Content = "BA reduced free baggage allowance to 1×23kg on JNB-LHR route", Icon = "warning", TypeClass = "border-[#f4a300] text-[#f4a300]", BgClass = "bg-[#fff9ec]", Timestamp = "07:30 UTC" }
};
// --- Math Helpers ---
private decimal GetActiveBookingsValue()
{
return activeBookings.Where(b => !b.IsCancelled).Sum(b => b.Price);
}
private decimal GetUnusedTicketsValue()
{
return unusedTickets.Where(t => !t.IsRefunded && !t.IsReused).Sum(t => t.Price);
}
// --- Interactive Action Handlers ---
// Modal Control properties
private bool showBookingModal = false;
private bool showRefundModal = false;
private bool showReuseModal = false;
private bool showModifyModal = false;
private bool showCancelConfirmModal = false;
private bool showNewTripModal = false;
private bool showApprovalModal = false;
private SearchResultItem? selectedSearchResult;
private UnusedTicketItem? selectedTicket;
private BookingItem? selectedBooking;
private TripRequest? selectedApproval;
// Field bindings for modals
private string selectedTraveller = "J. Mokoena";
private string selectedCostCentre = "Executive Administration";
private string bookingSpecialNotes = string.Empty;
private string reusedTargetPassenger = "T. Maluleke";
private string reusedTargetRoute = "JNB to CPT (02 Jun)";
private string modifyNewDetails = string.Empty;
private string cancellationReason = "Meeting rescheduled by external stakeholders";
private string customCategory = "Flight";
private string customTitle = string.Empty;
private string customSubtitle = string.Empty;
private decimal customPrice = 2500M;
private string approvalComments = string.Empty;
// Trigger Refund Workflow
private void OpenRefundModal(UnusedTicketItem ticket)
{
selectedTicket = ticket;
showRefundModal = true;
}
private void ConfirmRefund()
{
if (selectedTicket != null)
{
selectedTicket.IsRefunded = true;
LodgeCardBalance += selectedTicket.Price;
// Log action
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = $"Refund Approved: {selectedTicket.TicketDetails}",
Content = $"Refund of R {selectedTicket.Price.ToString("N0")} credited back to Lodge Card for passenger {selectedTicket.Passenger}",
Icon = "email",
TypeClass = "border-[#22a06b] text-[#22a06b]",
BgClass = "bg-[#effaf3]",
Timestamp = "Just Now"
});
AlertFeedback = $"Refund sequence completed for {selectedTicket.TicketDetails}. R {selectedTicket.Price.ToString("N0")} returned to Lodge balance.";
showRefundModal = false;
}
}
// Trigger Reuse Workflow
private void OpenReuseModal(UnusedTicketItem ticket)
{
selectedTicket = ticket;
reusedTargetPassenger = ticket.Passenger;
showReuseModal = true;
}
private void ConfirmReuse()
{
if (selectedTicket != null)
{
selectedTicket.IsReused = true;
// Add custom booking representing reused ticket
var newBooking = new BookingItem
{
Title = $"{selectedTicket.TicketDetails} Reused - {reusedTargetPassenger}",
Subtitle = $"Ticket Credit Applied for {reusedTargetRoute}",
Price = 0, // No extra cash spent
Passenger = reusedTargetPassenger,
IsCancelled = false
};
activeBookings.Add(newBooking);
// Log action
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = $"Credit Applied: {selectedTicket.TicketDetails}",
Content = $"Transferred unused ticket worth R {selectedTicket.Price.ToString("N0")} to passenger {reusedTargetPassenger}",
Icon = "sms",
TypeClass = "border-[#0066b3] text-[#0066b3]",
BgClass = "bg-[#f3f8fd]",
Timestamp = "Just Now"
});
AlertFeedback = $"Successfully reassigned ticket {selectedTicket.TicketDetails} to passenger {reusedTargetPassenger}!";
showReuseModal = false;
}
}
// Trigger Booking Search Result Workflow
private void OpenBookingModal(SearchResultItem result)
{
selectedSearchResult = result;
bookingSpecialNotes = string.Empty;
showBookingModal = true;
}
private void ConfirmBooking()
{
if (selectedSearchResult != null)
{
LodgeCardBalance -= selectedSearchResult.Price;
// Create booking
var newBooking = new BookingItem
{
Title = $"{selectedSearchResult.Title}, {selectedTraveller}",
Subtitle = $"Confirmed, {DateTime.Now.AddDays(7):dd MMM 2026}, ref {GenerateRandomRef()}",
Price = selectedSearchResult.Price,
Passenger = selectedTraveller,
IsCancelled = false
};
activeBookings.Add(newBooking);
// Add SMS delivery notification
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = "e-Ticket Delivered",
Content = $"GDS ticket sent to {selectedTraveller} for {selectedSearchResult.Title}. Reference code attached.",
Icon = "sms",
TypeClass = "border-[#22a06b] text-[#22a06b]",
BgClass = "bg-[#effaf3]",
Timestamp = "Just Now"
});
AlertFeedback = $"Successfully issued GDS ticket for {selectedTraveller}. Lodged R {selectedSearchResult.Price.ToString("N0")} to {selectedCostCentre} cost center.";
showBookingModal = false;
}
}
// Modify Booking
private void OpenModifyModal(BookingItem booking)
{
selectedBooking = booking;
modifyNewDetails = booking.Subtitle;
showModifyModal = true;
}
private void ConfirmModify()
{
if (selectedBooking != null)
{
selectedBooking.Subtitle = modifyNewDetails;
// Log action
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = "Itinerary Amended",
Content = $"Changed booking parameters for {selectedBooking.Title} to '{modifyNewDetails}' via Amadeus GDS Node.",
Icon = "warning",
TypeClass = "border-[#f4a300] text-[#f4a300]",
BgClass = "bg-[#fff9ec]",
Timestamp = "Just Now"
});
AlertFeedback = $"Successfully updated itinerary details for booking.";
showModifyModal = false;
}
}
// Cancel Booking
private void OpenCancelConfirmModal(BookingItem booking)
{
selectedBooking = booking;
cancellationReason = "Meeting rescheduled by external stakeholders";
showCancelConfirmModal = true;
}
private void ConfirmCancel()
{
if (selectedBooking != null)
{
selectedBooking.IsCancelled = true;
LodgeCardBalance += selectedBooking.Price; // credit back
// Log action
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = "Booking Cancelled",
Content = $"Cancelled {selectedBooking.Title}. Reason: {cancellationReason}. Net refund of R {selectedBooking.Price.ToString("N0")} queued.",
Icon = "warning",
TypeClass = "border-red-500 text-red-600",
BgClass = "bg-red-50",
Timestamp = "Just Now"
});
AlertFeedback = $"Booking for {selectedBooking.Title} has been cancelled successfully. Refund dispatched.";
showCancelConfirmModal = false;
}
}
// Custom Trip Creator
private void OpenNewTripModal()
{
customTitle = string.Empty;
customSubtitle = string.Empty;
customPrice = 2500;
showNewTripModal = true;
}
private void ConfirmNewTrip()
{
if (string.IsNullOrEmpty(customTitle))
{
AlertFeedback = "Error: Booking title cannot be empty.";
return;
}
LodgeCardBalance -= customPrice;
var newBooking = new BookingItem
{
Title = customTitle,
Subtitle = string.IsNullOrEmpty(customSubtitle) ? $"Confirmed on {DateTime.Now:dd MMM 2026}" : customSubtitle,
Price = customPrice,
Passenger = "T. Maluleke",
IsCancelled = false
};
activeBookings.Insert(0, newBooking);
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = $"{customCategory} Booked",
Content = $"Manually logged {customTitle} at cost of R {customPrice:N0}. System updated.",
Icon = "email",
TypeClass = "border-[#0066b3] text-[#0066b3]",
BgClass = "bg-[#f3f8fd]",
Timestamp = "Just Now"
});
AlertFeedback = $"Manual {customCategory} booking logged and charged R {customPrice:N0} to Lodge Card.";
showNewTripModal = false;
}
// Review Approvals
private void OpenApprovalModal(TripRequest trip)
{
selectedApproval = trip;
approvalComments = string.Empty;
showApprovalModal = true;
}
private void ActionTripRequest(TripRequest trip, string newStatus)
{
trip.Status = newStatus;
if (newStatus == "Approved")
{
// deduct balance & issue e-Ticket
decimal price = decimal.Parse(trip.Cost.Replace("R", "").Replace(",", "").Trim());
LodgeCardBalance -= price;
var newBooking = new BookingItem
{
Title = $"{trip.TripDetails}, {trip.Name}",
Subtitle = $"Manager Approved · Ref {GenerateRandomRef()}",
Price = price,
Passenger = trip.Name,
IsCancelled = false
};
activeBookings.Insert(0, newBooking);
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = "Approval Confirmed",
Content = $"e-Ticket successfully authorized by CFO for {trip.Name} - {trip.TripDetails}",
Icon = "sms",
TypeClass = "border-[#22a06b] text-[#22a06b]",
BgClass = "bg-[#effaf3]",
Timestamp = "Just Now"
});
AlertFeedback = $"Trip approved for {trip.Name}! Ticket issued under corporate quota.";
}
else
{
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = "Trip Request Declined",
Content = $"Authorization declined for {trip.Name} - {trip.TripDetails}. Comments: '{approvalComments}'",
Icon = "warning",
TypeClass = "border-red-500 text-red-600",
BgClass = "bg-red-50",
Timestamp = "Just Now"
});
AlertFeedback = $"Trip authorization request declined for {trip.Name}.";
}
showApprovalModal = false;
}
// Telemetry ping
private void TriggerDeliveryTest()
{
deliveryLogs.Insert(0, new DeliveryNotification
{
Title = "GDS Terminal Pinged",
Content = "Node connectivity test completed successfully. Amadeus: 14ms · Sabre: 18ms · Travelport: 11ms",
Icon = "sms",
TypeClass = "border-[#0066b3] text-[#0066b3]",
BgClass = "bg-[#f3f8fd]",
Timestamp = "Just Now"
});
AlertFeedback = "Direct node ping successful. 3/3 active global distribution systems reporting online.";
}
// Helper
private string GenerateRandomRef()
{
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
return new string(Enumerable.Repeat(chars, 6).Select(s => s[random.Next(s.Length)]).ToArray());
}
}