1346 lines
80 KiB
Plaintext
1346 lines
80 KiB
Plaintext
@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 & Sabre · e-ticketing & 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 & 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 & 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 & Notifications</h3>
|
||
<p class="text-[12px] text-[#8a96a8] mt-0.5">E-tickets, SMS alerts & 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 & 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 & Operations Support">Finance & 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 & 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 & 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 & 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 & 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());
|
||
}
|
||
}
|