From 192772f0c2f546ee78629c0e552a0df83d029bba Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 9 Jul 2026 21:41:26 +0200 Subject: [PATCH] Stable main pages --- .../Components/Layout/NavMenu.razor | 11 +- Tmt.SaqaTravel/Components/Pages/Booking.razor | 1340 +++++++++++++- .../Components/Pages/Compliance.razor | 566 +++++- Tmt.SaqaTravel/Components/Pages/Counter.razor | 19 - .../Components/Pages/Financial.razor | 383 +++- Tmt.SaqaTravel/Components/Pages/Forex.razor | 556 +++++- Tmt.SaqaTravel/Components/Pages/GdsSync.razor | 598 +++++- Tmt.SaqaTravel/Components/Pages/Home.razor | 264 ++- Tmt.SaqaTravel/Components/Pages/Mobile.razor | 686 ++++++- .../Components/Pages/SlaReports.razor | 1222 +++++++++++++ .../Components/Pages/TripReports.razor | 183 +- Tmt.SaqaTravel/Components/Pages/Vas.razor | 763 +++++++- Tmt.SaqaTravel/Components/Pages/Visa.razor | 1089 ++++++++++- Tmt.SaqaTravel/Components/Pages/Weather.razor | 66 - Tmt.SaqaTravel/wwwroot/app.min.css | 1615 ++++++++++++++++- 15 files changed, 9171 insertions(+), 190 deletions(-) delete mode 100644 Tmt.SaqaTravel/Components/Pages/Counter.razor create mode 100644 Tmt.SaqaTravel/Components/Pages/SlaReports.razor delete mode 100644 Tmt.SaqaTravel/Components/Pages/Weather.razor diff --git a/Tmt.SaqaTravel/Components/Layout/NavMenu.razor b/Tmt.SaqaTravel/Components/Layout/NavMenu.razor index 9a958a4..8827727 100644 --- a/Tmt.SaqaTravel/Components/Layout/NavMenu.razor +++ b/Tmt.SaqaTravel/Components/Layout/NavMenu.razor @@ -56,6 +56,15 @@ 3 + + + + + + + Trip Reports + + @@ -118,7 +127,7 @@
Integrations
- + diff --git a/Tmt.SaqaTravel/Components/Pages/Booking.razor b/Tmt.SaqaTravel/Components/Pages/Booking.razor index 4962a30..bd91b5b 100644 --- a/Tmt.SaqaTravel/Components/Pages/Booking.razor +++ b/Tmt.SaqaTravel/Components/Pages/Booking.razor @@ -1,7 +1,1345 @@ @page "/booking" +@using System +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@rendermode InteractiveServer + +
+ + @* Page Head *@ +
+
+

Online GDS Booking Console

+

GDS-integrated · Amadeus, Travelport & Sabre · e-ticketing & vouchers

+
+
+ + +
+
+ + @* Notification Feedback Banner *@ + @if (!string.IsNullOrEmpty(AlertFeedback)) + { +
+
+ + + + + @AlertFeedback +
+ +
+ } + + @* Financial & Booking KPI Dashboard *@ +
+
+
+ + + + +
+
+ Lodge Card Balance + R @LodgeCardBalance.ToString("N0") +
+
+ +
+
+ + + + +
+
+ Active Bookings Value + R @GetActiveBookingsValue().ToString("N0") +
+
+ +
+
+ + + + +
+
+ Unused e-Ticket Credits + R @GetUnusedTicketsValue().ToString("N0") +
+
+ +
+
+ + + + +
+
+ Approval Backlog + @tripRequests.Count(t => t.Status == "Pending") Awaiting +
+
+
+ + @* Search Card *@ +
+ @* Tabs *@ +
+ + + +
+ + @* Form Row *@ +
+ @if (activeTab == "flights") + { +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ } + else if (activeTab == "hotels") + { +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ } + else + { +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ } + +
+
+ + @* Simulated GDS Node Loader / Spinner *@ + @if (IsSearching) + { +
+
+ Interrogating GDS Telemetry Nodes... +

Fetching pre-negotiated corporate rates from Amadeus, Sabre and Travelport databases

+
+ } + else + { + @* Grid 3: Results *@ +
+ @foreach (var result in GetActiveSearchResults()) + { +
+
+
+
+

@result.Title

+

@result.Subtitle

+
+ @result.BadgeText +
+
+
+
+
R @result.Price.ToString("N0")
+ @result.HighlightText +
+ +
+
+ } +
+ } + + @* Grid 2: Delivery & Unused e-Tickets *@ +
+
+
+
+
+

Unused e-Tickets

+

Tracked & refund-eligible corporate flight stock

+
+ @unusedTickets.Count(t => !t.IsRefunded && !t.IsReused) open +
+ +
+ @foreach (var ticket in unusedTickets) + { +
+
+
+ @ticket.TicketDetails for @ticket.Passenger + @if (ticket.IsRefunded) + { + Refunded + } + else if (ticket.IsReused) + { + Reused + } +
+
Expires @ticket.ExpiryDate · Face Value: R @ticket.Price.ToString("N0")
+
+
+ @if (!ticket.IsRefunded && !ticket.IsReused) + { + + + } +
+
+ } +
+
+ +
+ + + + Refunds are directly re-credited to SAQA's master Lodge Card balance instantly upon approval. +
+
+ +
+
+
+
+

Delivery & Notifications

+

E-tickets, SMS alerts & hotel vouchers

+
+ Active Feed +
+ +
+ @foreach (var log in deliveryLogs) + { +
+
+ @if (log.Icon == "sms") + { + + + + + } + else if (log.Icon == "email") + { + + + + + } + else + { + + + + + } +
+
+

@log.Title

+

@log.Content

+ @log.Timestamp +
+
+ } +
+
+ + +
+
+ + @* Grid 2: Approval & Cancellations *@ +
+ @* Approvals Table *@ +
+
+
+
+

Travel Approval Queue

+

Manager sign-off required before GDS auto-ticketing

+
+ @tripRequests.Count(t => t.Status == "Pending") awaiting +
+ +
+ + + + + + + + + + + + @foreach (var trip in tripRequests) + { + + + + + + + + } + +
TravellerTripCostApproverAction
+
+
@trip.Initials
+
+ @trip.Name + @trip.Role +
+
+
@trip.TripDetails@trip.Cost + @trip.ApproverRole + + @if (trip.Status == "Pending") + { + + } + else if (trip.Status == "Approved") + { + Approved + } + else + { + Declined + } +
+
+
+
+ + @* Manage Bookings *@ +
+
+
+
+

Active Corporate Bookings

+

Amend, cancel & track refunds

+
+ Live Feed +
+ +
+ @foreach (var booking in GetFilteredBookings()) + { +
+
+
+ @booking.Title + @if (booking.IsCancelled) + { + Cancelled + } +
+
@booking.Subtitle · R @booking.Price.ToString("N0")
+
+
+ @if (booking.IsCancelled) + { + Refund Queued + } + else + { + + + } +
+
+ } +
+
+ +
+
+ +
+
+

Automated voucher

+

Vouchers are auto-sent to hotel, traveller and back-office instantly on finalization.

+
+
+
+
+ +
+ +@* ===================== MODAL DIALOGS ===================== *@ + +@* 1. Booking Confirmation Modal *@ +@if (showBookingModal && selectedSearchResult != null) +{ +
+
+
+
+ GDS Transaction Gateway +

Confirm GDS Lodge Booking

+
+ +
+ +
+
+
Selected Offering
+
@selectedSearchResult.Title
+
@selectedSearchResult.Subtitle
+
+ Cost Center Rate: + R @selectedSearchResult.Price.ToString("N0") +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + Corporate Policy Verification + +

This travel option fits comfortably within SAQA National Treasury Instruction 03 guidelines and budget thresholds. Lodge card is pre-authorized.

+
+
+ +
+ + +
+
+
+} + +@* 2. Refund Confirmation Modal *@ +@if (showRefundModal && selectedTicket != null) +{ +
+
+
+

+ + Confirm e-Ticket Refund +

+ +
+ +
+

Are you sure you want to request an immediate GDS automated refund for this unused e-ticket?

+
+
@selectedTicket.TicketDetails
+
Traveller: @selectedTicket.Passenger · Expiry: @selectedTicket.ExpiryDate
+
Refund Amount: R @selectedTicket.Price.ToString("N0")
+
+

Note: Airline administrative fees of R150 will be auto-settled via GDS system. The net refund will reflect on the lodge card instantly.

+
+ +
+ + +
+
+
+} + +@* 3. Reuse Credit Confirmation Modal *@ +@if (showReuseModal && selectedTicket != null) +{ +
+
+
+

+ + Reassign Unused ticket credit +

+ +
+ +
+

You are reassigning R @selectedTicket.Price.ToString("N0") of unused ticket credit from @selectedTicket.Passenger.

+ +
+ + +
+ +
+ + +
+ +
+ This credit will be applied directly to the booking fee, reducing the new trip's cash settlement requirement. No extra cash outflows. +
+
+ +
+ + +
+
+
+} + +@* 4. Modify Active Booking Modal *@ +@if (showModifyModal && selectedBooking != null) +{ +
+
+
+

Amend Corporate Booking

+ +
+ +
+
+
@selectedBooking.Title
+
Current: @selectedBooking.Subtitle
+
+ +
+ + +
+ +

Note: Modifications are verified against the original GDS fare category rules. No-fee changes are guaranteed for executive tiers.

+
+ +
+ + +
+
+
+} + +@* 5. Cancel Booking Confirmation Modal *@ +@if (showCancelConfirmModal && selectedBooking != null) +{ +
+
+
+

Cancel GDS Booking

+ +
+ +
+

Are you sure you want to cancel this booking and trigger the refund sequence?

+ +
+ @selectedBooking.Title +
@selectedBooking.Subtitle
+
Value: R @selectedBooking.Price.ToString("N0")
+
+ +
+ + +
+
+ +
+ + +
+
+
+} + +@* 6. Custom New Trip Booking Modal *@ +@if (showNewTripModal) +{ +
+
+
+

Create Manual Corporate Booking

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+} + +@* 7. Travel Approval Queue Detail Modal *@ +@if (showApprovalModal && selectedApproval != null) +{ +
+
+
+

Review Travel Authorization

+ +
+ +
+
+
@selectedApproval.Initials
+
+ @selectedApproval.Name +
@selectedApproval.Role · @selectedApproval.ApproverRole Check
+
+
+ +
+
+ Trip Route: + @selectedApproval.TripDetails +
+
+ Cost Quote: + @selectedApproval.Cost +
+
+ Policy Match: + ✓ National Treasury compliant +
+
+ +
+ + +
+
+ +
+ + +
+
+
+} -

Booking

@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 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 GetActiveSearchResults() + { + return allSearchResults.Where(r => r.TabType == activeTab).ToList(); + } + + private List 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 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 GetFilteredBookings() + { + if (ShowMyBookingsOnly) + { + return activeBookings.Where(b => b.Passenger == "T. Maluleke").ToList(); + } + return activeBookings; + } + + private List 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 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()); + } } diff --git a/Tmt.SaqaTravel/Components/Pages/Compliance.razor b/Tmt.SaqaTravel/Components/Pages/Compliance.razor index 9e4d770..6b66127 100644 --- a/Tmt.SaqaTravel/Components/Pages/Compliance.razor +++ b/Tmt.SaqaTravel/Components/Pages/Compliance.razor @@ -1,7 +1,571 @@ @page "/compliance" -

Compliance

+@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@rendermode InteractiveServer + +
+ + @* Page Header Area *@ +
+
+

Compliance & Policy Control

+

Audit travel approvals, verify National Treasury compliance, and review POPIA privacy safeguards

+
+
+ + +
+
+ + @* Alert / Feedback Notification Banner *@ + @if (!string.IsNullOrEmpty(AlertFeedback)) + { +
+ + + + + @AlertFeedback +
+ } + + @* Scorecard Grid *@ +
+
+
Overall Compliance Rating
+
@OverallCompliance%
+
+ National Treasury compliant +
+
+ +
+
POPIA Security Safeguards
+
Fully Certified
+
+ ISO 27001 active framework +
+
+ +
+
Bookings Outside Policy
+
@OutsidePolicyCount
+
+ Requires CFO/Exec approval +
+
+ +
+
Open Policy Anomalies
+
@AnomaliesCount
+
+ Requires instant review +
+
+
+ + @* Compliance split layout *@ +
+ + @* Card 1: Scorecard & metrics *@ +
+
+
+

SAQA Travel Compliance Scorecard

+

Live index measurements against SAQA policy rules and National Treasury Instruction No. 03

+
+ +
+ + @* Metric 1 *@ +
+
+ POPIA Privacy Compliance + 100% compliant +
+

AES 256 bit encryption on travel documents & traveller records at rest & in transit.

+
+
+
+
+ + @* Metric 2 *@ +
+
+ Cabin Class Limits Adherence + 96% adherence +
+

Economy limit enforced strictly. Exceptional premium cabins tracked & logged.

+
+
+
+
+ + @* Metric 3 *@ +
+
+ Hotel Rate Matrix Compliance + 78% compliance +
+

Ensures 3 quotes per booking. Handful of peak-season bookings bypassed limit matrix.

+
+
+
+
+ + @* Metric 4 *@ +
+
+ After-Hours Support SLA Response + 98% performance +
+

Emergency consultant call backs resolved in less than 15 minutes.

+
+
+
+
+ +
+
+ +
+ Sourced from GDS telemetry sync logs + Updated May 2026 +
+
+ + @* Card 2: Exceptions and Flags *@ +
+
+
+
+

Exceptions & Flagged Bookings

+

Real-time alerts requiring human justification & executive approval

+
+ Active Review +
+ +
+ @foreach (var exc in ExceptionsList) + { +
+
+ @if (exc.IsCritical) + { + + + + + } + else + { + + + + + } +
+ +
+
+

@exc.Title

+ @exc.Date +
+

@exc.Description

+

Justification: @exc.Justification

+ +
+ Owner: @exc.Traveller +
+ @if (exc.Status == "Pending") + { + + + } + else + { + + @exc.Status + + } +
+
+
+
+ } +
+
+ +
+

Flagged records automatically route alerts to executive dashboards upon booking completion.

+
+
+ +
+ + @* Compliance and Policy Library Grid Sections *@ +
+ + @* Security compliance cards - left side (lg:col-span-8) *@ +
+
+
+

Travel Data Privacy & Cyber Safeguards

+

POPIA compliance rules and robust data protection standards configured across platforms

+
+ ISO 27001 +
+ +
+ + @* Safeguard 1 *@ +
+
+ + + + +
+

AES 256 Data Encryption

+

All passports, reward accounts, and credit card frameworks are shielded securely using military-grade AES-256 standard protocols.

+
+ + @* Safeguard 2 *@ +
+
+ + + + +
+

POPIA Information Safeguards

+

Strict privacy protocols verified. Consent tracking is logged on every user profile activation, complying with South African privacy laws.

+
+ + @* Safeguard 3 *@ +
+
+ + + + +
+

7-Year Audit Trail Retained

+

Every booking, approval, modifier, or exception justification is timestamped and archived safely for external AGSA auditing purposes.

+
+ + @* Safeguard 4 *@ +
+
+ + + + +
+

ISO 27001 Certified Host

+

Data is hosted inside secure data centers. Monitored 24/7 with strict failover controls.

+
+ +
+
+ + @* Compliance reports generator - right side (lg:col-span-4) *@ +
+
+
+

Standard SLA Report Outputs

+

Export standard compliance matrices & cost containment breakdowns

+
+ +
+ + @* Report row 1 *@ +
+
+
National Treasury Matrix
+ Instruction 03 compliance logs +
+ +
+ + @* Report row 2 *@ +
+
+
POPIA Privacy Compliance Audit
+ Access log validation reports +
+ +
+ + @* Report row 3 *@ +
+
+
Executive Class Upgrade Log
+ Exemptions log with justifications +
+ +
+ + @* Report row 4 *@ +
+
+
Standard Monthly SLA Review
+ Performance vs target response matrix +
+ +
+ +
+
+ +
+

Report metrics cover SAQA travel spend data for the May 2026 reporting cycle.

+
+
+ +
+ +
+ +@* ===================== MODAL DIALOGS ===================== *@ + +@* 1. Policy Library Reference Modal *@ +@if (ShowPolicyLibraryModal) +{ +
+
+
+
+

SAQA Travel Policy Library

+

Authorized compliance guidelines and cap matrices

+
+ +
+ +
+ +
+ + + + + + + All bookings generated must comply with SAQA corporate governance policy and National Treasury frameworks. Exceptions require written CFO sign-off prior to ticketing. +
+ +
+
+
1. Airline Cabin Restrictions
+

All local, regional, and international travel bookings are restricted to Economy Cabin flights. Premium Economy or Business classes require verified medical justifications from Human Resources, approved by the Chief Financial Officer.

+
+ +
+
2. Accommodation Rate Matrices
+

Maximum nightly accommodation rates are aligned strictly with National Treasury Instruction No. 03 of 2016/17 (Cape Town maximum cap: R 1,900/night; Johannesburg: R 1,650/night). Minimum of 3 competitive quotes must be processed by the corporate booking tool prior to checkout.

+
+ +
+
3. Ground Transportation Caps
+

Car rental class is capped at Group B vehicles (e.g. automatic compact). Shuttles should be booked through verified pre-authorized local transfer vendors. Personal extension days differential billing is strictly monitored.

+
+ +
+
4. Foreign Exchange & Per Diems
+

Daily travel allowances (per diems) are aligned with SARS guidelines for overseas destinations. Expenses must be OCR-submitted and synced within 7 working days post-trip completion.

+
+
+ +
+ +
+ +
+
+
+} + @code { + // Exception class model representation + public class TravelException + { + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Date { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Justification { get; set; } = string.Empty; + public string Traveller { get; set; } = string.Empty; + public string Status { get; set; } = "Pending"; + public bool IsCritical { get; set; } + } + // Core States + private int OverallCompliance { get; set; } = 94; + private int OutsidePolicyCount { get; set; } = 7; + private int AnomaliesCount { get; set; } = 2; + private string AlertFeedback { get; set; } = string.Empty; + + // Modal control + private bool ShowPolicyLibraryModal { get; set; } = false; + + // Database arrays + private List ExceptionsList { get; set; } = new(); + + protected override void OnInitialized() + { + // Populate pre-loaded Exceptions & Policy alerts matching GDS parameters + ExceptionsList = new List + { + new TravelException + { + Id = 1, + Title = "Cabin Class Upgrade (BA JNB to LHR)", + Date = "14 May 2026", + Description = "T. Maluleke booked Premium Economy cabin on British Airways JNB-LHR route.", + Justification = "Medical reasons verified by SAQA HR and confirmed by executive CFO. Approved cabin class exception.", + Traveller = "T. Maluleke", + Status = "Approved", + IsCritical = false + }, + new TravelException + { + Id = 2, + Title = "Hotel Rate Bypassed Limits (Cape Town Engagement)", + Date = "22 May 2026", + Description = "J. Mokoena nightly rate processed at R 2,400 per night vs maximum NT Cap matrix of R 1,900.", + Justification = "Peak season business engagement at Waterfront. No alternative hotels available within standard NT limits on short notice.", + Traveller = "J. Mokoena", + Status = "Pending", + IsCritical = true + }, + new TravelException + { + Id = 3, + Title = "Leisure Extension differential check", + Date = "20 May 2026", + Description = "S. Pillay added 2 non-working personal days to domestic Durban travel itinerary.", + Justification = "Weekend stay extension. Airfare price differential & weekend hotel lodging billed personally to coordinator account.", + Traveller = "S. Pillay", + Status = "Approved", + IsCritical = false + } + }; + } + + // Interactive Exception Approvals + private void AuthorizeException(int id) + { + var exc = ExceptionsList.FirstOrDefault(e => e.Id == id); + if (exc != null) + { + exc.Status = "Approved"; + if (exc.IsCritical) + { + AnomaliesCount = System.Math.Max(0, AnomaliesCount - 1); + OverallCompliance = System.Math.Min(100, OverallCompliance + 2); + } + TriggerFeedback($"Exception justification authorized successfully for {exc.Traveller}. Compliance indices recalculated."); + } + } + + private void RejectException(int id) + { + var exc = ExceptionsList.FirstOrDefault(e => e.Id == id); + if (exc != null) + { + exc.Status = "Rejected"; + if (exc.IsCritical) + { + AnomaliesCount = System.Math.Max(0, AnomaliesCount - 1); + } + TriggerFeedback($"Exception justification rejected for {exc.Traveller}. Compliance violation flagged for PO settlement collection."); + } + } + + // Compliance Audit trigger + private async Task TriggerFullComplianceAudit() + { + TriggerFeedback("Analyzing Sync Logs... Querying 142 Active GDS PNR entries for policy compliance."); + await Task.Delay(1800); + + OverallCompliance = 96; + AnomaliesCount = ExceptionsList.Count(e => e.Status == "Pending"); + OutsidePolicyCount = ExceptionsList.Count; + + TriggerFeedback("Compliance Telemetry Scan Finished! 94% overall compliance rate recalculated across all May 2026 travel bookings. All flags archived."); + } + + // Report Downloads simulation + private void TriggerReportDownload(string reportName) + { + TriggerFeedback($"Compiling dataset... {reportName} download queued. System auto-generating document structure."); + } + + // Modal Helpers + private void OpenPolicyLibraryModal() + { + ShowPolicyLibraryModal = true; + } + + private void ClosePolicyLibraryModal() + { + ShowPolicyLibraryModal = false; + } + + // Feedback alert triggered toaster + private void TriggerFeedback(string message) + { + AlertFeedback = message; + _ = DismissAlertAfterDelay(); + } + + private async Task DismissAlertAfterDelay() + { + await Task.Delay(5000); + AlertFeedback = string.Empty; + StateHasChanged(); + } } diff --git a/Tmt.SaqaTravel/Components/Pages/Counter.razor b/Tmt.SaqaTravel/Components/Pages/Counter.razor deleted file mode 100644 index e7bb815..0000000 --- a/Tmt.SaqaTravel/Components/Pages/Counter.razor +++ /dev/null @@ -1,19 +0,0 @@ -@page "/counter" -@rendermode InteractiveServer - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() - { - currentCount++; - } -} \ No newline at end of file diff --git a/Tmt.SaqaTravel/Components/Pages/Financial.razor b/Tmt.SaqaTravel/Components/Pages/Financial.razor index 5360d76..cbbef2f 100644 --- a/Tmt.SaqaTravel/Components/Pages/Financial.razor +++ b/Tmt.SaqaTravel/Components/Pages/Financial.razor @@ -1,7 +1,386 @@ @page "/financial" -

Financial

+
+ + +
+
+

Financial Management

+

Bill-back consolidation, lodge card reconciliation, virtual credit cards

+
+
+ + +
+
+ + +
+ +
+
Outstanding Invoices
+
R @OutstandingInvoices.ToString("N0")
+
+ 14 invoices · ageing 0-30 days +
+
+ + +
+
Lodge Card Balance
+
R @LodgeCardBalance.ToString("N0")
+
+ Reconciled to 24 May +
+
+ + +
+
YTD Savings
+
@YtdSavingsText
+
+ ▲ 18.2% + vs. baseline +
+
+ + +
+
No-Show Fees
+
R @NoShowFees.ToString("N0")
+
+ ▼ R 2,300 + vs. last month +
+
+
+ + +
+ +
+
+
+

Spend vs. Savings

+

Cost containment performance

+
+ On track +
+ + +
+ + + + + + + + + + + + R2.5M + R2.0M + R1.5M + R1.0M + R500k + + + + + + + + + + + + + + + + + + + + + + + + + Dec + Jan + Feb + Mar + Apr + May + + +
+ +
+
+ Total Spend +
+
+ Savings Realised +
+
+
+ + +
+
+
+

Savings Breakdown

+

How May savings of R 412,580 were realised

+
+ +R 412k +
+ +
+ + + + + + + + + + + + @foreach (var item in SavingsData) + { + + + + + + + + } + + + + + + +
CategoryOpen RateNegotiatedSaved%
+ + @item.Category + + R @item.OpenRate.ToString("N0")R @item.NegotiatedRate.ToString("N0")R @item.Saved.ToString("N0")@item.Percentage.ToString("F1")%
Total saved this monthR @TotalSavedThisMonth.ToString("N0")16.8%
+
+
+
+ + +
+ +
+
+

Virtual Credit Card

+

Single-use, per-transaction

+
+ + +
+
SAQA · Single-use VCC
+
4716 •••• •••• 8842
+
+
+ Cardholder + SAQA Travel Desk +
+
+ Limit + R 3,500.00 +
+
+ Expires + 02/06/26 +
+
+
+ +
+ + +
+
+ + +
+
+

Spend by Cost Centre

+

Where the May spend went

+
+ +
+ @foreach (var cc in CostCentreData) + { +
+
+
@cc.Name
+
@cc.Travellers travellers
+
+ R @cc.Spend.ToString("N0") +
+ } +
+
+
+ + +
+
+
+

Consolidated Invoices

+

Bill-back from suppliers · linked to PO & Travel Authorisation

+
+ May 2026 +
+ +
+ + + + + + + + + + + + + @foreach (var invoice in InvoicesData) + { + + + + + + + + + } + +
Invoice #SupplierLinked POItemsTotalStatus
@invoice.InvoiceNum@invoice.Supplier@invoice.LinkedPo@invoice.ItemsR @invoice.Total.ToString("N0") + + @invoice.Status + +
+
+
+ +
@code { + // KPI Data Properties + private double OutstandingInvoices { get; set; } = 184920; + private double LodgeCardBalance { get; set; } = 312440; + private string YtdSavingsText { get; set; } = "R 1.94M"; + private double NoShowFees { get; set; } = 8140; -} + private double TotalSavedThisMonth { get; set; } = 412580; + + // Savings Breakdown Data Structure + private List SavingsData { get; set; } = new() + { + new SavingsItem("Air, domestic", 980400, 832340, 148060, 15.1, "bg-[#e2eefb] text-[#0066b3]"), + new SavingsItem("Air, international", 320200, 260920, 59280, 18.5, "bg-[#e2eefb] text-[#0066b3]"), + new SavingsItem("Accommodation", 845600, 722420, 123180, 14.6, "bg-[#f4f7fb] text-[#1f2a3a] border border-[#e4eaf2]"), + new SavingsItem("Car rental", 215800, 168180, 47620, 22.1, "bg-[#fff4dd] text-[#b06b00]"), + new SavingsItem("Shuttle and transfers", 92800, 75400, 17400, 18.8, "bg-[#f4f7fb] text-[#1f2a3a] border border-[#e4eaf2]"), + new SavingsItem("Service fees waived", 17040, 0, 17040, 100.0, "bg-[#e3f6ed] text-[#22a06b]") + }; + + // Cost Centre Data Structure + private List CostCentreData { get; set; } = new() + { + new CostCentreItem("Office of the CEO", 12, 684200), + new CostCentreItem("Qualifications and Standards", 38, 542400), + new CostCentreItem("Verifications", 26, 418950), + new CostCentreItem("Research and Stats", 18, 312800), + new CostCentreItem("Corporate Services", 14, 248400), + new CostCentreItem("International Liaison", 6, 640560) + }; + + // Consolidated Invoices Data Structure + private List InvoicesData { get; set; } = new() + { + new InvoiceItem("INV-2026-0541", "SAA", "PO-44219", "3 tickets", 12420, "Paid", "bg-[#e3f6ed] text-[#22a06b]"), + new InvoiceItem("INV-2026-0542", "Premier Hotels", "PO-44230", "5 nights", 9475, "Pending", "bg-[#fff4dd] text-[#b06b00]"), + new InvoiceItem("INV-2026-0543", "Avis", "PO-44245", "1 rental · 3 days", 1420, "Reconciled", "bg-[#e2eefb] text-[#0066b3]"), + new InvoiceItem("INV-2026-0544", "FlySafair", "PO-44250", "2 tickets", 3970, "Paid", "bg-[#e3f6ed] text-[#22a06b]"), + new InvoiceItem("INV-2026-0545", "British Airways", "PO-44261", "1 ticket (LHR)", 18940, "Disputed", "bg-[#fde5e8] text-[#dc2626]") + }; + + // Sub-classes for models + public class SavingsItem + { + public string Category { get; set; } + public double OpenRate { get; set; } + public double NegotiatedRate { get; set; } + public double Saved { get; set; } + public double Percentage { get; set; } + public string BadgeClass { get; set; } + + public SavingsItem(string category, double openRate, double negotiatedRate, double saved, double percentage, string badgeClass) + { + Category = category; + OpenRate = openRate; + NegotiatedRate = negotiatedRate; + Saved = saved; + Percentage = percentage; + BadgeClass = badgeClass; + } + } + + public class CostCentreItem + { + public string Name { get; set; } + public int Travellers { get; set; } + public double Spend { get; set; } + + public CostCentreItem(string name, int travellers, double spend) + { + Name = name; + Travellers = travellers; + Spend = spend; + } + } + + public class InvoiceItem + { + public string InvoiceNum { get; set; } + public string Supplier { get; set; } + public string LinkedPo { get; set; } + public string Items { get; set; } + public double Total { get; set; } + public string Status { get; set; } + public string StatusBadgeClass { get; set; } + + public InvoiceItem(string invoiceNum, string supplier, string linkedPo, string items, double total, string status, string statusBadgeClass) + { + InvoiceNum = invoiceNum; + Supplier = supplier; + LinkedPo = linkedPo; + Items = items; + Total = total; + Status = status; + StatusBadgeClass = statusBadgeClass; + } + } +} \ No newline at end of file diff --git a/Tmt.SaqaTravel/Components/Pages/Forex.razor b/Tmt.SaqaTravel/Components/Pages/Forex.razor index cde24f1..879b020 100644 --- a/Tmt.SaqaTravel/Components/Pages/Forex.razor +++ b/Tmt.SaqaTravel/Components/Pages/Forex.razor @@ -1,7 +1,559 @@ @page "/forex" -

Forex

+
+ + +
+
+

Foreign Exchange and Expense Management

+

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

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

Live Forex Rates

+

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

+
+ Bank rate +
+ +
+ + + + + + + + + + + + @foreach (var rate in ForexRates) + { + + + + + + + + } + +
CurrencyBuySell1 DayAction
+ @rate.Code + @rate.Name + R @rate.BuyRate.ToString("N2")R @rate.SellRate.ToString("N2") + @if (rate.DayChange >= 0) + { + ▲ @rate.DayChange.ToString("N2") + } + else + { + ▼ @Math.Abs(rate.DayChange).ToString("N2") + } + + +
+
+
+ + +
+

Quick ZAR Estimate Calculator

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

Travel Money Card

+

Multi-currency, reloadable, traveller assigned

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

Foreign Expense Capture and Reconciliation

+

Receipts auto converted at transaction rate

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

Order Foreign Currency

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

Load Travel Money Card

+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+} @code { + // ===================== STATE & DATA PROPERTIES ===================== + private int OpenForexOrdersCount { get; set; } = 4; + private int LoadedCardBalance { get; set; } = 3420; + private int ActiveCardsCount { get; set; } = 3; + private int SettledAmountZar { get; set; } = 184200; + private int UnreconciledReceiptsCount { get; set; } = 7; -} + private DateTime LastUpdatedTime { get; set; } = DateTime.Now; + + private List ForexRates { get; set; } = new() + { + new ForexRateModel("USD", "US Dollar", 18.42, 19.05, 0.12), + new ForexRateModel("EUR", "Euro", 19.98, 20.62, -0.04), + new ForexRateModel("GBP", "Pound", 23.34, 24.10, 0.18), + new ForexRateModel("AUD", "Aus Dollar", 12.05, 12.48, 0.03), + new ForexRateModel("BWP", "Pula", 1.31, 1.38, -0.01) + }; + + private List TravellerExpenses { get; set; } = new() + { + new ExpenseModel("20 May", "T. Maluleke", "Heathrow Express", "£", 25.00, 602.50, "Ground transport", "Reconciled"), + new ExpenseModel("20 May", "T. Maluleke", "Pret a Manger", "£", 12.40, 298.84, "Meals", "Reconciled"), + new ExpenseModel("21 May", "A. Dlamini", "Hilton Nairobi", "$", 210.00, 3886.20, "Accommodation", "Pending Receipt"), + new ExpenseModel("22 May", "A. Dlamini", "Java House", "$", 14.50, 268.39, "Meals", "In review") + }; + + // Travel money card states + private string MaskedCardNumber { get; set; } = "5318 ●●●● ●●●● 2207"; + private string CardHolderName { get; set; } = "T. Maluleke"; + private int CardUsdBalance { get; set; } = 1200; + private int CardGbpBalance { get; set; } = 820; + private bool IsCardFrozen { get; set; } = false; + + // Search query property + private string SearchQuery { get; set; } = string.Empty; + + // Estimator property bindings + private double ConvertAmount { get; set; } = 100; + private string SelectedConvertCurrency { get; set; } = "USD"; + + // Dialog trigger states + private bool ShowOrderDialog { get; set; } = false; + private bool ShowLoadDialog { get; set; } = false; + private string SelectedOrderCurrency { get; set; } = "USD"; + private double OrderAmount { get; set; } = 500; + private double LoadUsdAmount { get; set; } = 250; + private double LoadGbpAmount { get; set; } = 150; + + // Interactive banner states + private bool ShowAlertFeedback { get; set; } = false; + private string AlertFeedbackText { get; set; } = string.Empty; + + // Estimated calculator property + private double CalculatedZarAmount + { + get + { + var rate = ForexRates.FirstOrDefault(r => r.Code == SelectedConvertCurrency); + return rate != null ? ConvertAmount * rate.BuyRate : 0; + } + } + + // Live search filtered results + private IEnumerable FilteredExpenses + { + get + { + if (string.IsNullOrWhiteSpace(SearchQuery)) + { + return TravellerExpenses; + } + return TravellerExpenses.Where(e => + e.Merchant.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || + e.Traveller.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || + e.Category.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) + ); + } + } + + // ===================== C# LOGIC ACTIONS ===================== + private double GetSellRate(string currencyCode) + { + var rate = ForexRates.FirstOrDefault(r => r.Code == currencyCode); + return rate != null ? rate.SellRate : 0; + } + + private void OpenForexOrderDialog() + { + SelectedOrderCurrency = "USD"; + OrderAmount = 500; + ShowOrderDialog = true; + } + + private void OpenLoadCardDialog() + { + LoadUsdAmount = 250; + LoadGbpAmount = 150; + ShowLoadDialog = true; + } + + private void CloseDialogs() + { + ShowOrderDialog = false; + ShowLoadDialog = false; + } + + private void InitiateOrder(ForexRateModel rate) + { + SelectedOrderCurrency = rate.Code; + OrderAmount = 200; + ShowOrderDialog = true; + } + + private void SubmitForexOrder() + { + OpenForexOrdersCount++; + TriggerFeedback($"Order of {OrderAmount:N2} {SelectedOrderCurrency} confirmed for collection."); + CloseDialogs(); + } + + private void SubmitLoadCard() + { + CardUsdBalance += (int)LoadUsdAmount; + CardGbpBalance += (int)LoadGbpAmount; + LoadedCardBalance += (int)LoadUsdAmount; + TriggerFeedback($"Loaded ${LoadUsdAmount:N0} and £{LoadGbpAmount:N0} successfully onto Travel Card."); + CloseDialogs(); + } + + private void QuickReloadCard(int amount) + { + CardUsdBalance += amount; + LoadedCardBalance += amount; + TriggerFeedback($"Auto-reload triggered: successfully added ${amount} to USD wallet."); + } + + private void ToggleCardFreeze() + { + IsCardFrozen = !IsCardFrozen; + var state = IsCardFrozen ? "FROZEN" : "UNFROZEN"; + TriggerFeedback($"Travel money card has been successfully {state}."); + } + + private void ReconcileExpense(ExpenseModel expense) + { + expense.Status = "Reconciled"; + UnreconciledReceiptsCount = Math.Max(0, UnreconciledReceiptsCount - 1); + TriggerFeedback($"Expense at {expense.Merchant} successfully reconciled."); + } + + private void TriggerFeedback(string message) + { + AlertFeedbackText = message; + ShowAlertFeedback = true; + StateHasChanged(); + } + + // ===================== DATA MODEL CLASSES ===================== + public class ForexRateModel + { + public string Code { get; set; } + public string Name { get; set; } + public double BuyRate { get; set; } + public double SellRate { get; set; } + public double DayChange { get; set; } + + public ForexRateModel(string code, string name, double buyRate, double sellRate, double dayChange) + { + Code = code; + Name = name; + BuyRate = buyRate; + SellRate = sellRate; + DayChange = dayChange; + } + } + + public class ExpenseModel + { + public string Date { get; set; } + public string Traveller { get; set; } + public string Merchant { get; set; } + public string ForeignCurrency { get; set; } + public double ForeignAmount { get; set; } + public double ZarAmount { get; set; } + public string Category { get; set; } + public string Status { get; set; } + + public ExpenseModel(string date, string traveller, string merchant, string foreignCurrency, double foreignAmount, double zarAmount, string category, string status) + { + Date = date; + Traveller = traveller; + Merchant = merchant; + ForeignCurrency = foreignCurrency; + ForeignAmount = foreignAmount; + ZarAmount = zarAmount; + Category = category; + Status = status; + } + } +} \ No newline at end of file diff --git a/Tmt.SaqaTravel/Components/Pages/GdsSync.razor b/Tmt.SaqaTravel/Components/Pages/GdsSync.razor index 3fa2fd5..ef82650 100644 --- a/Tmt.SaqaTravel/Components/Pages/GdsSync.razor +++ b/Tmt.SaqaTravel/Components/Pages/GdsSync.razor @@ -1,7 +1,603 @@ @page "/gds-sync" -

GdsSync

+@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@rendermode InteractiveServer + +
+ + @* Page Header Area *@ +
+
+

GDS Sync Console

+

Real-time Amadeus, Travelport, and Sabre telemetry synchronization gateway and GDS negotiated rates validator

+
+
+ + +
+
+ + @* Alert / Feedback Notification Banner *@ + @if (!string.IsNullOrEmpty(AlertFeedback)) + { +
+ + + + + @AlertFeedback +
+ } + + @* GDS Status Core KPI Cards *@ +
+
+
Amadeus Node
+
+
Active / Live
+ 100% Up +
+
+ Last sync @AmadeusLastSyncs ago +
+
+ +
+
Travelport Node
+
+
Active / Live
+ 100% Up +
+
+ Last sync @TravelportLastSyncs ago +
+
+ +
+
Sabre Node
+
+
Active / Live
+ 100% Up +
+
+ Last sync @SabreLastSyncs ago +
+
+ +
+
In-Flight PNR Queue
+
@PnrQueueCount
+
+ Pending ticket printing checks +
+
+
+ + @* Spend vs Savings Visual Indicators & GDS Providers Metrics split *@ +
+ + @* Spend vs Savings Metrics Panel - Left Column (lg:col-span-7) *@ +
+
+
+
+

Spend vs. Savings Telemetry

+

Negotiated corporate GDS fares against open published consumer tickets for May 2026

+
+ +18.2% YTD Savings +
+ +
+

Below metrics reflect the total savings realized directly from GDS nodes through TMT projects pre-negotiated corporate rates.

+ +
+
+ Open Published Cost + R 2,662,890 + Consumer benchmark rates +
+
+ Negotiated Corporate Cost + R 2,250,310 + SAQA contracted limits +
+
+ Direct Savings Realized + R @TotalSavingsThisMonth.ToString("N0") + May 2026 reporting index +
+
+ + @* Visual Simulated Bar stack *@ +
+
+ May Spend Budget Saved Rate + 15.5% Saved +
+
+
+
+
+
+
+ + Negotiated (R 2,250,310) +
+
+ + Savings Realized (R @TotalSavingsThisMonth.ToString("N0")) +
+
+
+
+
+ +
+ Database verified by ASATA / IATA standards + Sync is operational +
+
+ + @* Savings by GDS Provider progress list - Right Column (lg:col-span-5) *@ +
+
+
+

Savings by GDS Provider Node

+

Contribution percentages realized across Amadeus, Travelport, and Sabre connection pools

+
+ +
+ + @* Provider 1 *@ +
+
+
+ Amadeus Node + Domestic & regional flights / local lodging +
+
+ R @AmadeusSavings.ToString("N0") + 52.9% of total +
+
+
+
+
+
+ + @* Provider 2 *@ +
+
+
+ Travelport Node + International airlines / premium class cabins +
+
+ R @TravelportSavings.ToString("N0") + 30.1% of total +
+
+
+
+
+
+ + @* Provider 3 *@ +
+
+
+ Sabre Node + Hotel aggregates & car rental structures +
+
+ R @SabreSavings.ToString("N0") + 17.0% of total +
+
+
+
+
+
+ + @* Highlighted Box *@ +
+ Total GDS Savings Verified + R @TotalSavingsThisMonth.ToString("N0") +
+ +
+
+ +
+ Savings are calculated on GDS booking confirmation vs standard unrestricted booking baseline. Verified by continuous live audit checks. +
+
+ +
+ + @* Live Supplier Feed Table & Sync Activity grid *@ +
+ + @* GDS Supplier Fares Feed - Left Column (lg:col-span-8) *@ +
+
+
+
+

Live Supplier Feed via GDS

+

Contracted tariffs fetched directly from active connection handshakes

+
+ + @* Search Filter *@ +
+ GDS Provider: + +
+
+ +
+ + + + + + + + + + + + + + @foreach (var feed in FilteredSupplierFeed) + { + + + + + + + + + + } + +
SupplierGDS NodeTypeOpen RateNegotiatedZAR SavedStatus
@feed.SupplierName + + @feed.GdsNode + + @feed.CategoryTypeR @feed.OpenPrice.ToString("N0")R @feed.NegotiatedPrice.ToString("N0") + R @feed.SavedValue.ToString("N0") (@feed.SavedPercentage%) + + + @feed.Status + +
+
+
+ +
+ * Rates are updated dynamically every 30 seconds. Supplier cache lifetimes are managed according to commercial service agreements. +
+
+ + @* Sync Activity & Health status indicators - Right Column (lg:col-span-4) *@ +
+ +
+
+

Node Traffic Analysis

+

Aggregated telemetry actions logged over last 24 hours

+
+ +
+ +
+
+ PNRs Generated + Across all connection tunnels +
+ 142 +
+ +
+
+ Fares & Tariff Queries + Read operations processed +
+ 8,420 +
+ +
+
+ Confirmed Air & Hotel Bookings + Ticketed in line with SAQA policies +
+ 118 +
+ +
+
+ Auto Refund / Voucher Queues + Cancellations processed smoothly +
+ 9 +
+ +
+
+ GDS Connection Failure Rate + Auto-retry failover threshold +
+ 0.04% (Exceptional) +
+ +
+
+ +
+
+

Telemetry Node Health

+

Direct response times & availability metrics

+
+ +
+ + @* Bar 1 *@ +
+
+ Amadeus API latency + 88ms (Avg) +
+
+
+
+
+ + @* Bar 2 *@ +
+
+ Travelport API latency + 82ms (Avg) +
+
+
+
+
+ + @* Bar 3 *@ +
+
+ Sabre API latency + 124ms (Avg) +
+
+
+
+
+ +
+ + + + +
+ GDS Node Uptime: 99.98% +

Automatic node failover setup ensures queries bypass any downtime immediately by fetching rates from auxiliary tunnels.

+
+
+ +
+
+ +
+ +
+ +
+ +@* ===================== MODAL DIALOGS ===================== *@ + +@* 1. GDS Sync Logs Modal *@ +@if (ShowSyncLogsModal) +{ +
+
+
+
+

Live GDS Telemetry Sync Logs

+

Handshake queries and active transaction parameters

+
+ +
+ + @* Console style panel *@ +
+ @foreach (var log in SyncLogs) + { +
+ @log.Timestamp + + [@log.Type] + + @log.Message +
+ } +
+ +
+ + +
+
+
+} + @code { + // Model Definitions + public class GdsSupplierFeed + { + public int Id { get; set; } + public string SupplierName { get; set; } = string.Empty; + public string GdsNode { get; set; } = string.Empty; // Amadeus, Travelport, Sabre + public string CategoryType { get; set; } = string.Empty; + public decimal OpenPrice { get; set; } + public decimal NegotiatedPrice { get; set; } + public decimal SavedValue => OpenPrice - NegotiatedPrice; + public int SavedPercentage => (int)System.Math.Round((SavedValue / OpenPrice) * 100); + public string Status { get; set; } = "Live"; + } + public class GdsConsoleLog + { + public string Timestamp { get; set; } = string.Empty; + public string Type { get; set; } = "Info"; // Success, Warning, Error, Info + public string Message { get; set; } = string.Empty; + } + + // Core States + private bool IsSyncing { get; set; } = false; + private string AlertFeedback { get; set; } = string.Empty; + private string SelectedGdsFilter { get; set; } = "All"; + private int PnrQueueCount { get; set; } = 14; + + // Last sync counters + private int AmadeusLastSyncs { get; set; } = 14; + private int TravelportLastSyncs { get; set; } = 41; + private int SabreLastSyncs { get; set; } = 22; + + // Total aggregate calculations + private decimal TotalSavingsThisMonth => AmadeusSavings + TravelportSavings + SabreSavings; + private decimal AmadeusSavings { get; set; } = 218400M; + private decimal TravelportSavings { get; set; } = 124180M; + private decimal SabreSavings { get; set; } = 70000M; + + // Modal Control + private bool ShowSyncLogsModal { get; set; } = false; + + // Databases + private List SupplierFeeds { get; set; } = new(); + private List SyncLogs { get; set; } = new(); + + // Filtered Feed list based on selection + private IEnumerable FilteredSupplierFeed => + SelectedGdsFilter == "All" + ? SupplierFeeds + : SupplierFeeds.Where(f => f.GdsNode.Equals(SelectedGdsFilter, System.StringComparison.OrdinalIgnoreCase)); + + protected override void OnInitialized() + { + // Populate supplier contracted feeds matching dashboard data + SupplierFeeds = new List + { + new GdsSupplierFeed { Id = 1, SupplierName = "SAA (South African Airways)", GdsNode = "Amadeus", CategoryType = "Air, Domestic", OpenPrice = 3290M, NegotiatedPrice = 2890M, Status = "Live" }, + new GdsSupplierFeed { Id = 2, SupplierName = "FlySafair Flights", GdsNode = "Amadeus", CategoryType = "Air, Low Cost", OpenPrice = 2180M, NegotiatedPrice = 1985M, Status = "Live" }, + new GdsSupplierFeed { Id = 3, SupplierName = "British Airways Standard", GdsNode = "Travelport", CategoryType = "Air, International", OpenPrice = 21440M, NegotiatedPrice = 18940M, Status = "Live" }, + new GdsSupplierFeed { Id = 4, SupplierName = "Emirates Corporate", GdsNode = "Travelport", CategoryType = "Air, International", OpenPrice = 28800M, NegotiatedPrice = 23100M, Status = "Live" }, + new GdsSupplierFeed { Id = 5, SupplierName = "Premier Hotels Collection", GdsNode = "Sabre", CategoryType = "Accommodation", OpenPrice = 2200M, NegotiatedPrice = 1895M, Status = "Live" }, + new GdsSupplierFeed { Id = 6, SupplierName = "City Lodge Hotels Corp", GdsNode = "Sabre", CategoryType = "Accommodation", OpenPrice = 1850M, NegotiatedPrice = 1520M, Status = "Live" }, + new GdsSupplierFeed { Id = 7, SupplierName = "Avis Car Rental", GdsNode = "Sabre", CategoryType = "Car Rental", OpenPrice = 520M, NegotiatedPrice = 420M, Status = "Live" }, + new GdsSupplierFeed { Id = 8, SupplierName = "Europcar SA Group", GdsNode = "Sabre", CategoryType = "Car Rental", OpenPrice = 580M, NegotiatedPrice = 460M, Status = "Cached, 2 min" } + }; + + // Populate initial sync terminal logger + SyncLogs = new List + { + new GdsConsoleLog { Timestamp = "11:15:02 AM", Type = "Info", Message = "Opening communication socket with Amadeus JNB Node (196.34.12.8)..." }, + new GdsConsoleLog { Timestamp = "11:15:03 AM", Type = "Success", Message = "Handshake authenticated successfully. Security key established." }, + new GdsConsoleLog { Timestamp = "11:15:04 AM", Type = "Info", Message = "Fetching May 2026 PNR ticket confirmation logs. Parsing 142 records..." }, + new GdsConsoleLog { Timestamp = "11:15:08 AM", Type = "Success", Message = "Amadeus sync process successfully closed. 14 open e-Tickets synced to active dashboard." }, + new GdsConsoleLog { Timestamp = "11:15:10 AM", Type = "Info", Message = "Querying Sabre GDS hotel & transfer rates. Auto-refresh threshold enabled." }, + new GdsConsoleLog { Timestamp = "11:15:12 AM", Type = "Warning", Message = "Europcar API socket returned lag index of 180ms. Auto-refresh fallback to cache enabled." } + }; + } + + // Force Resync Simulation Trigger + private async Task TriggerForceResync() + { + IsSyncing = true; + TriggerFeedback("Executing Force GDS Node Resync. Parsing active socket connections..."); + + // Simulating sync steps and logging parameters + SyncLogs.Add(new GdsConsoleLog { Timestamp = "Just Now", Type = "Info", Message = "USER SIGNAL: Initiating full cluster resynchronization..." }); + StateHasChanged(); + + await Task.Delay(1200); + SyncLogs.Add(new GdsConsoleLog { Timestamp = "Just Now", Type = "Success", Message = "Amadeus connection flushed and pulled fresh corporate PNR sets." }); + AmadeusLastSyncs = 1; + StateHasChanged(); + + await Task.Delay(800); + SyncLogs.Add(new GdsConsoleLog { Timestamp = "Just Now", Type = "Success", Message = "Travelport & Sabre nodes auto-recalibrated rates cache parameters." }); + TravelportLastSyncs = 1; + SabreLastSyncs = 1; + PnrQueueCount = System.Math.Max(0, PnrQueueCount - 2); + + IsSyncing = false; + TriggerFeedback("GDS Cluster Sync Complete! Telemetry matrices successfully mapped and negotiated rates parsed smoothly."); + StateHasChanged(); + } + + // Modal view triggers + private void OpenSyncLogsModal() + { + ShowSyncLogsModal = true; + } + + private void CloseSyncLogsModal() + { + ShowSyncLogsModal = false; + } + + private void ClearSyncLogs() + { + SyncLogs.Clear(); + SyncLogs.Add(new GdsConsoleLog { Timestamp = "Terminal Purged", Type = "Info", Message = "Sync log interface successfully cleared by admin command." }); + } + + // Toast alarm toaster + private void TriggerFeedback(string message) + { + AlertFeedback = message; + _ = DismissAlertAfterDelay(); + } + + private async Task DismissAlertAfterDelay() + { + await Task.Delay(5000); + AlertFeedback = string.Empty; + StateHasChanged(); + } } diff --git a/Tmt.SaqaTravel/Components/Pages/Home.razor b/Tmt.SaqaTravel/Components/Pages/Home.razor index 25fb7c9..746fd78 100644 --- a/Tmt.SaqaTravel/Components/Pages/Home.razor +++ b/Tmt.SaqaTravel/Components/Pages/Home.razor @@ -1,5 +1,9 @@ @page "/" @rendermode InteractiveServer +@using System +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks Overview Dashboard @@ -19,7 +23,7 @@ Export - @@ -288,66 +292,59 @@ - - -
-
JM
-
J. MokoenaExecutive
-
- - JNB → CPT, 02 Jun - Flight - Confirmed - R 3,240 - - - -
-
NN
-
N. NdlovuManager
-
- - Premier Hotel OR Tambo - Hotel - Confirmed - R 1,895 - - - -
-
SP
-
S. PillayCoordinator
-
- - Avis Group B, 3 days - Car - Pending - R 1,420 - - - -
-
TM
-
T. MalulekeDirector
-
- - JNB → LHR, 14 Jun - Flight - Confirmed - R 18,940 - - - -
-
LK
-
L. KhumaloOfficer
-
- - Shuttle: Sandton ⇄ OR Tambo - Shuttle - Cancelled - R 380 - + @foreach (var item in _recentBookings) + { + + +
+
@item.Initials
+
@item.Traveller@item.Role
+
+ + @item.Details + + @if (item.Type == "Flight") + { + Flight + } + else if (item.Type == "Hotel") + { + Hotel + } + else if (item.Type == "Car") + { + Car + } + else if (item.Type == "Shuttle") + { + Shuttle + } + else + { + @item.Type + } + + + @if (item.Status == "Confirmed") + { + Confirmed + } + else if (item.Status == "Pending") + { + Pending + } + else if (item.Status == "Cancelled") + { + Cancelled + } + else + { + @item.Status + } + + R @item.Cost.ToString("N0") + + } @@ -402,6 +399,74 @@ +@* Create Booking Modal *@ +@if (_showNewBookingModal) +{ +
+
+
+

+ + + + New Travel Booking +

+ +
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ This booking will be automatically synced with the global GDS network and reconciled against the corporate Lodge Card parameters. +
+
+ +
+ + +
+
+
+} + @code { private bool _isLoaded = false; @@ -426,6 +491,79 @@ private double ChartWidth = 800; private double ChartHeight = 220; + // Recent Bookings nested model & active list + public class HomeBookingItem + { + public string Traveller { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public string Initials { get; set; } = string.Empty; + public string Details { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // Flight, Hotel, Car, Shuttle + public string Status { get; set; } = string.Empty; // Confirmed, Pending, Cancelled + public decimal Cost { get; set; } + } + + private List _recentBookings = new() + { + new() { Traveller = "J. Mokoena", Role = "Executive", Initials = "JM", Details = "JNB → CPT, 02 Jun", Type = "Flight", Status = "Confirmed", Cost = 3240M }, + new() { Traveller = "N. Ndlovu", Role = "Manager", Initials = "NN", Details = "Premier Hotel OR Tambo", Type = "Hotel", Status = "Confirmed", Cost = 1895M }, + new() { Traveller = "S. Pillay", Role = "Coordinator", Initials = "SP", Details = "Avis Group B, 3 days", Type = "Car", Status = "Pending", Cost = 1420M }, + new() { Traveller = "T. Maluleke", Role = "Director", Initials = "TM", Details = "JNB → LHR, 14 Jun", Type = "Flight", Status = "Confirmed", Cost = 18940M }, + new() { Traveller = "L. Khumalo", Role = "Officer", Initials = "LK", Details = "Shuttle: Sandton ⇄ OR Tambo", Type = "Shuttle", Status = "Cancelled", Cost = 380M } + }; + + // Modal creation state bindings + private bool _showNewBookingModal = false; + private string _newTraveller = "J. Mokoena"; + private string _newRole = "Executive"; + private string _newDetails = "JNB → CPT, 18 Jun"; + private string _newType = "Flight"; + private decimal _newCost = 2500M; + + private void OpenBookingModal() + { + _newTraveller = ""; + _newRole = "Executive"; + _newDetails = ""; + _newType = "Flight"; + _newCost = 2500M; + _showNewBookingModal = true; + } + + private string GetInitials(string name) + { + if (string.IsNullOrWhiteSpace(name)) return "??"; + var parts = name.Split(new[] { ' ', '.' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) return "??"; + if (parts.Length == 1) return parts[0].Substring(0, Math.Min(2, parts[0].Length)).ToUpper(); + return (parts[0][0].ToString() + parts[parts.Length - 1][0].ToString()).ToUpper(); + } + + private void CreateBooking() + { + if (string.IsNullOrWhiteSpace(_newTraveller)) _newTraveller = "Anonymous"; + if (string.IsNullOrWhiteSpace(_newDetails)) _newDetails = "Local Trip"; + + var newBooking = new HomeBookingItem + { + Traveller = _newTraveller, + Role = _newRole, + Initials = GetInitials(_newTraveller), + Details = _newDetails, + Type = _newType, + Status = "Confirmed", + Cost = _newCost + }; + + _recentBookings.Insert(0, newBooking); + + // Update total spend dynamically + _kpiSpend += (double)_newCost; + _kpiTravellers += 1; + + _showNewBookingModal = false; + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -498,4 +636,4 @@ return (x, y); } -} \ No newline at end of file +} diff --git a/Tmt.SaqaTravel/Components/Pages/Mobile.razor b/Tmt.SaqaTravel/Components/Pages/Mobile.razor index 2bd285c..4bf7d0d 100644 --- a/Tmt.SaqaTravel/Components/Pages/Mobile.razor +++ b/Tmt.SaqaTravel/Components/Pages/Mobile.razor @@ -1,7 +1,691 @@ @page "/mobile" +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks + +
+ + @* Page Header Area *@ +
+
+

SAQA Mobile Companion

+

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

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

@ActiveTraveller

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

Marriott Bonvoy Partner ID Linked

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

Auto-converting exchange rates to ZAR settlement metrics

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

Johannesburg (JNB) to Cape Town (CPT)

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

2 Nights confirmed lodging · PO linked

+

Voucher reference: HT7811 · Billed to SAQA Lodge Card

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

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

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

Dynamic Mobile Capabilities

+

Purpose-built workflows configured for SAQA corporate compliance

+
+ Always Syncing +
+ +
+ + @* Cap 1 *@ +
+
+ + + + +
+

Profile & Doc Synchronization

+

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

+
+ + @* Cap 2 *@ +
+
+ + + + + +
+

Receipt OCR Engine (Offline Ready)

+

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

+
+ + @* Cap 3 *@ +
+
+ + + + +
+

Push Notification Alerts

+

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

+
+ + @* Tile 4 *@ +
+
+ + + + +
+

Approve On-the-Go

+

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

+
+
+
+ +
+
+

Adoption and Usage Analysis

+

Continuous telemetry tracking of mobile app engagements

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

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

+
+
+
+
+ +
+ +
+ +
-

Mobile

@code { + // Model Definitions + public class MockExpense + { + public string Date { get; set; } = string.Empty; + public string Merchant { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public string ForeignAmount { get; set; } = string.Empty; + public decimal ZarAmount { get; set; } + } + public class ChatBubble + { + public string Sender { get; set; } = string.Empty; + public string Timestamp { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; + public bool IsTraveller { get; set; } + } + + // Core states + private string ActivePhoneTab { get; set; } = "profile"; + private int ActiveInstalls { get; set; } = 186; + private int ReceiptsScannedCount { get; set; } = 412; + private decimal TotalValueScanned { get; set; } = 184200M; + private string AlertFeedback { get; set; } = string.Empty; + + // Active Traveller Details (Synchronized across UI metrics) + private string ActiveTraveller { get; set; } = "J. Mokoena"; + private string ActiveTravellerInitials { get; set; } = "JM"; + private int LoyaltyMiles { get; set; } = 84210; + + // Live OCR Scanning State simulation + private bool IsScanning { get; set; } = false; + + // Chat Interface Simulator states + private string NewChatMessage { get; set; } = string.Empty; + private bool IsTyping { get; set; } = false; + private int UnreadChatCount { get; set; } = 1; + + // Mock Database Arrays + private List MockExpenses { get; set; } = new(); + private List ChatMessages { get; set; } = new(); + + protected override void OnInitialized() + { + // Populate standard mock receipts matching the React Seed dataset + MockExpenses = new List + { + new MockExpense { Date = "20 May", Merchant = "Heathrow Express", Category = "Ground Transport", ForeignAmount = "£25.00", ZarAmount = 602.50M }, + new MockExpense { Date = "20 May", Merchant = "Pret a Manger", Category = "Meals", ForeignAmount = "£12.40", ZarAmount = 298.84M }, + new MockExpense { Date = "21 May", Merchant = "Hilton Nairobi", Category = "Accommodation", ForeignAmount = "$210.00", ZarAmount = 3886.20M }, + new MockExpense { Date = "22 May", Merchant = "Java House", Category = "Meals", ForeignAmount = "$14.50", ZarAmount = 268.39M } + }; + + // Populate initial live consultant conversation + ChatMessages = new List + { + new ChatBubble { Sender = "System Bot", Timestamp = "09:12 AM", Text = "Your travel profile is pre-verified for the CPT trip.", IsTraveller = false }, + new ChatBubble { Sender = "J. Mokoena", Timestamp = "09:14 AM", Text = "Hi, is my hotel voucher HT7811 checked with billing back?", IsTraveller = true }, + new ChatBubble { Sender = "Consultant (Sarah)", Timestamp = "09:15 AM", Text = "Yes, Mr. Mokoena. The bill-back to SAQA lodge card has been settled with the Premier hotel desk.", IsTraveller = false } + }; + } + + private void SetPhoneTab(string tabName) + { + ActivePhoneTab = tabName; + if (tabName == "chat") + { + UnreadChatCount = 0; + } + } + + // Handles traveller changes from selector + private void OnTravellerDeviceChanged(ChangeEventArgs e) + { + var selected = e.Value?.ToString() ?? "J. Mokoena"; + ActiveTraveller = selected; + ActiveTravellerInitials = string.Concat(selected.Split(' ').Select(n => n[0])).ToUpper(); + + if (selected == "J. Mokoena") + { + LoyaltyMiles = 84210; + } + else if (selected == "T. Maluleke") + { + LoyaltyMiles = 112450; + } + else if (selected == "A. Dlamini") + { + LoyaltyMiles = 45120; + } + else + { + LoyaltyMiles = 12890; + } + + // Regenerate contextual mock messages for the selected traveller + ChatMessages.Clear(); + ChatMessages.Add(new ChatBubble { Sender = "Sarah (TMT Desk)", Timestamp = "11:00 AM", Text = $"Welcome to SAQA Support channel, {ActiveTraveller}. How may we assist you today?", IsTraveller = false }); + + TriggerFeedback($"Device synchronization profile swapped to {ActiveTraveller} successfully."); + } + + // Live Simulated OCR Scanner trigger + private async Task TriggerMockOcrCapture() + { + IsScanning = true; + StateHasChanged(); + + // Simulated visual scan wait + await Task.Delay(2000); + + // Generate contextual mock foreign receipt based on current random factors + var rand = new System.Random(); + var currencies = new[] { ("€", 19.98M, "Uber Paris"), ("$", 18.42M, "Starbucks NY"), ("£", 23.34M, "Tesco London") }; + var chosen = currencies[rand.Next(currencies.Length)]; + + decimal foreignVal = rand.Next(10, 45) + 0.99M; + decimal convertedVal = foreignVal * chosen.Item2; + + var newExpense = new MockExpense + { + Date = "Today", + Merchant = chosen.Item3, + Category = "Business Travel Expense", + ForeignAmount = $"{chosen.Item1}{foreignVal.ToString("F2")}", + ZarAmount = System.Math.Round(convertedVal, 2) + }; + + MockExpenses.Insert(0, newExpense); + ReceiptsScannedCount++; + TotalValueScanned += convertedVal; + IsScanning = false; + + TriggerFeedback($"Live OCR Scan Completed! Detected {newExpense.Merchant} receipt: {newExpense.ForeignAmount} -> Auto-converted to R {newExpense.ZarAmount.ToString("N2")} at bank rate."); + StateHasChanged(); + } + + // Chat Simulator actions + private void HandleChatKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + _ = SendChatMessage(); + } + } + + private async Task SendChatMessage() + { + if (string.IsNullOrWhiteSpace(NewChatMessage)) return; + + var userMsg = new ChatBubble + { + Sender = ActiveTraveller, + Timestamp = "Just Now", + Text = NewChatMessage, + IsTraveller = true + }; + + ChatMessages.Add(userMsg); + var inputQuery = NewChatMessage; + NewChatMessage = string.Empty; + IsTyping = true; + StateHasChanged(); + + // Simulate typical support operator delay + await Task.Delay(1500); + + string replyText = "Understood. Let me look up your SAQA corporate itinerary. I will respond to this query immediately via email or text alert."; + + // Simple contextual mock chat responder + var lowered = inputQuery.ToLower(); + if (lowered.Contains("hotel") || lowered.Contains("voucher")) + { + replyText = $"I can confirm your booking voucher HT7811 with billing settled on Lodge Card. Late check-in has been requested at the desk."; + } + else if (lowered.Contains("flight") || lowered.Contains("gate") || lowered.Contains("delay")) + { + replyText = $"Let me check SA 311. Gate A12 is current and boarding is still scheduled for 05:30 AM SAST. No delays are reported on the GDS."; + } + else if (lowered.Contains("claim") || lowered.Contains("expense") || lowered.Contains("ocr")) + { + replyText = "For all claims, simply snap a picture under the Expenses tab inside this app. Our offline OCR sync transforms it into a draft claim instantly."; + } + + var botMsg = new ChatBubble + { + Sender = " Sarah (Travel Desk)", + Timestamp = "Just Now", + Text = replyText, + IsTraveller = false + }; + + ChatMessages.Add(botMsg); + IsTyping = false; + StateHasChanged(); + } + + // Feedback Alert helper + private void TriggerFeedback(string message) + { + AlertFeedback = message; + _ = DismissAlertAfterDelay(); + } + + private async Task DismissAlertAfterDelay() + { + await Task.Delay(5000); + AlertFeedback = string.Empty; + StateHasChanged(); + } } diff --git a/Tmt.SaqaTravel/Components/Pages/SlaReports.razor b/Tmt.SaqaTravel/Components/Pages/SlaReports.razor new file mode 100644 index 0000000..55f0631 --- /dev/null +++ b/Tmt.SaqaTravel/Components/Pages/SlaReports.razor @@ -0,0 +1,1222 @@ +@page "/sla-reports" +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@rendermode InteractiveServer + +
+ + @* Page Header Area *@ +
+
+

SLA & Monthly Reports Dashboard

+

Enterprise service level compliance, National Treasury rate-cap checking, and monthly operational reports ledger prepared for SAQA bid submission by TMT Projects

+
+ + @* Filters & Controls *@ +
+
+ Period: + +
+ +
+ Cost Centre: + +
+ +
+ + +
+
+
+ + @* Alert / Action Feedback Banner *@ + @if (!string.IsNullOrEmpty(AlertFeedback)) + { +
+
+ + + + + @AlertFeedback +
+ +
+ } + + @* SLA Scorecard Cards - Quick telemetry indicators *@ +
+ @foreach (var metric in CoreSlaMetrics) + { +
+
@metric.Name
+
+ @metric.Actual + @metric.Unit +
+
+ Target: @metric.Target + @metric.StatusLabel +
+
+ } +
+ + @* Navigation Tabs Bar *@ +
+ + + + +
+ + @* ===================== TAB 1: SCORECARD DETAILS ===================== *@ + @if (ActiveTab == "scorecard") + { +
+ + @* Left block: Target meters list (lg:col-span-8) *@ +
+
+
+

SAQA Dedicated SLA Telemetry Matrix

+

May 2026 contractual performance indexes, target deviations, and billing benchmarks

+
+ 97.8% SLA Aggregated Compliance +
+ +
+ + @* Metric row 1 *@ +
+
+
+ Booking Response turn-around (Domestic) + Measure of time elapsed from travel authorization submission to first quote iteration +
+
+ 44 minutes + Target: < 60 mins (98.4% met) +
+
+
+
+
+
+ + @* Metric row 2 *@ +
+
+
+ Emergency After-Hours response speed + Emergency helpline consultant response and ticket remediation threshold +
+
+ 9 minutes + Target: < 15 mins (100% met) +
+
+
+
+
+
+ + @* Metric row 3 *@ +
+
+
+ National Treasury Price Caps Adherence Rate + Adherence to Treasury instructions on flight quotas, hotel matrices and shuttle spending caps +
+
+ 94.1% compliance + Target: 100% (7 authorized deviations) +
+
+
+
+
+
+ + @* Metric row 4 *@ +
+
+
+ Billing Reconciliation & Lodge Matching + Automated lodge card clearing speed and invoice linkages matching PO guidelines +
+
+ 28 hours + Target: < 48 hours (99.1% met) +
+
+
+
+
+
+ + @* Metric row 5 *@ +
+
+
+ Ticketing Accuracy / Zero Voucher Error Rate + Zero ticketing rejects or hotel vouchers invalid on check-in +
+
+ 99.96% accuracy + Target: > 99.90% (0 errors) +
+
+
+
+
+
+ + @* Consolidated SLA Quality Box *@ +
+
+ SLA Target Met Rating + TMT Projects dedicated operations are strictly aligned with ASATA standards, assuring SAQA of seamless continuity. +
+
+ 97.8% Achieved +
+
+ +
+
+ + @* Right block: Live SLA alerts & warnings desk (lg:col-span-4) *@ +
+
+
+

Live SLA Alert Desk

+

Real-time GDS operational alerts matching May SLAs

+
+ +
+
+ + + + +
+
Hotel Cap Deviation Pending Review
+

Cape Town peak-booking R 2,400/night has passed 24h limit without CFO override. Ticket pending hold.

+ SLA limit: 4h response standard +
+
+ +
+ + + + +
+
After-Hours Call log triggered
+

London flight delay rerouting handled for Dr. Sithole. Resolution verified in 12 mins.

+ Target met (9m actual) +
+
+ +
+ + + + +
+
Lodge Reconciliation auto-cleared
+

142 May flight billing matched automatically with zero manual discrepancies logged.

+ Reconciled standard: 100% +
+
+
+
+ +
+ Real-time monitoring nodes are automatically backed by failover pathways. Performance thresholds audited independently. +
+
+ +
+ } + + @* ===================== TAB 2: REPORTS LIBRARY ===================== *@ + @if (ActiveTab == "library") + { +
+ + @* Reports Listing column (lg:col-span-4) *@ +
+
+
+

Standard Monthly Logs

+

Select a ledger below to view the interactive live telemetry data

+
+ +
+ @foreach (var report in MonthlyReportTemplates) + { + + } +
+
+ +
+ National Treasury compliant formats +

All ledgers export smoothly to National Treasury standard spreadsheets with pre-populated cost centers.

+
+
+ + @* Report Interactive Preview Table (lg:col-span-8) *@ +
+
+ @* Table Header Area *@ +
+
+

+ @ActiveReportMetadata.Emoji + @ActiveReportMetadata.Name +

+

Active data preview for @SelectedPeriod period

+
+ + @* Search Filter *@ +
+ Search: + +
+
+ + @* Table Renderings based on chosen reports *@ +
+ @if (SelectedReportKey == "deviations") + { + + + + + + + + + + + + + @foreach (var row in FilteredDeviations) + { + + + + + + + + + } + @if (!FilteredDeviations.Any()) + { + + + + } + +
TravellerRoutePaidJustification / ReasonApproverStatus
+
@row.Traveller
+ @row.CostCentre +
+
@row.Route (@row.Supplier)
+ @row.Date +
+
R @row.PaidPrice.ToString("N0")
+ Saved: R @row.Saving.ToString("N0") +
+
@row.Justification
+ Code: @row.ReasonCode +
@row.ApprovedBy + @row.Status +
No matching records found.
+ } + else if (SelectedReportKey == "afterhours") + { + + + + + + + + + + + + + @foreach (var row in FilteredAfterHours) + { + + + + + + + + + } + @if (!FilteredAfterHours.Any()) + { + + + + } + +
TimestampTravellerCategory / ReasonResolution ProcessSLA SpeedStatus
+
@row.Date
+ @row.Time +
@row.Traveller +
@row.Category
+ @row.Reason +
@row.Resolution + + @row.ResponseTimeMinutes mins + + Target: <15m + + @row.Status +
No matching records found.
+ } + else if (SelectedReportKey == "refunds") + { + + + + + + + + + + + + + @foreach (var row in FilteredRefunds) + { + + + + + + + + + } + @if (!FilteredRefunds.Any()) + { + + + + } + +
Traveller / Ticket #Airline & RouteFace ValueIssue / Expiry DateClaim StatusAction Required
+
@row.Traveller
+ Ticket: @row.TicketNumber +
+
@row.Airline
+ @row.Route +
R @row.FaceValue.ToString("N0") +
Issued: @row.IssueDate
+ Expires: @row.ExpiryDate +
+ + @row.RefundStatus + + + +
No matching records found.
+ } + else if (SelectedReportKey == "carbon") + { +
+ @{ + decimal totalCarbon = FilteredCarbon.Sum(c => c.CarbonFootprint); + decimal greenAccommodationRatio = FilteredCarbon.Any(c => c.Category == "Lodging" && c.EcoPerformanceTag.Contains("Eco")) ? 42.8M : 0M; + int treesOffset = (int)(totalCarbon * 41.5M); + } +
+
+ Total Carbon Emitted + @totalCarbon.ToString("F1") Tons CO₂ + @((totalCarbon * -0.24M).ToString("F1"))% vs baseline +
+
+ Green Accommodation Ratio + @(greenAccommodationRatio > 0 ? "42.8%" : "0.0%") Bookings + Target: 30.0% standard +
+
+ Tree-Offset Equivalent + @treesOffset.ToString("N0") Trees/Year + SLA Sustainability Match +
+
+ + + + + + + + + + + + + @foreach (var row in FilteredCarbon) + { + + + + + + + + } + @if (!FilteredCarbon.Any()) + { + + + + } + +
CategoryActivity MetricCarbon FootprintEco Performance TagGreen Recommendations
@row.Category@row.ActivityMetric= 5M ? "text-[#0066b3]" : "text-[#22a06b]")"> + @row.CarbonFootprint.ToString("F1") Tons CO₂ + + + @row.EcoPerformanceTag + + @row.GreenRecommendation
No matching records found.
+
+ } +
+
+ +
+ * Showing live data queried directly from ASATA-linked accounts. Securely verified by POPIA guidelines. + +
+
+ +
+ } + + @* ===================== TAB 3: CONSULTANTS PERFORMANCE ===================== *@ + @if (ActiveTab == "consultants") + { +
+ +
+
+
+

SAQA Dedicated Travel Consultants Performance

+

SLA productivity tracker for TMT Projects specialists permanently assigned to South African Qualifications Authority

+
+ 100% Availability Verified +
+ +
+ @foreach (var consultant in ConsultantPerformances) + { +
+
+
+
+ @string.Concat(consultant.Name.Split(' ').Select(n => n[0])) +
+
+

@consultant.Name

+ @consultant.Role +
+
+ +
+
+ Specialty Focus: + @consultant.Specialty +
+
+ Bookings Handled: + @consultant.BookingsHandled +
+
+ Avg Response Speed: + @consultant.ResponseTimeMinutes mins +
+
+ Error Ticket Volume: + + @consultant.ErrorsCount + +
+ + @* Performance bar *@ +
+
+ SLA Success Score: + @(consultant.ErrorsCount == 0 ? "100%" : "99.8%") +
+
+
+
+
+ + @* CSAT Score rating representation *@ +
+ CSAT Rating: +
+ ★ @consultant.CsatScore.ToString("F2") + / 5.0 +
+
+
+
+ + +
+ } +
+
+ +
+

Dedicated SLA Service Protocols

+

+ TMT Projects provides a dedicated 24/7 corporate desk. For executive and VIP bookings, Thabo Khumalo is assigned as the primary liaison officer. Escalated service matters and disputes bypass the standard queue and route instantly to Lerato Nkosi for immediate SLA verification and reconciliation logs. +

+
+ +
+ } + + @* ===================== TAB 4: DISPUTE & SERVICE LOGS ===================== *@ + @if (ActiveTab == "disputes") + { +
+ + @* Service dispute logging form (lg:col-span-5) *@ +
+
+

Log SLA Query or Service Violation

+

Submit service delays or rate cap disputes for priority TMT investigation

+
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+
+ + @* Active disputes / Queries tracking ledger (lg:col-span-7) *@ +
+
+
+
+

SAQA Active Service Quality Log

+

Live status tracker of submitted queries in May 2026

+
+ + @ServiceQueries.Count(q => q.Status != "Resolved") Active Disputes + +
+ +
+ @foreach (var query in ServiceQueries) + { +
+
+ @if (query.Urgency == "High") + { + + + + + } + else + { + + + + + } +
+ +
+
+

@query.Category

+ @query.Date +
+

@query.Description

+
+ Traveller Reference: @query.TravellerName +
+ + @query.Status + + @if (query.Status != "Resolved") + { + + } +
+
+
+
+ } +
+
+ +
+ Disputes logged are actioned by Lerato Nkosi within 4 hours. Escalations are automatically flagged in daily reports. +
+
+ +
+ } + +
+ +@code { + // Reporting parameters + private string SelectedPeriod { get; set; } = "May 2026"; + private string SelectedCostCentre { get; set; } = "All"; + + // SLA state feedback messages + private string AlertFeedback { get; set; } = string.Empty; + + // Primary Tab Selection + private string ActiveTab { get; set; } = "scorecard"; + + // Reports Library state + private string SelectedReportKey { get; set; } = "deviations"; + private string ReportSearchQuery { get; set; } = string.Empty; + + // SLA Query / Dispute form bind states + private string NewQueryCategory { get; set; } = "Booking Response Delay"; + private string NewQueryTraveller { get; set; } = string.Empty; + private string NewQueryUrgency { get; set; } = "Medium"; + private string NewQueryDescription { get; set; } = string.Empty; + + // Data Structures & Models + public class SlaMetric + { + public string Name { get; set; } = string.Empty; + public string Target { get; set; } = string.Empty; + public string Actual { get; set; } = string.Empty; + public string Unit { get; set; } = string.Empty; + public string StatusLabel { get; set; } = string.Empty; + public string StatusClass { get; set; } = string.Empty; + public string BarColorClass { get; set; } = string.Empty; + } + + public class MonthlyReportMetadata + { + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Emoji { get; set; } = string.Empty; + public int RecordCount { get; set; } + } + + public class DeviationReportRow + { + public string Traveller { get; set; } = string.Empty; + public string CostCentre { get; set; } = string.Empty; + public string Route { get; set; } = string.Empty; + public string Date { get; set; } = string.Empty; + public string Supplier { get; set; } = string.Empty; + public decimal OpenPrice { get; set; } + public decimal PaidPrice { get; set; } + public decimal Saving => OpenPrice - PaidPrice; + public string ReasonCode { get; set; } = string.Empty; + public string Justification { get; set; } = string.Empty; + public string ApprovedBy { get; set; } = string.Empty; + public string Status { get; set; } = "Approved"; + } + + public class AfterHoursLogRow + { + public string Date { get; set; } = string.Empty; + public string Time { get; set; } = string.Empty; + public string Traveller { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public string Reason { get; set; } = string.Empty; + public string Resolution { get; set; } = string.Empty; + public int ResponseTimeMinutes { get; set; } + public string Status { get; set; } = "Resolved"; + public string CostCentre { get; set; } = string.Empty; + } + + public class UnusedTicketRow + { + public string Traveller { get; set; } = string.Empty; + public string TicketNumber { get; set; } = string.Empty; + public string Airline { get; set; } = string.Empty; + public string Route { get; set; } = string.Empty; + public decimal FaceValue { get; set; } + public string IssueDate { get; set; } = string.Empty; + public string ExpiryDate { get; set; } = string.Empty; + public string RefundStatus { get; set; } = "Pending"; + public string ActionPending { get; set; } = "Process Refund"; + public string CostCentre { get; set; } = string.Empty; + } + + public class CarbonReportRow + { + public string Category { get; set; } = string.Empty; + public string ActivityMetric { get; set; } = string.Empty; + public decimal CarbonFootprint { get; set; } + public string EcoPerformanceTag { get; set; } = string.Empty; + public string GreenRecommendation { get; set; } = string.Empty; + public string CostCentre { get; set; } = string.Empty; + public string Date { get; set; } = string.Empty; + } + + public class ConsultantPerformance + { + public string Name { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public string Specialty { get; set; } = string.Empty; + public int BookingsHandled { get; set; } + public int ErrorsCount { get; set; } + public int ResponseTimeMinutes { get; set; } + public double CsatScore { get; set; } + } + + public class SlaQueryDispute + { + public int Id { get; set; } + public string Date { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public string TravellerName { get; set; } = string.Empty; + public string Urgency { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Status { get; set; } = "In Review"; + } + + // Databases + private List CoreSlaMetrics { get; set; } = new(); + private Dictionary MonthlyReportTemplates { get; set; } = new(); + private List DeviationRows { get; set; } = new(); + private List AfterHoursRows { get; set; } = new(); + private List UnusedTicketRows { get; set; } = new(); + private List CarbonRows { get; set; } = new(); + private List ConsultantPerformances { get; set; } = new(); + private List ServiceQueries { get; set; } = new(); + + // Active Metadata calculated helper property + private MonthlyReportMetadata ActiveReportMetadata => MonthlyReportTemplates.ContainsKey(SelectedReportKey) + ? MonthlyReportTemplates[SelectedReportKey] + : new MonthlyReportMetadata { Name = "Report Log", Emoji = "📁" }; + + // Dynamic Record Counter + private int GetReportCount(string key) + { + return key switch + { + "deviations" => FilteredDeviations.Count(), + "afterhours" => FilteredAfterHours.Count(), + "refunds" => FilteredRefunds.Count(), + "carbon" => FilteredCarbon.Count(), + _ => 0 + }; + } + + // Cost Centre normalizer + private bool IsCostCentreMatch(string itemCc, string selectedCc) + { + if (selectedCc == "All") return true; + + string normalizedItem = itemCc.Replace("&", "and").Replace(" ", "").ToLower(); + string normalizedSelected = selectedCc.Replace("&", "and").Replace(" ", "").ToLower(); + + return normalizedItem == normalizedSelected; + } + + // Date Period helper + private bool IsDateInPeriod(string itemDate, string selectedPeriod) + { + if (string.IsNullOrEmpty(selectedPeriod) || selectedPeriod == "All") return true; + + if (selectedPeriod == "May 2026") + { + return itemDate.Contains("May", System.StringComparison.OrdinalIgnoreCase); + } + else if (selectedPeriod == "April 2026") + { + return itemDate.Contains("Apr", System.StringComparison.OrdinalIgnoreCase); + } + else if (selectedPeriod == "Q1 2026") + { + return itemDate.Contains("Jan", System.StringComparison.OrdinalIgnoreCase) + || itemDate.Contains("Feb", System.StringComparison.OrdinalIgnoreCase) + || itemDate.Contains("Mar", System.StringComparison.OrdinalIgnoreCase); + } + + return true; + } + + // Filtered Preview Data Lists + private IEnumerable FilteredDeviations => DeviationRows + .Where(d => IsCostCentreMatch(d.CostCentre, SelectedCostCentre) + && IsDateInPeriod(d.Date, SelectedPeriod) + && (string.IsNullOrEmpty(ReportSearchQuery) + || d.Traveller.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || d.CostCentre.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || d.Supplier.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || d.Justification.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase))); + + private IEnumerable FilteredAfterHours => AfterHoursRows + .Where(a => IsCostCentreMatch(a.CostCentre, SelectedCostCentre) + && IsDateInPeriod(a.Date, SelectedPeriod) + && (string.IsNullOrEmpty(ReportSearchQuery) + || a.Traveller.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || a.Category.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || a.Reason.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || a.Resolution.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase))); + + private IEnumerable FilteredRefunds => UnusedTicketRows + .Where(r => IsCostCentreMatch(r.CostCentre, SelectedCostCentre) + && IsDateInPeriod(r.IssueDate, SelectedPeriod) + && (string.IsNullOrEmpty(ReportSearchQuery) + || r.Traveller.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || r.Airline.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || r.Route.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase))); + + private IEnumerable FilteredCarbon => CarbonRows + .Where(c => IsCostCentreMatch(c.CostCentre, SelectedCostCentre) + && IsDateInPeriod(c.Date, SelectedPeriod) + && (string.IsNullOrEmpty(ReportSearchQuery) + || c.Category.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || c.ActivityMetric.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase) + || c.GreenRecommendation.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase))); + + protected override void OnInitialized() + { + LoadScorecardData(); + LoadMonthlyReportsMeta(); + LoadDeviationReport(); + LoadAfterHoursReport(); + LoadUnusedTicketsReport(); + LoadCarbonReport(); + LoadConsultantPerformances(); + LoadServiceDisputes(); + } + + private void SetActiveTab(string tab) + { + ActiveTab = tab; + ReportSearchQuery = string.Empty; + } + + private void SelectReport(string reportKey) + { + SelectedReportKey = reportKey; + ReportSearchQuery = string.Empty; + } + + private void OnFilterChanged() + { + TriggerFeedback($"Loaded SLA and operations data for period '{SelectedPeriod}' under Cost Centre '{SelectedCostCentre}' Filter."); + } + + // PDF and Excel download simulation triggers + private void TriggerPdfDownload() + { + TriggerFeedback($"Generating Monthly SLA Compliance Pack for period {SelectedPeriod}. Downloader thread initialized..."); + } + + private void TriggerExcelDownload() + { + TriggerFeedback($"Exporting Full Operational Ledgers ({ActiveReportMetadata.Name}) as spreadsheet output for National Treasury submission."); + } + + private void TriggerFeedback(string msg) + { + AlertFeedback = msg; + _ = DismissAlertDelay(); + } + + private async Task DismissAlertDelay() + { + await Task.Delay(4000); + AlertFeedback = string.Empty; + StateHasChanged(); + } + + // Submit user SLA Issue query + private void SubmitSlaQuery() + { + if (string.IsNullOrEmpty(NewQueryTraveller) || string.IsNullOrEmpty(NewQueryDescription)) + { + TriggerFeedback("ERROR: Please fill in all fields (Traveller Name & Problem description) before logging a query."); + return; + } + + var newQuery = new SlaQueryDispute + { + Id = ServiceQueries.Count + 1, + Date = "Just Now", + Category = NewQueryCategory, + TravellerName = NewQueryTraveller, + Urgency = NewQueryUrgency, + Description = NewQueryDescription, + Status = "Awaiting Desk Action" + }; + + ServiceQueries.Insert(0, newQuery); + NewQueryTraveller = string.Empty; + NewQueryDescription = string.Empty; + + TriggerFeedback($"SUCCESS: SLA Service Query registered successfully as Issue ID #2026-QS0{newQuery.Id}. Dispatched to Lerato Nkosi."); + } + + // Complete dispute action simulating resolution + private void ResolveSlaQuerySim(int id) + { + var found = ServiceQueries.FirstOrDefault(q => q.Id == id); + if (found != null) + { + found.Status = "Resolved"; + TriggerFeedback($"SLA Service Issue #{id} marked as RESOLVED. Audit trace logged."); + } + } + + // Process voucher simulation inside registry + private void TriggerVoucherAction(UnusedTicketRow row) + { + if (row.RefundStatus == "Pending") + { + row.RefundStatus = "Refunded"; + row.ActionPending = "Audit Logged"; + TriggerFeedback($"Refund of R {row.FaceValue:N0} successfully processed to SAQA Lodge Card for traveller '{row.Traveller}'."); + } + else if (row.RefundStatus == "Credit Logged") + { + row.RefundStatus = "Applied to Flight"; + row.ActionPending = "Synced to GDS"; + TriggerFeedback($"Credit note R {row.FaceValue:N0} reallocated dynamically to upcoming domestic route."); + } + } + + // Contact Consultant sim + private void StartConsultantChatSim(string name) + { + TriggerFeedback($"Escalated priority channel connected directly with {name}. Chat initialized securely via TLS."); + } + + // Core Data Loaders + private void LoadScorecardData() + { + CoreSlaMetrics = new List + { + new SlaMetric { Name = "Response Time", Target = "< 60m", Actual = "44", Unit = "Mins Avg", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#22a06b]" }, + new SlaMetric { Name = "After Hours SLA", Target = "< 15m", Actual = "9", Unit = "Mins Avg", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#00a3a1]" }, + new SlaMetric { Name = "Rate Cap Adherence", Target = "100%", Actual = "94.1", Unit = "% Booked", StatusLabel = "Warning", StatusClass = "bg-[#fff4dd] text-[#b06b00]", BarColorClass = "before:bg-[#f4a300]" }, + new SlaMetric { Name = "Lodge Matching Speed", Target = "< 48h", Actual = "28", Unit = "Hours Avg", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#22a06b]" }, + new SlaMetric { Name = "Ticketing Quality", Target = "> 99.9%", Actual = "99.96", Unit = "% Perfect", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#22a06b]" }, + new SlaMetric { Name = "Adoption (Mobile Portal)", Target = "> 75%", Actual = "78.1", Unit = "% Active", StatusLabel = "Target Met", StatusClass = "bg-[#e2eefb] text-[#0066b3]", BarColorClass = "before:bg-[#0066b3]" } + }; + } + + private void LoadMonthlyReportsMeta() + { + MonthlyReportTemplates = new Dictionary + { + { "deviations", new MonthlyReportMetadata { Name = "NT Cost Containment Deviation Ledger", Description = "Out-of-policy bookings requiring CFO exception logs and approvals", Emoji = "⚖️", RecordCount = 7 } }, + { "afterhours", new MonthlyReportMetadata { Name = "After-Hours Support Events Log", Description = "Late night crisis calls, flight delays and urgent re-routing records", Emoji = "🌙", RecordCount = 4 } }, + { "refunds", new MonthlyReportMetadata { Name = "Unused Ticket & Open Refund Registry", Description = "Credits notes tracked securely to prevent voucher value expiry", Emoji = "🎟️", RecordCount = 5 } }, + { "carbon", new MonthlyReportMetadata { Name = "CO2 Sustainability Footprint Report", Description = "Carbon offsets, bed-night emissions, and eco accommodation trends", Emoji = "🍃", RecordCount = 3 } } + }; + } + + private void LoadDeviationReport() + { + DeviationRows = new List + { + new DeviationReportRow { Traveller = "T. Maluleke", CostCentre = "Office of the CEO", Route = "JNB → LHR", Date = "14 May 2026", Supplier = "British Airways", OpenPrice = 21440M, PaidPrice = 18940M, ReasonCode = "DEV-CAB-01", Justification = "Premium Economy class upgrade justified by medical clearance certification", ApprovedBy = "CFO (Approved)", Status = "Authorized" }, + new DeviationReportRow { Traveller = "J. Mokoena", CostCentre = "Office of the CEO", Route = "JNB → CPT", Date = "02 May 2026", Supplier = "SAA", OpenPrice = 3290M, PaidPrice = 2890M, ReasonCode = "CAP-AIR-01", Justification = "Late schedule confirmation due to urgent CEO board meetings; exceeded treasury R2,500 flight cap.", ApprovedBy = "CFO Office", Status = "Authorized" }, + new DeviationReportRow { Traveller = "N. Ndlovu", CostCentre = "Qualifications & Standards", Route = "Cape Town stay", Date = "22 May 2026", Supplier = "Premier Hotels", OpenPrice = 2400M, PaidPrice = 1895M, ReasonCode = "CAP-HOT-02", Justification = "Cape Town peak-season rates exceed R 1,900/night limit. Bypassed with CFO support.", ApprovedBy = "CFO (Line Manager)", Status = "Authorized" }, + new DeviationReportRow { Traveller = "S. Pillay", CostCentre = "Verifications", Route = "Durban engagement", Date = "14 May 2026", Supplier = "Avis Car", OpenPrice = 520M, PaidPrice = 420M, ReasonCode = "CAP-CAR-01", Justification = "Personal leisure extension added. Differential costs calculated and billed to personal card.", ApprovedBy = "Corporate Services", Status = "Authorized" } + }; + } + + private void LoadAfterHoursReport() + { + AfterHoursRows = new List + { + new AfterHoursLogRow { Date = "22 May 2026", Time = "11:14 PM", Traveller = "Dr. P. Sithole", Category = "Flight Delay Rerouting", Reason = "BA JNB-LHR delayed by 4 hours, missing connecting transit node", Resolution = "Rerouted to early morning shuttle flight. Lounge voucher auto-issued.", ResponseTimeMinutes = 9, Status = "Resolved", CostCentre = "Office of the CEO" }, + new AfterHoursLogRow { Date = "18 May 2026", Time = "01:05 AM", Traveller = "T. Maluleke", Category = "Hotel Check-In Dispute", Reason = "Premier Hotel desk logged zero confirmation for booking #HT7811", Resolution = "Contacted hotel night supervisor, synced Sabre API voucher, check-in cleared.", ResponseTimeMinutes = 11, Status = "Resolved", CostCentre = "Office of the CEO" }, + new AfterHoursLogRow { Date = "10 May 2026", Time = "05:45 AM", Traveller = "A. Dlamini", Category = "Urgent eVisa Query", Reason = "eVisa barcode rejected on Nairobi check-in transit node", Resolution = "Repulled VFS Global Kenyan eVisa PDF. SMS copy sent to boarding agent.", ResponseTimeMinutes = 6, Status = "Resolved", CostCentre = "Qualifications & Standards" } + }; + } + + private void LoadUnusedTicketsReport() + { + UnusedTicketRows = new List + { + new UnusedTicketRow { Traveller = "J. Mokoena", TicketNumber = "083-4412092", Airline = "SAA", Route = "JNB → CPT", FaceValue = 2890M, IssueDate = "21 May 2026", ExpiryDate = "21 Jul 2026", RefundStatus = "Pending", ActionPending = "Process Refund", CostCentre = "Office of the CEO" }, + new UnusedTicketRow { Traveller = "T. Maluleke", TicketNumber = "125-9921443", Airline = "British Airways", Route = "JNB → LHR", FaceValue = 18940M, IssueDate = "05 Jan 2026", ExpiryDate = "05 Sep 2026", RefundStatus = "Credit Logged", ActionPending = "Apply Note", CostCentre = "Office of the CEO" }, + new UnusedTicketRow { Traveller = "N. Ndlovu", TicketNumber = "083-1144021", Airline = "FlySafair", Route = "JNB → DUR", FaceValue = 1985M, IssueDate = "18 Apr 2026", ExpiryDate = "18 Aug 2026", RefundStatus = "Pending", ActionPending = "Process Refund", CostCentre = "Qualifications & Standards" } + }; + } + + private void LoadCarbonReport() + { + CarbonRows = new List + { + new CarbonReportRow { Category = "Air Travel", ActivityMetric = "142 Flights logged", CarbonFootprint = 21.4M, EcoPerformanceTag = "Heavy Footprint", GreenRecommendation = "Consolidate multiple JNB-CPT site trips", CostCentre = "Office of the CEO", Date = "14 May 2026" }, + new CarbonReportRow { Category = "Lodging", ActivityMetric = "128 Bed nights", CarbonFootprint = 9.8M, EcoPerformanceTag = "Eco Certified (42%)", GreenRecommendation = "Select Green-flag Premier Hotels", CostCentre = "Qualifications & Standards", Date = "22 May 2026" }, + new CarbonReportRow { Category = "Ground & Transit", ActivityMetric = "32 Car rental cycles", CarbonFootprint = 3.0M, EcoPerformanceTag = "Optimal", GreenRecommendation = "Utilize Gautrain connections regularly", CostCentre = "Verifications", Date = "14 May 2026" }, + new CarbonReportRow { Category = "Air Travel", ActivityMetric = "28 Flights logged", CarbonFootprint = 4.2M, EcoPerformanceTag = "Moderate", GreenRecommendation = "Use Microsoft Teams for routine alignments", CostCentre = "Qualifications & Standards", Date = "18 Apr 2026" }, + new CarbonReportRow { Category = "Lodging", ActivityMetric = "15 Bed nights", CarbonFootprint = 1.1M, EcoPerformanceTag = "Eco Certified (100%)", GreenRecommendation = "Green accommodation target met", CostCentre = "Office of the CEO", Date = "10 Jan 2026" } + }; + } + + private void LoadConsultantPerformances() + { + ConsultantPerformances = new List + { + new ConsultantPerformance { Name = "Thabo Khumalo", Role = "Senior Travel Lead", Specialty = "VIP & Executive Bookings", BookingsHandled = 412, ErrorsCount = 0, ResponseTimeMinutes = 44, CsatScore = 4.92 }, + new ConsultantPerformance { Name = "Lerato Nkosi", Role = "Reconciliation Expert", Specialty = "SLA Compliance & Disputes", BookingsHandled = 310, ErrorsCount = 1, ResponseTimeMinutes = 22, CsatScore = 4.88 }, + new ConsultantPerformance { Name = "Zama Ndlovu", Role = "Helpline Coordinator", Specialty = "After-Hours Emergency Support", BookingsHandled = 142, ErrorsCount = 0, ResponseTimeMinutes = 9, CsatScore = 4.96 } + }; + } + + private void LoadServiceDisputes() + { + ServiceQueries = new List + { + new SlaQueryDispute { Id = 1, Date = "24 May 2026", Category = "Booking Response Delay", TravellerName = "A. Dlamini", Urgency = "High", Description = "Flight request from Durban took 74 mins to receive first quote validation. SLA limit is 60 mins.", Status = "In Review" }, + new SlaQueryDispute { Id = 2, Date = "19 May 2026", Category = "Billing / Invoice Dispute", TravellerName = "N. Ndlovu", Urgency = "Medium", Description = "Double billing check-in on Durban Airport transfer invoice. Outstanding audit matched in ledger.", Status = "Resolved" } + }; + } +} diff --git a/Tmt.SaqaTravel/Components/Pages/TripReports.razor b/Tmt.SaqaTravel/Components/Pages/TripReports.razor index 9583292..99b1380 100644 --- a/Tmt.SaqaTravel/Components/Pages/TripReports.razor +++ b/Tmt.SaqaTravel/Components/Pages/TripReports.razor @@ -1,7 +1,186 @@ @page "/trip-reports" +@rendermode InteractiveServer -

TripReports

+
+ + +
+
+

Trip Reports

+

System generated consolidated cost per trip, in line with NT Cost Containment reporting

+
+
+ + +
+
+ + +
+
+
+

Trip TRP-2026-0488, J. Mokoena, Cape Town engagement

+

Travel Authorisation TA-44219, dated 02 to 04 Jun 2026

+
+ All components consolidated +
+ +
+ + + + + + + + + + + + + @foreach (var item in ConsolidatedItems) + { + + + + + + + + + } + + + + + + + + + +
ComponentSupplierReferenceDateNegotiatedAmount
+ + @item.Component + + @item.Supplier@item.Reference@item.Date@item.Negotiated@item.Amount
Total trip costR 12,960.00
Savings against open ratesR 1,840.00
+
+ +
+ + + +
+
+ + +
+ + +
+
+
+

Standard Monthly Reports

+

Auto-issued per NT Cost Containment template

+
+
+ +
+ @foreach (var report in MonthlyReports) + { +
+
+
@report.Title
+
@report.Description
+
+ +
+ } +
+
+ + +
+
+
+

SLA Reports

+

Per the SAQA SLA template

+
+
+ +
+ @foreach (var report in SlaReports) + { +
+
+
@report.Title
+
@report.Description
+
+ +
+ } +
+
+ +
+
@code { + public class ReportItem + { + public string Component { get; set; } = string.Empty; + public string Supplier { get; set; } = string.Empty; + public string Reference { get; set; } = string.Empty; + public string Date { get; set; } = string.Empty; + public string Negotiated { get; set; } = string.Empty; + public string Amount { get; set; } = string.Empty; + public string Type { get; set; } = "default"; + } -} + public class ReportInfo + { + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } + + private List ConsolidatedItems = new() + { + new ReportItem { Component = "Flight (out)", Supplier = "SAA, SA 311", Reference = "AB23PQ", Date = "02 Jun", Negotiated = "Yes, 12 percent off", Amount = "R 2,890.00", Type = "blue" }, + new ReportItem { Component = "Flight (return)", Supplier = "SAA, SA 322", Reference = "AB23PQ", Date = "04 Jun", Negotiated = "Yes", Amount = "R 2,640.00", Type = "blue" }, + new ReportItem { Component = "Hotel", Supplier = "Premier Hotel Waterfront", Reference = "HT7811", Date = "02 to 04 Jun, 2 nights", Negotiated = "Govt rate", Amount = "R 3,790.00", Type = "default" }, + new ReportItem { Component = "Car rental", Supplier = "Avis, Group B", Reference = "AV1140", Date = "02 to 04 Jun, 3 days", Negotiated = "Yes", Amount = "R 1,420.00", Type = "gold" }, + new ReportItem { Component = "Shuttle", Supplier = "SA Express Shuttle", Reference = "SX0921", Date = "02 Jun", Negotiated = "No", Amount = "R 380.00", Type = "default" }, + new ReportItem { Component = "Daily allowance", Supplier = "SAQA per diem policy", Reference = "PD-019", Date = "3 days", Negotiated = "n/a", Amount = "R 1,560.00", Type = "default" }, + new ReportItem { Component = "Service fee", Supplier = "TMT Projects", Reference = "SF-2026-0488", Date = "02 Jun", Negotiated = "Flat", Amount = "R 280.00", Type = "green" } + }; + + private List MonthlyReports = new() + { + new ReportInfo { Title = "Cost per trip report", Description = "Consolidated, all components, all travellers" }, + new ReportInfo { Title = "Spend by category", Description = "Air, accommodation, ground, other" }, + new ReportInfo { Title = "Traveller activity", Description = "Per cost centre and per traveller" } + }; + + private List SlaReports = new() + { + new ReportInfo { Title = "After hours report", Description = "All out of hours support events" }, + new ReportInfo { Title = "Consultant productivity", Description = "Bookings handled per consultant" }, + new ReportInfo { Title = "Refund and open voucher log", Description = "Status of all open items" } + }; + + private string GetComponentBadgeClass(string type) + { + return type switch + { + "blue" => "bg-[#e2eefb] text-[#0066b3]", + "green" => "bg-[#e3f6ed] text-[#22a06b]", + "gold" => "bg-[#fff4dd] text-[#b06b00]", + _ => "bg-[#e8f4f4] text-[#00a3a1]" + }; + } +} \ No newline at end of file diff --git a/Tmt.SaqaTravel/Components/Pages/Vas.razor b/Tmt.SaqaTravel/Components/Pages/Vas.razor index 4e0ced4..6092239 100644 --- a/Tmt.SaqaTravel/Components/Pages/Vas.razor +++ b/Tmt.SaqaTravel/Components/Pages/Vas.razor @@ -1,7 +1,768 @@ @page "/vas" +@rendermode InteractiveServer + +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks + +
+ + @* Page Header Area *@ +
+
+

Value-Added Services (VAS)

+

Global destination intelligence, live travel risk feeds, and premium VIP traveller concierge support

+
+
+ +
+
+ + @* Alert / Feedback Notification Banner *@ + @if (!string.IsNullOrEmpty(AlertFeedback)) + { +
+ + + + + @AlertFeedback +
+ } + + @* Quick intelligence overview widgets *@ +
+ @foreach (var dest in IntelligentDestinations) + { +
+
+
+
+

@dest.City, @dest.CountryCode

+

Traveller: @dest.Traveller

+
+ @dest.WeatherEmoji +
+ +
+
@dest.Temperature°C
+
+ @dest.WeatherCondition + Local time: @dest.LocalTime +
+
+ +
+
+ Visa Requirement: + + @dest.VisaStatus + +
+
+ Health Warning: + @dest.HealthWarning +
+
+ Global Safety Risk: + + @dest.RiskLevel + +
+
+
+ +
+ +
+
+ } +
+ + @* Risk Advisories and Services Splitting grid *@ +
+ + @* Live Travel Advisories Feed - Left Column (lg:col-span-7) *@ +
+
+
+
+

Global Safety & Risk Feed

+

Real-time alerts synced through TMT global risk metrics and WMO meteorology bulletins

+
+
+ + + +
+
+ +
+ @foreach (var alert in FilteredRiskAlerts) + { +
+
+ @if (alert.Severity == "Critical") + { + + + + + } + else if (alert.Severity == "Warning") + { + + + + + } + else + { + + + + + } +
+ +
+
+

@alert.Title

+ @alert.PublishedTime +
+

@alert.Description

+
+ Category: @alert.Category + +
+
+
+ } +
+
+ +
+ Alert definitions are updated dynamically via real-time international GDS PNR telemetry streams. +
+
+ + @* Value-added Services At a Glance - Right Column (lg:col-span-5) *@ +
+ +
+
+

Always-On Concierge Services

+

Select a capability to simulate corporate booking and guidelines

+
+ +
+ + @* Tile 1: Airport VIP Lounge *@ +
+
+ + + + +
+

Airport Lounges

+

Pre-book SLOW & international Bidvest airport lounges.

+
+ + @* Tile 2: Shuttle Transfer Calcs *@ +
+
+ + + + +
+

Shuttle Transfers

+

Calculate airport transfers, shuttles & express trains.

+
+ + @* Tile 3: Places of Interest *@ +
+
+ + + + + +
+

Culture Guides

+

Corporate travel orientations & localized city guides.

+
+ + @* Tile 4: Health Warnings Tracker *@ +
+
+ + + + +
+

Health & Vaccines

+

WHO, CDC Yellow Fever certificate lookup checker.

+
+ +
+
+ + @* Travel Help Desk Assistance Box *@ +
+
+
+ Premium Support +

24/7 Global Emergency Assistance

+

If a traveller gets stranded, requires flight changes, or encounters medical events, our emergency assistance room handles all fast-tracks instantly.

+
+
+
+ Local Help-desk + 0800 TMT HELP +
+
+ International Hotline + +27 82 555 4400 +
+
+
+ +
+ +
+ +
+ +@* ===================== MODAL DIALOGS ===================== *@ + +@* 1. Book VIP Lounge & Concierge Modal *@ +@if (ShowVipBookingModal) +{ +
+
+
+
+

Book Premium VIP Services

+

Fast-track security and airport lounge clearance for SAQA travellers

+
+ +
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+

+ + + + + + SAQA Travel Policy Check +

+

Airport lounge clearances are automatically covered for executive travellers. Other staff require line-manager travel authorization justification files linked to their PO.

+
+ +
+ + +
+
+
+
+} + +@* 2. Public Transport & Shuttle Calculator Modal *@ +@if (ShowShuttleModal) +{ +
+
+
+
+

Airport Shuttle Transfer Calculator

+

Calculate corporate transfers vs. public options at destination

+
+ +
+ +
+
+ + +
+ + @{ + var options = GetTransferOptions(SelectedCalcCity); + } + +
+
Cost Comparison Matrix (converted to ZAR)
+ @foreach (var opt in options) + { +
+
+ @opt.Name + Time: @opt.Duration +
+
+ R @opt.ZarCost.ToString("N0") + @opt.Type +
+
+ } +
+ +
+

TMT Recommendations:

+

Our preferred corporate shuttle transfers provide meet & greet in the terminal arrivals hall, reducing delays and offering bill-back directly to the corporate travel card.

+
+ +
+ +
+
+
+
+} + +@* 3. Culture Guidelines Modal *@ +@if (ShowCultureModal) +{ +
+
+
+
+

Travel Orientation & Cultural Guidelines

+

Empowering corporate travellers with destination intelligence and local advice

+
+ +
+ +
+ +
+
+
United Kingdom (London Orientation)
+

Always keep to the left on tube escalators to avoid delays. Mobile contactless is widely accepted across all public transport. Standard tipping is 10-12.5% on meals if service charge is not already included. Safe and well-lit walking spaces throughout.

+
+ +
+
Kenya (Nairobi Orientation)
+

M-Pesa mobile money is universal; ask the help-desk on how to set it up prior to landing. Avoid carrying excess physical cash or wearing flashy items. Pre-arranged hotel transport or Uber is highly recommended for late-night airport transfers.

+
+ +
+
South Africa (Cape Town Orientation)
+

Local microclimate weather shifts can occur quickly; packing lightweight warm layers is ideal. Use pre-arranged airport transfer shuttles rather than flagging roadside cabs. High alert on security awareness in localized central sectors after hours.

+
+
+ +
+ +
+ +
+
+
+} + +@* 4. Health Warnings Tracker & Vaccine Requirements *@ +@if (ShowHealthTrackerModal) +{ +
+
+
+
+

Health & Vaccine Checklist Portal

+

Pre-travel health screening for SAQA corporate compliance

+
+ +
+ +
+ +
+ + + + +
+ International Yellow Card +

SAQA travellers returning from East Africa, West Africa or South America require a physical Yellow Fever vaccine certificate logged on checkout.

+
+
+ +
+
+
Nairobi, Kenya
+

Yellow Fever vaccination certificate mandatory upon landing. Malaria prophylaxis is strongly advised for excursions outside urban Nairobi.

+
+
+
United Kingdom & Schengen Area
+

No special vaccine cards or malaria restrictions required. General healthcare reciprocal frameworks apply for travel insurance files.

+
+
+
United Arab Emirates (Dubai)
+

Ensure any special prescription medicine is logged on official SAQA Support channels beforehand, as UAE customs maintains strict medical framework checklists.

+
+
+ +
+ +
+ +
+
+
+} -

Vas

@code { + // Model Definitions + public class DestinationGuide + { + public string City { get; set; } = string.Empty; + public string CountryCode { get; set; } = string.Empty; + public string Traveller { get; set; } = string.Empty; + public string WeatherEmoji { get; set; } = string.Empty; + public int Temperature { get; set; } + public string WeatherCondition { get; set; } = string.Empty; + public string LocalTime { get; set; } = string.Empty; + public string VisaStatus { get; set; } = string.Empty; + public string HealthWarning { get; set; } = string.Empty; + public string RiskLevel { get; set; } = string.Empty; + } + public class GlobalRiskAlert + { + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string PublishedTime { get; set; } = string.Empty; + public string Severity { get; set; } = "Info"; // Critical, Warning, Info + public string Category { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } + + public class AirportTransferOpt + { + public string Name { get; set; } = string.Empty; + public string Duration { get; set; } = string.Empty; + public decimal ZarCost { get; set; } + public string Type { get; set; } = string.Empty; // Train, Shuttle, Cab + } + + // Core States + private string AlertFeedback { get; set; } = string.Empty; + private string SelectedAlertFilter { get; set; } = "All"; + + // Modals Control + private bool ShowVipBookingModal { get; set; } = false; + private bool ShowShuttleModal { get; set; } = false; + private bool ShowCultureModal { get; set; } = false; + private bool ShowHealthTrackerModal { get; set; } = false; + + // Vip booking form states + private string VipSelectedTraveller { get; set; } = "J. Mokoena"; + private string VipSelectedAirport { get; set; } = "OR Tambo (JNB)"; + private string VipServiceLevel { get; set; } = "Fast-track + Lounge"; + private DateTime VipFlightDate { get; set; } = new DateTime(2026, 6, 2); + + // Shuttle calculator states + private string SelectedCalcCity { get; set; } = "London"; + + // Databases + private List IntelligentDestinations { get; set; } = new(); + private List SafetyAlerts { get; set; } = new(); + + // Filtered Risk Alerts derived properties + private IEnumerable FilteredRiskAlerts => + SelectedAlertFilter == "All" + ? SafetyAlerts + : SelectedAlertFilter == "Critical" + ? SafetyAlerts.Where(a => a.Severity == "Critical") + : SafetyAlerts.Where(a => a.Severity == "Warning" || a.Severity == "Info"); + + protected override void OnInitialized() + { + // Populate standard intelligence trackers matching React seed database + IntelligentDestinations = new List + { + new DestinationGuide + { + City = "London", + CountryCode = "UK", + Traveller = "T. Maluleke", + WeatherEmoji = "☁️", + Temperature = 14, + WeatherCondition = "Light drizzle, windy", + LocalTime = "10:15 AM BST", + VisaStatus = "Standard Visa", + HealthWarning = "None active", + RiskLevel = "Low" + }, + new DestinationGuide + { + City = "Nairobi", + CountryCode = "KE", + Traveller = "A. Dlamini", + WeatherEmoji = "⛅", + Temperature = 22, + WeatherCondition = "Partly Cloudy", + LocalTime = "12:15 PM EAT", + VisaStatus = "eVisa Required", + HealthWarning = "Yellow Fever Mandatory", + RiskLevel = "Moderate" + }, + new DestinationGuide + { + City = "Cape Town", + CountryCode = "ZA", + Traveller = "J. Mokoena", + WeatherEmoji = "🌧️", + Temperature = 12, + WeatherCondition = "Heavy rain, cold front", + LocalTime = "11:15 AM SAST", + VisaStatus = "Not Applicable", + HealthWarning = "None", + RiskLevel = "Low" + } + }; + + // Populate global advisories feed + SafetyAlerts = new List + { + new GlobalRiskAlert + { + Id = 1, + Title = "Planned Ground Crew Strike action (London Heathrow)", + PublishedTime = "22 May 2026, 09:14 AM", + Severity = "Critical", + Category = "Labor Dispute", + Description = "A 48-hour ground handler strike is notified for Heathrow Airport Terminal 2 & 5. Expect airline rescheduling, baggage delays, and boarding queues. SAQA travellers on the BA route have been contacted for pre-arranged lounge transfers." + }, + new GlobalRiskAlert + { + Id = 2, + Title = "Tropical Cyclone advisory (Mozambique Channel)", + PublishedTime = "23 May 2026, 08:30 AM", + Severity = "Warning", + Category = "Severe Weather", + Description = "High-velocity tropical system tracking near Beira. Staff scheduled for Maputo or regional liaison should avoid non-essential regional flights until June 2." + }, + new GlobalRiskAlert + { + Id = 3, + Title = "High Capacity Notice: SLOW Lounge OR Tambo JNB", + PublishedTime = "24 May 2026, 07:15 AM", + Severity = "Info", + Category = "Facility Advisory", + Description = "SLOW lounge is reporting high peak queues between 06:00 and 09:00 SAST daily. Executive travellers are advised to pre-book their slots using this companion app to guarantee priority entry." + } + }; + } + + // Interactive Filters + private void SetAlertFilter(string filter) + { + SelectedAlertFilter = filter; + } + + // Destinations action guides + private void ExploreDestinationInDepth(DestinationGuide dest) + { + TriggerFeedback($"Loaded complete Orientation Portfolio for {dest.City} ({dest.CountryCode}). Sending localized safety briefs to {dest.Traveller}'s device."); + } + + private void SimulateNotifyTravellers(GlobalRiskAlert alert) + { + TriggerFeedback($"Notification sent successfully! Pushed critical alert '{alert.Title}' as an SMS and inside the Companion Wallet of active SAQA travellers."); + } + + // Vip booking processors + private void ProcessVipBooking() + { + TriggerFeedback($"VIP Service Request successfully processed! Authorized '{VipServiceLevel}' for {VipSelectedTraveller} departing {VipSelectedAirport} on {VipFlightDate.ToString("yyyy-MM-dd")}. Billed to SAQA Lodge Card."); + ShowVipBookingModal = false; + } + + // Airport transfer calculator helper + private List GetTransferOptions(string city) + { + if (city == "London") + { + return new List + { + new AirportTransferOpt { Name = "Heathrow Express (Train)", Duration = "15 mins", ZarCost = 600M, Type = "Train" }, + new AirportTransferOpt { Name = "Elizabeth Line Tube", Duration = "35 mins", ZarCost = 310M, Type = "Train" }, + new AirportTransferOpt { Name = "Corporate Partner Shuttle", Duration = "45 mins", ZarCost = 1450M, Type = "Shuttle" } + }; + } + else if (city == "Nairobi") + { + return new List + { + new AirportTransferOpt { Name = "Pre-arranged Hotel Shuttle", Duration = "25 mins", ZarCost = 450M, Type = "Shuttle" }, + new AirportTransferOpt { Name = "Local Rideshare App (Uber)", Duration = "30 mins", ZarCost = 280M, Type = "Cab" } + }; + } + else + { + return new List + { + new AirportTransferOpt { Name = "SA Express Shuttle Service", Duration = "20 mins", ZarCost = 380M, Type = "Shuttle" }, + new AirportTransferOpt { Name = "MyCiTi Airport Bus 110", Duration = "30 mins", ZarCost = 120M, Type = "Shuttle" }, + new AirportTransferOpt { Name = "Airport authorized Private Cab", Duration = "25 mins", ZarCost = 320M, Type = "Cab" } + }; + } + } + + // Modals visibility Helpers + private void OpenVipBookingModal() + { + ShowVipBookingModal = true; + } + + private void CloseVipBookingModal() + { + ShowVipBookingModal = false; + } + + private void OpenShuttleModal() + { + ShowShuttleModal = true; + } + + private void CloseShuttleModal() + { + ShowShuttleModal = false; + } + + private void OpenCultureModal() + { + ShowCultureModal = true; + } + + private void CloseCultureModal() + { + ShowCultureModal = false; + } + + private void OpenHealthTrackerModal() + { + ShowHealthTrackerModal = true; + } + + private void CloseHealthTrackerModal() + { + ShowHealthTrackerModal = false; + } + + // Toast triggered alert toaster + private void TriggerFeedback(string message) + { + AlertFeedback = message; + _ = DismissAlertAfterDelay(); + } + + private async Task DismissAlertAfterDelay() + { + await Task.Delay(5000); + AlertFeedback = string.Empty; + StateHasChanged(); + } } diff --git a/Tmt.SaqaTravel/Components/Pages/Visa.razor b/Tmt.SaqaTravel/Components/Pages/Visa.razor index 0e1340a..7665c9a 100644 --- a/Tmt.SaqaTravel/Components/Pages/Visa.razor +++ b/Tmt.SaqaTravel/Components/Pages/Visa.razor @@ -1,7 +1,1094 @@ @page "/visa" +@using System.Collections.Generic +@using System.Linq + +
+ @* Page Header Area *@ +
+
+

Visa Services

+

Start, track and complete visa applications for SAQA travellers

+
+
+ + +
+
+ + @if (!string.IsNullOrEmpty(AlertFeedback)) + { +
+ + + + + @AlertFeedback +
+ } + + @* KPI Grid *@ +
+
+
Active Applications
+
@ActiveAppsCount
+
+ 3 in progress, 2 awaiting consulate +
+
+ +
+
Approved This Month
+
@ApprovedThisMonth
+
+ 100% success rate +
+
+ +
+
Average Turnaround
+
@AverageTurnaround
+
+ Schengen and UK +
+
+ +
+
Documents Pending
+
@DocumentsPending
+
+ Action required from traveller +
+
+
+ + @* Grid 2: Core Workflow and Table Panels *@ +
+ @* Visa Application Workflow Card *@ +
+
+
+

Visa Application Workflow

+

Start a new application or manage selected traveller's steps

+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+
+ + @* Steps Progress Checklist *@ + @if (ActiveWorkflowApp != null) + { +
+
+

+ Workflow Status: @ActiveWorkflowApp.Traveller (@ActiveWorkflowApp.Country) +

+ Travel Date: @ActiveWorkflowApp.TravelDate +
+ + @foreach (var step in ActiveWorkflowApp.Steps.Select((value, i) => new { value, i })) + { +
+
+
+ @(step.i + 1) +
+
+
@step.value.Label
+
@step.value.Sub
+
+
+ + @step.value.Status + +
+ } +
+ } +
+
+ + @* Application Tracker Card *@ +
+
+
+
+

Application Tracker

+

Live status across all travellers and destinations

+
+ Active Tracker +
+ +
+ + + + + + + + + + + + @foreach (var app in VisaApplications) + { + + + + + + + + } + +
TravellerCountryTypeStageAction
+
+
+ @app.Avatar +
+
+
@app.Traveller
+
+
+
@app.Country@app.Type + + @app.Stage + + + @if (app.Stage == "Docs required") + { + + } + else + { + + } +
+
+
+
+ +
+ + @* Visa Information Library Card *@ +
+
+
+

Visa Information Library

+

Live entry rules database for 195 countries · Updated daily

+
+ Updated daily +
+ +
+ @* Tile 1: Entry Requirements *@ +
+
+ + + + + +
+

Entry Requirements

+

Rules by passport nationality & destination country

+
+ + @* Tile 2: Processing Times *@ +
+
+ + + + + +
+

Processing Times

+

Live consulate turnaround estimation tracker

+
+ + @* Tile 3: Fee Calculator *@ +
+
+ + + + + + + +
+

Fee Calculator

+

Local embassy fees plus handling & VFS charges

+
+ + @* Tile 4: Embassy Locator *@ +
+
+ + + + + +
+

Embassy Locator

+

Nearest consulates, hours, booking portal links

+
+
+
+ +
+ +@* ===================== MODAL DIALOGS ===================== *@ + +@* 1. Visa Rules Database Modal *@ +@if (ShowVisaRulesModal) +{ +
+
+
+
+

Visa Rules Database

+

SAQA official compliance & reference database

+
+ +
+ + @* Tabs bar *@ +
+ + + + +
+ +
+ @if (ActiveLibraryTab == "requirements") + { +
+
+ + + + + + + Holders of South African passports require standard visas for UK, Schengen, and USA. eVise solutions are available for Kenya and UAE. +
+
+
+
United Kingdom (Standard Visitor Visa)
+

Required documents: Valid passport (6 months validity), detailed travel itinerary, 3 months payslips, 3 months bank statements, employment verification letter from SAQA Human Resources.

+
+
+
Schengen Area (Short-stay Business Visa)
+

Required documents: Schengen application form, travel insurance coverage of min €30,000, flight itinerary with reserved return tickets, hotel bookings, official SAQA invitation/support letter, 3-months stamped bank statements.

+
+
+
USA (B1/B2 Visitor Visa)
+

Required: Completed online DS-160 form, payment confirmation slip, interview scheduled at US Embassy (Pretoria/Johannesburg), physical photo (5x5cm), SAQA letter affirming funding responsibility.

+
+
+
+ } + + @if (ActiveLibraryTab == "processing") + { +
+

Approximate turnaround times based on VFS Global and consulate metrics for South African applicants:

+
+
+
+ United Kingdom + 10 to 15 working days +
+
+
+
+
+
+
+ Schengen Area (Germany / France / Italy) + 5 to 10 working days +
+
+
+
+
+
+
+ USA (B1/B2) + 30+ working days (Appointment backlog) +
+
+
+
+
+
+
+ Kenya (eVisa) + 1 to 3 working days +
+
+
+
+
+
+
+ } + + @if (ActiveLibraryTab == "fees") + { +
+
+

Live Interactive Visa Fee Calculator

+
+
+ + +
+
+ + +
+
+ + @{ + var fees = GetCalculatedFees(CalcCountry, CalcVisaType); + } + +
+
+ Embassy consulate fee (converted): + R @fees.embassyFee.ToString("N0") +
+
+ VFS Global/TLS handling fee: + R @fees.serviceFee.ToString("N0") +
+
+ Total Corporate Lodge Cost: + R @fees.total.ToString("N0") +
+
+ + +
+
+ } + + @if (ActiveLibraryTab == "locator") + { +
+
+
+
VFS Global (Schengen & UAE Processing)
+

Address: 1st Floor, Riviera Road Office Park, 66 Riviera Rd, Johannesburg

+

Hours: Mon-Fri 08:00 - 15:00 · Appointment Required

+
+
+
TLScontact (United Kingdom Visa Center)
+

Address: 3rd Floor, 24 Central, 6 Fort St, Sandton, Johannesburg

+

Hours: Mon-Fri 08:30 - 16:00 · Priority service slots available

+
+
+
United States Consulate General
+

Address: 1 Sandton Dr, Sandhurst, Sandton

+

Hours: By strict scheduled appointment slip only

+
+
+
+ } +
+ +
+ +
+
+
+} + +@* 2. Start New Visa Application Modal *@ +@if (ShowNewAppModal) +{ +
+
+
+
+

Start New Visa Application

+

Initiate visa record for tracking & automated checklist support

+
+ +
+
+
+ + +
+ + @if (NewAppTraveller == "Custom") + { +
+ + +
+ } + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + + + + + + SAQA Corporate Compliance +
+

Creating this record auto-populates pre-verified profile files, schedules background lodge card authorizations, and sends VFS checklists to the traveller.

+
+ +
+ + +
+
+
+
+} + +@* 3. Document Submission Portal Modal *@ +@if (ShowUploadDocsModal) +{ +
+
+
+
+

Document Submission Portal

+

Submit missing requirements to proceed with application

+
+ +
+
+
+ +
+ + + + + + + Awaiting: Signed visa declaration form & 3 months stamped bank statements. +
+
+ + @* Mock Drag and drop zone *@ +
+ + + + +
Click here to simulate file selection
+
Simulates uploading PDF, JPG, PNG (Max 10MB per file)
+
+ + @if (UploadSelectedFiles.Count > 0) + { +
+
Selected Files (@UploadSelectedFiles.Count):
+ @foreach (var file in UploadSelectedFiles) + { +
+ @file + Ready +
+ } +
+ } + +
+ + +
+
+
+
+} -

Visa

@code { + // Model Definitions + public class VisaStep + { + public string Label { get; set; } = string.Empty; + public string Sub { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string StatusClass { get; set; } = string.Empty; + } + public class VisaApplication + { + public int Id { get; set; } + public string Traveller { get; set; } = string.Empty; + public string Avatar { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string Stage { get; set; } = string.Empty; + public string TravelDate { get; set; } = string.Empty; + public List Steps { get; set; } = new(); + } + + // Core States + private int ActiveAppsCount { get; set; } = 5; + private int ApprovedThisMonth { get; set; } = 8; + private string AverageTurnaround { get; set; } = "9 days"; + private int DocumentsPending { get; set; } = 3; + private string AlertFeedback { get; set; } = string.Empty; + + // Interactive Selectors + private string VisaSelectedTraveller { get; set; } = "T. Maluleke"; + private string VisaSelectedDestination { get; set; } = "United Kingdom"; + private DateTime VisaTravelDate { get; set; } = new DateTime(2026, 6, 14); + private int ActiveWorkflowId { get; set; } = 1; + + // Library interactive fee calculator state + private string CalcCountry { get; set; } = "United Kingdom"; + private string CalcVisaType { get; set; } = "Business"; + + // Modals visibility toggles + private bool ShowVisaRulesModal { get; set; } = false; + private bool ShowNewAppModal { get; set; } = false; + private bool ShowUploadDocsModal { get; set; } = false; + private string ActiveLibraryTab { get; set; } = "requirements"; + + // Modals data input forms + private string NewAppTraveller { get; set; } = "T. Maluleke"; + private string NewAppCustomTraveller { get; set; } = string.Empty; + private string NewAppDestination { get; set; } = "Schengen Area"; + private string NewAppType { get; set; } = "Business"; + private DateTime NewAppDate { get; set; } = new DateTime(2026, 7, 15); + + // Document submissions portal tracker + private int? SelectedUploadAppId { get; set; } = null; + private List UploadSelectedFiles { get; set; } = new(); + + // Data Source List + private List VisaApplications { get; set; } = new(); + + // Derived active app in workflow detail panel + private VisaApplication? ActiveWorkflowApp => + VisaApplications.FirstOrDefault(a => a.Id == ActiveWorkflowId) ?? VisaApplications.FirstOrDefault(); + + protected override void OnInitialized() + { + // Populate standard mock data matching React App's seed dataset + VisaApplications = new List + { + new VisaApplication + { + Id = 1, + Traveller = "T. Maluleke", + Avatar = "TM", + Country = "United Kingdom", + Type = "Business", + Stage = "At consulate", + TravelDate = "2026-06-14", + Steps = new List + { + new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 2, document checklist", Sub = "Passport, photo, invitation letter, bank statement", Status = "4 of 4 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 2,840", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 4, consulate appointment", Sub = "VFS Sandton, 28 May, 10:30", Status = "Booked", StatusClass = "bg-[#e2eefb] text-[#0066b3] border border-[#b4d3f5]" }, + new VisaStep { Label = "Step 5, decision", Sub = "Visa sticker delivery to office", Status = "Awaiting", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" } + } + }, + new VisaApplication + { + Id = 2, + Traveller = "A. Dlamini", + Avatar = "AD", + Country = "Kenya", + Type = "eVisa", + Stage = "Approved", + TravelDate = "2026-06-20", + Steps = new List + { + new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step-2, document checklist", Sub = "Passport copy, digital photo, itinerary", Status = "2 of 2 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 1,250", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 4, consulate appointment", Sub = "Not Required (eVisa system)", Status = "Not Required", StatusClass = "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" }, + new VisaStep { Label = "Step 5, decision", Sub = "Approved eVisa copy sent to traveller email", Status = "Approved", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" } + } + }, + new VisaApplication + { + Id = 3, + Traveller = "J. Mokoena", + Avatar = "JM", + Country = "Schengen Area", + Type = "Business", + Stage = "Docs uploaded", + TravelDate = "2026-07-01", + Steps = new List + { + new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 2, document checklist", Sub = "Passport, photo, Schengen insurance, flights", Status = "4 of 4 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 2,200", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 4, consulate appointment", Sub = "VFS Global Cherry Lane, 12 June, 09:15", Status = "Booked", StatusClass = "bg-[#e2eefb] text-[#0066b3] border border-[#b4d3f5]" }, + new VisaStep { Label = "Step 5, decision", Sub = "Passport pending collection", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" } + } + }, + new VisaApplication + { + Id = 4, + Traveller = "N. Ndlovu", + Avatar = "NN", + Country = "USA", + Type = "B1 / B2", + Stage = "Docs required", + TravelDate = "2026-08-15", + Steps = new List + { + new VisaStep { Label = "Step 1, DS-160 application form", Sub = "Partially filled by traveller", Status = "In progress", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }, + new VisaStep { Label = "Step 2, document checklist", Sub = "Passport, US-spec photos, bank statements required", Status = "2 of 4 uploaded", StatusClass = "bg-[#fde5e8] text-[#dc2626] border border-[#f8b4b4]" }, + new VisaStep { Label = "Step 3, fee payment", Sub = "Pending approval from travel authority", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }, + new VisaStep { Label = "Step 4, consulate appointment", Sub = "Awaiting DS-160 completion & payment", Status = "Not booked", StatusClass = "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" }, + new VisaStep { Label = "Step 5, decision", Sub = "US Embassy Pretoria interview", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" } + } + }, + new VisaApplication + { + Id = 5, + Traveller = "S. Pillay", + Avatar = "SP", + Country = "UAE", + Type = "eVisa", + Stage = "Approved", + TravelDate = "2026-06-05", + Steps = new List + { + new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 2, document checklist", Sub = "Passport copy, photo, hotel booking", Status = "2 of 2 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 1,850", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 4, consulate appointment", Sub = "Not Required (eVisa system)", Status = "Not Required", StatusClass = "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" }, + new VisaStep { Label = "Step 5, decision", Sub = "Approved eVisa copy sent to traveller email", Status = "Approved", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" } + } + } + }; + } + + // Workflows and Tracker actions + private void OnWorkflowTravellerChanged() + { + var matched = VisaApplications.FirstOrDefault(a => a.Traveller.Equals(VisaSelectedTraveller, System.StringComparison.OrdinalIgnoreCase)); + if (matched != null) + { + ActiveWorkflowId = matched.Id; + } + } + + private void SetActiveWorkflow(int id) + { + ActiveWorkflowId = id; + var app = VisaApplications.FirstOrDefault(a => a.Id == id); + if (app != null) + { + VisaSelectedTraveller = app.Traveller; + VisaSelectedDestination = app.Country; + } + } + + private void HandleStartWorkflow() + { + var existing = VisaApplications.FirstOrDefault(app => app.Traveller.Equals(VisaSelectedTraveller, System.StringComparison.OrdinalIgnoreCase)); + if (existing != null) + { + ActiveWorkflowId = existing.Id; + TriggerFeedback($"Application for {VisaSelectedTraveller} already exists. Showing current progress below."); + } + else + { + var initials = string.Concat(VisaSelectedTraveller.Split(' ').Select(n => n.Length > 0 ? n[0].ToString() : string.Empty)).ToUpper(); + if (string.IsNullOrEmpty(initials)) initials = "TR"; + + var newId = (VisaApplications.Any() ? VisaApplications.Max(a => a.Id) : 0) + 1; + var newApp = new VisaApplication + { + Id = newId, + Traveller = VisaSelectedTraveller, + Avatar = initials, + Country = VisaSelectedDestination, + Type = "Business", + Stage = "Docs uploaded", + TravelDate = VisaTravelDate.ToString("yyyy-MM-dd"), + Steps = new List + { + new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 2, document checklist", Sub = "Passport copy, digital photos, supporting letters", Status = "4 of 4 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 2,150", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep { Label = "Step 4, consulate appointment", Sub = "Awaiting appointment confirmation", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }, + new VisaStep { Label = "Step 5, decision", Sub = "Embassy review in progress", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" } + } + }; + VisaApplications.Insert(0, newApp); + ActiveWorkflowId = newId; + ActiveAppsCount++; + TriggerFeedback($"Successfully initialized a new {VisaSelectedDestination} visa application workflow for {VisaSelectedTraveller}."); + } + } + + private void HandleCreateNewAppFromModal() + { + var name = NewAppTraveller == "Custom" ? NewAppCustomTraveller : NewAppTraveller; + if (string.IsNullOrWhiteSpace(name)) + { + TriggerFeedback("Please enter a traveller name."); + return; + } + + var initials = string.Concat(name.Split(' ').Select(n => n.Length > 0 ? n[0].ToString() : string.Empty)).ToUpper(); + if (string.IsNullOrEmpty(initials)) initials = "TR"; + + var newId = (VisaApplications.Any() ? VisaApplications.Max(a => a.Id) : 0) + 1; + bool isUS = NewAppDestination == "USA"; + + var newApp = new VisaApplication + { + Id = newId, + Traveller = name, + Avatar = initials, + Country = NewAppDestination, + Type = NewAppType, + Stage = isUS ? "Docs required" : "Docs uploaded", + TravelDate = NewAppDate.ToString("yyyy-MM-dd"), + Steps = new List + { + new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }, + new VisaStep + { + Label = "Step 2, document checklist", + Sub = isUS ? "Passport, photo, US visa fee, 3-months bank statement" : "Passport copy, photos, travel insurance, hotel bookings", + Status = isUS ? "2 of 4 uploaded" : "4 of 4 uploaded", + StatusClass = isUS ? "bg-[#fde5e8] text-[#dc2626] border border-[#f8b4b4]" : "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" + }, + new VisaStep + { + Label = "Step 3, fee payment", + Sub = "Settled via SAQA lodge card", + Status = isUS ? "Pending" : "Paid R 2,250", + StatusClass = isUS ? "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" : "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" + }, + new VisaStep + { + Label = "Step 4, consulate appointment", + Sub = isUS ? "Awaiting DS-160 and payment" : "VFS Global appointment confirmed", + Status = isUS ? "Not booked" : "Booked", + StatusClass = isUS ? "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" : "bg-[#e2eefb] text-[#0066b3] border border-[#b4d3f5]" + }, + new VisaStep { Label = "Step 5, decision", Sub = "Visa sticker or eVisa approval pending review", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" } + } + }; + + VisaApplications.Insert(0, newApp); + ActiveWorkflowId = newId; + ActiveAppsCount++; + if (isUS) + { + DocumentsPending++; + } + + TriggerFeedback($"New application created successfully for {name} ({NewAppDestination})."); + ShowNewAppModal = false; + NewAppCustomTraveller = string.Empty; + } + + private void HandleOpenUploadDocs(int appId) + { + SelectedUploadAppId = appId; + UploadSelectedFiles.Clear(); + ShowUploadDocsModal = true; + } + + private void TriggerMockFileUpload() + { + // Simulates double-clicking or uploading typical checklist files in Blazor page + UploadSelectedFiles = new List { "signed_visa_form.pdf", "stamped_bank_statements_3_months.pdf" }; + StateHasChanged(); + } + + private void HandleConfirmUploadDocs() + { + if (SelectedUploadAppId == null) return; + var targetId = SelectedUploadAppId.Value; + + var app = VisaApplications.FirstOrDefault(a => a.Id == targetId); + if (app != null) + { + app.Stage = "Docs uploaded"; + if (app.Steps.Count >= 3) + { + app.Steps[1].Status = "4 of 4 uploaded"; + app.Steps[1].StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]"; + + app.Steps[2].Status = "Paid R 2,840"; + app.Steps[2].StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]"; + } + + DocumentsPending = System.Math.Max(0, DocumentsPending - 1); + TriggerFeedback($"Documents uploaded successfully for {app.Traveller}. Application is now complete for submission!"); + } + + ShowUploadDocsModal = false; + SelectedUploadAppId = null; + } + + // Modal Helpers + private void OpenLibraryTab(string tabName) + { + ActiveLibraryTab = tabName; + ShowVisaRulesModal = true; + } + + private void SetActiveLibraryTab(string tabName) + { + ActiveLibraryTab = tabName; + } + + private void CloseVisaRulesModal() + { + ShowVisaRulesModal = false; + } + + private void OpenNewAppModal() + { + ShowNewAppModal = true; + } + + private void CloseNewAppModal() + { + ShowNewAppModal = false; + } + + private void CloseUploadDocsModal() + { + ShowUploadDocsModal = false; + } + + // Fee calculations + private (int embassyFee, int serviceFee, int total) GetCalculatedFees(string country, string type) + { + int embassyFee = 2450; + int serviceFee = 450; + + if (country == "United Kingdom") + { + embassyFee = type == "Transit" ? 950 : type == "Tourist" ? 2150 : 2840; + } + else if (country == "Schengen Area") + { + embassyFee = type == "Transit" ? 820 : type == "Tourist" ? 1820 : 2200; + } + else if (country == "USA") + { + embassyFee = type == "Transit" ? 1950 : type == "Tourist" ? 3420 : 3650; + } + else if (country == "Kenya") + { + embassyFee = 540; + serviceFee = 150; + } + else if (country == "UAE") + { + embassyFee = type == "Transit" ? 1150 : type == "Tourist" ? 1650 : 1850; + } + + return (embassyFee, serviceFee, embassyFee + serviceFee); + } + + private void HandleAuthorizeLodgeCardFee(int totalFee) + { + TriggerFeedback($"Authorized R {totalFee.ToString("N0")} on Lodge Card for {CalcCountry} ({CalcVisaType} Visa)."); + } + + // Badge CSS Mapper helper + private string GetStageBadgeClass(string stage) + { + return stage switch + { + "Approved" => "bg-[#e3f6ed] text-[#22a06b]", + "Docs required" => "bg-[#fde5e8] text-[#dc2626]", + "Docs uploaded" => "bg-[#e2eefb] text-[#0066b3]", + _ => "bg-[#fff4dd] text-[#b06b00]" + }; + } + + // Toast/Feedback Trigger + private void TriggerFeedback(string message) + { + AlertFeedback = message; + // Simulate simple asynchronous auto-dismiss in C# Blazor + _ = DismissAlertAfterDelay(); + } + + private async System.Threading.Tasks.Task DismissAlertAfterDelay() + { + await System.Threading.Tasks.Task.Delay(5000); + AlertFeedback = string.Empty; + StateHasChanged(); + } } diff --git a/Tmt.SaqaTravel/Components/Pages/Weather.razor b/Tmt.SaqaTravel/Components/Pages/Weather.razor deleted file mode 100644 index faa4c54..0000000 --- a/Tmt.SaqaTravel/Components/Pages/Weather.razor +++ /dev/null @@ -1,66 +0,0 @@ -@page "/weather" -@attribute [StreamRendering] - -Weather - -

Weather

- -

This component demonstrates showing data.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ -
- - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - // Simulate asynchronous loading to demonstrate streaming rendering - await Task.Delay(500); - - var startDate = DateOnly.FromDateTime(DateTime.Now); - var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; - forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = startDate.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = summaries[Random.Shared.Next(summaries.Length)] - }).ToArray(); - } - - private class WeatherForecast - { - public DateOnly Date { get; set; } - public int TemperatureC { get; set; } - public string? Summary { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } -} \ No newline at end of file diff --git a/Tmt.SaqaTravel/wwwroot/app.min.css b/Tmt.SaqaTravel/wwwroot/app.min.css index 40ca835..63de688 100644 --- a/Tmt.SaqaTravel/wwwroot/app.min.css +++ b/Tmt.SaqaTravel/wwwroot/app.min.css @@ -7,39 +7,100 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-red-50: oklch(97.1% 0.013 17.38); + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-amber-600: oklch(66.6% 0.179 58.318); + --color-yellow-500: oklch(79.5% 0.184 86.047); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-600: oklch(62.7% 0.194 149.214); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-emerald-50: oklch(97.9% 0.021 166.113); + --color-emerald-100: oklch(95% 0.052 163.051); + --color-emerald-200: oklch(90.5% 0.093 164.15); + --color-emerald-500: oklch(69.6% 0.17 162.48); + --color-emerald-600: oklch(59.6% 0.145 163.225); + --color-emerald-700: oklch(50.8% 0.118 165.612); + --color-emerald-800: oklch(43.2% 0.095 166.913); + --color-sky-600: oklch(58.8% 0.158 241.966); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-200: oklch(88.2% 0.059 254.128); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-700: oklch(48.8% 0.243 264.376); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-blue-950: oklch(28.2% 0.091 267.935); + --color-slate-50: oklch(98.4% 0.003 247.858); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); --color-gray-50: oklch(98.5% 0.002 247.839); --color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-200: oklch(92.8% 0.006 264.531); --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); --color-gray-900: oklch(21% 0.034 264.665); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-2xl: 42rem; + --container-7xl: 80rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25 / 1.875); + --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; + --font-weight-extrabold: 800; + --font-weight-black: 900; --tracking-tight: -0.025em; --tracking-wide: 0.025em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; --leading-tight: 1.25; --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --radius-xs: 0.125rem; + --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; + --radius-2xl: 1rem; --ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --animate-spin: spin 1s linear infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --animate-bounce: bounce 1s infinite; + --blur-xs: 4px; + --blur-sm: 8px; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); @@ -213,21 +274,45 @@ .inset-0 { inset: 0; } + .-top-1 { + top: calc(var(--spacing) * -1); + } .top-0 { top: 0; } + .top-1 { + top: var(--spacing); + } .top-2 { top: calc(var(--spacing) * 2); } + .top-2\.5 { + top: calc(var(--spacing) * 2.5); + } + .top-4 { + top: calc(var(--spacing) * 4); + } + .-right-1\.5 { + right: calc(var(--spacing) * -1.5); + } + .-right-8 { + right: calc(var(--spacing) * -8); + } .right-0 { right: 0; } .right-2 { right: calc(var(--spacing) * 2); } + .right-2\.5 { + right: calc(var(--spacing) * 2.5); + } .right-3 { right: calc(var(--spacing) * 3); } + .-bottom-8 { + bottom: calc(var(--spacing) * -8); + } .bottom-0 { bottom: 0; } @@ -237,6 +322,9 @@ .left-0 { left: 0; } + .left-1\/2 { + left: calc(1 / 2 * 100%); + } .left-12 { left: calc(var(--spacing) * 12); } @@ -246,6 +334,12 @@ .z-20 { z-index: 20; } + .z-30 { + z-index: 30; + } + .z-50 { + z-index: 50; + } .z-\[1000\] { z-index: 1000; } @@ -258,6 +352,9 @@ .mx-auto { margin-inline: auto; } + .my-5 { + margin-block: calc(var(--spacing) * 5); + } .my-\[20vh\] { margin-block: 20vh; } @@ -267,24 +364,48 @@ .mt-1 { margin-top: var(--spacing); } + .mt-1\.5 { + margin-top: calc(var(--spacing) * 1.5); + } .mt-2 { margin-top: calc(var(--spacing) * 2); } + .mt-2\.5 { + margin-top: calc(var(--spacing) * 2.5); + } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } .mt-4 { margin-top: calc(var(--spacing) * 4); } + .mt-5 { + margin-top: calc(var(--spacing) * 5); + } + .mt-6 { + margin-top: calc(var(--spacing) * 6); + } + .mt-\[2px\] { + margin-top: 2px; + } .mt-auto { margin-top: auto; } .mr-3 { margin-right: calc(var(--spacing) * 3); } + .-mb-px { + margin-bottom: -1px; + } .mb-0\.5 { margin-bottom: calc(var(--spacing) * 0.5); } .mb-1 { margin-bottom: var(--spacing); } + .mb-1\.5 { + margin-bottom: calc(var(--spacing) * 1.5); + } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } @@ -303,12 +424,18 @@ .mb-6 { margin-bottom: calc(var(--spacing) * 6); } + .mb-\[18px\] { + margin-bottom: 18px; + } .mb-\[22px\] { margin-bottom: 22px; } .-ml-4 { margin-left: calc(var(--spacing) * -4); } + .ml-1\.5 { + margin-left: calc(var(--spacing) * 1.5); + } .ml-12 { margin-left: calc(var(--spacing) * 12); } @@ -327,9 +454,15 @@ .inline-block { display: inline-block; } + .inline-flex { + display: inline-flex; + } .h-0 { height: 0; } + .h-1 { + height: var(--spacing); + } .h-1\.5 { height: calc(var(--spacing) * 1.5); } @@ -339,12 +472,21 @@ .h-2\.5 { height: calc(var(--spacing) * 2.5); } + .h-3 { + height: calc(var(--spacing) * 3); + } .h-3\.5 { height: calc(var(--spacing) * 3.5); } .h-4 { height: calc(var(--spacing) * 4); } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); + } .h-7 { height: calc(var(--spacing) * 7); } @@ -354,29 +496,35 @@ .h-9 { height: calc(var(--spacing) * 9); } + .h-10 { + height: calc(var(--spacing) * 10); + } .h-20 { height: calc(var(--spacing) * 20); } + .h-24 { + height: calc(var(--spacing) * 24); + } .h-\[18px\] { height: 18px; } .h-\[38px\] { height: 38px; } + .h-\[39px\] { + height: 39px; + } .h-\[52px\] { height: 52px; } .h-\[54px\] { height: 54px; } - .h-\[56px\] { - height: 56px; + .h-\[220px\] { + height: 220px; } - .h-\[160px\] { - height: 160px; - } - .h-\[180px\] { - height: 180px; + .h-\[280px\] { + height: 280px; } .h-\[calc\(100vh-130px\)\] { height: calc(100vh - 130px); @@ -384,12 +532,33 @@ .h-full { height: 100%; } + .max-h-\[90vh\] { + max-height: 90vh; + } + .max-h-\[220px\] { + max-height: 220px; + } + .max-h-\[360px\] { + max-height: 360px; + } + .max-h-\[400px\] { + max-height: 400px; + } .min-h-\[72px\] { min-height: 72px; } .min-h-\[260px\] { min-height: 260px; } + .min-h-\[350px\] { + min-height: 350px; + } + .min-h-\[500px\] { + min-height: 500px; + } + .min-h-\[580px\] { + min-height: 580px; + } .min-h-screen { min-height: 100vh; } @@ -402,12 +571,24 @@ .w-2\.5 { width: calc(var(--spacing) * 2.5); } + .w-3 { + width: calc(var(--spacing) * 3); + } .w-3\.5 { width: calc(var(--spacing) * 3.5); } + .w-3\/4 { + width: calc(3 / 4 * 100%); + } .w-4 { width: calc(var(--spacing) * 4); } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); + } .w-7 { width: calc(var(--spacing) * 7); } @@ -417,9 +598,21 @@ .w-9 { width: calc(var(--spacing) * 9); } + .w-10 { + width: calc(var(--spacing) * 10); + } .w-20 { width: calc(var(--spacing) * 20); } + .w-24 { + width: calc(var(--spacing) * 24); + } + .w-28 { + width: calc(var(--spacing) * 28); + } + .w-48 { + width: calc(var(--spacing) * 48); + } .w-64 { width: calc(var(--spacing) * 64); } @@ -435,27 +628,51 @@ .w-auto { width: auto; } + .w-fit { + width: fit-content; + } .w-full { width: 100%; } + .max-w-2xl { + max-width: var(--container-2xl); + } + .max-w-\[85\%\] { + max-width: 85%; + } + .max-w-\[140px\] { + max-width: 140px; + } + .max-w-\[150px\] { + max-width: 150px; + } .max-w-\[170px\] { max-width: 170px; } .max-w-\[180px\] { max-width: 180px; } + .max-w-\[200px\] { + max-width: 200px; + } + .max-w-\[340px\] { + max-width: 340px; + } .max-w-\[1600px\] { max-width: 1600px; } + .max-w-lg { + max-width: var(--container-lg); + } + .max-w-md { + max-width: var(--container-md); + } .min-w-0 { min-width: 0; } .min-w-\[600px\] { min-width: 600px; } - .min-w-full { - min-width: 100%; - } .flex-1 { flex: 1; } @@ -468,27 +685,52 @@ .border-collapse { border-collapse: collapse; } + .-translate-x-1\/2 { + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } .animate-\[ping_1\.5s_cubic-bezier\(0\,0\.2\,0\.8\,1\)_infinite\] { animation: ping 1.5s cubic-bezier(0,0.2,0.8,1) infinite; } + .animate-bounce { + animation: var(--animate-bounce); + } + .animate-pulse { + animation: var(--animate-pulse); + } + .animate-spin { + animation: var(--animate-spin); + } + .cursor-not-allowed { + cursor: not-allowed; + } .cursor-pointer { cursor: pointer; } + .resize-none { + resize: none; + } .appearance-none { appearance: none; } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } + .grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } .flex-col { flex-direction: column; } .flex-wrap { flex-wrap: wrap; } + .items-baseline { + align-items: baseline; + } .items-center { align-items: center; } @@ -498,12 +740,24 @@ .items-start { align-items: flex-start; } + .justify-around { + justify-content: space-around; + } .justify-between { justify-content: space-between; } .justify-center { justify-content: center; } + .justify-end { + justify-content: flex-end; + } + .gap-0\.5 { + gap: calc(var(--spacing) * 0.5); + } + .gap-1 { + gap: var(--spacing); + } .gap-1\.5 { gap: calc(var(--spacing) * 1.5); } @@ -528,6 +782,23 @@ .gap-8 { gap: calc(var(--spacing) * 8); } + .gap-\[6px\] { + gap: 6px; + } + .space-y-1 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(var(--spacing) * var(--tw-space-y-reverse)); + margin-block-end: calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-1\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-2 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -535,6 +806,27 @@ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-2\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-3 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-3\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3.5) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-4 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -542,6 +834,13 @@ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-6 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); + } + } .divide-y { :where(& > :not(:last-child)) { --tw-divide-y-reverse: 0; @@ -551,11 +850,16 @@ border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); } } - .divide-gray-200 { + .divide-\[\#e4eaf2\] { :where(& > :not(:last-child)) { - border-color: var(--color-gray-200); + border-color: #e4eaf2; } } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .overflow-hidden { overflow: hidden; } @@ -565,12 +869,27 @@ .overflow-x-auto { overflow-x: auto; } + .overflow-y-auto { + overflow-y: auto; + } .rounded { border-radius: 0.25rem; } + .rounded-2xl { + border-radius: var(--radius-2xl); + } .rounded-\[10px\] { border-radius: 10px; } + .rounded-\[14px\] { + border-radius: 14px; + } + .rounded-\[32px\] { + border-radius: 32px; + } + .rounded-\[42px\] { + border-radius: 42px; + } .rounded-full { border-radius: calc(infinity * 1px); } @@ -580,9 +899,29 @@ .rounded-md { border-radius: var(--radius-md); } + .rounded-sm { + border-radius: var(--radius-sm); + } .rounded-xl { border-radius: var(--radius-xl); } + .rounded-xs { + border-radius: var(--radius-xs); + } + .rounded-r-lg { + border-top-right-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); + } + .rounded-b-\[32px\] { + border-bottom-right-radius: 32px; + border-bottom-left-radius: 32px; + } + .rounded-br-none { + border-bottom-right-radius: 0; + } + .rounded-bl-none { + border-bottom-left-radius: 0; + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -595,14 +934,30 @@ border-style: var(--tw-border-style); border-width: 2px; } + .border-4 { + border-style: var(--tw-border-style); + border-width: 4px; + } .border-\[3px\] { border-style: var(--tw-border-style); border-width: 3px; } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } .border-b { border-bottom-style: var(--tw-border-style); border-bottom-width: 1px; } + .border-b-2 { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 2px; + } + .border-l-4 { + border-left-style: var(--tw-border-style); + border-left-width: 4px; + } .border-l-\[3px\] { border-left-style: var(--tw-border-style); border-left-width: 3px; @@ -615,18 +970,120 @@ --tw-border-style: none; border-style: none; } + .border-\[\#00a3a1\] { + border-color: #00a3a1; + } + .border-\[\#003b71\] { + border-color: #003b71; + } + .border-\[\#22a06b\] { + border-color: #22a06b; + } + .border-\[\#0066b3\] { + border-color: #0066b3; + } + .border-\[\#0066b3\]\/30 { + border-color: color-mix(in oklab, #0066b3 30%, transparent); + } .border-\[\#0087ff\] { border-color: #0087ff; } + .border-\[\#222\] { + border-color: #222; + } + .border-\[\#a1dfc2\] { + border-color: #a1dfc2; + } + .border-\[\#b4d3f5\] { + border-color: #b4d3f5; + } + .border-\[\#c8d3e1\] { + border-color: #c8d3e1; + } + .border-\[\#d2f1df\] { + border-color: #d2f1df; + } + .border-\[\#dc2626\] { + border-color: #dc2626; + } .border-\[\#e4eaf2\] { border-color: #e4eaf2; } + .border-\[\#e4eaf2\]\/40 { + border-color: color-mix(in oklab, #e4eaf2 40%, transparent); + } .border-\[\#eef2f8\] { border-color: #eef2f8; } + .border-\[\#f4a300\] { + border-color: #f4a300; + } + .border-\[\#f4f7fb\] { + border-color: #f4f7fb; + } + .border-\[\#f8b4b4\] { + border-color: #f8b4b4; + } + .border-\[\#fbd897\] { + border-color: #fbd897; + } + .border-\[\#fde5e8\] { + border-color: #fde5e8; + } + .border-blue-100 { + border-color: var(--color-blue-100); + } + .border-blue-800\/40 { + border-color: color-mix(in srgb, oklch(42.4% 0.199 265.638) 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-blue-800) 40%, transparent); + } + } + .border-current { + border-color: currentcolor; + } + .border-emerald-100 { + border-color: var(--color-emerald-100); + } + .border-emerald-200 { + border-color: var(--color-emerald-200); + } + .border-gray-100 { + border-color: var(--color-gray-100); + } + .border-gray-200\/50 { + border-color: color-mix(in srgb, oklch(92.8% 0.006 264.531) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-gray-200) 50%, transparent); + } + } .border-gray-300 { border-color: var(--color-gray-300); } + .border-gray-400 { + border-color: var(--color-gray-400); + } + .border-gray-700 { + border-color: var(--color-gray-700); + } + .border-gray-800 { + border-color: var(--color-gray-800); + } + .border-gray-900 { + border-color: var(--color-gray-900); + } + .border-green-500\/30 { + border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-green-500) 30%, transparent); + } + } + .border-red-100 { + border-color: var(--color-red-100); + } + .border-red-500 { + border-color: var(--color-red-500); + } .border-transparent { border-color: transparent; } @@ -639,27 +1096,60 @@ border-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } + .border-t-transparent { + border-top-color: transparent; + } .bg-\[\#00a3a1\] { background-color: #00a3a1; } + .bg-\[\#1f2a3a\] { + background-color: #1f2a3a; + } + .bg-\[\#1f2a3a\]\/40 { + background-color: color-mix(in oklab, #1f2a3a 40%, transparent); + } + .bg-\[\#1f2a3a\]\/60 { + background-color: color-mix(in oklab, #1f2a3a 60%, transparent); + } .bg-\[\#003b71\] { background-color: #003b71; } + .bg-\[\#003b71\]\/30 { + background-color: color-mix(in oklab, #003b71 30%, transparent); + } .bg-\[\#6b9ed2\] { background-color: #6b9ed2; } + .bg-\[\#8a96a8\]\/50 { + background-color: color-mix(in oklab, #8a96a8 50%, transparent); + } .bg-\[\#22a06b\] { background-color: #22a06b; } .bg-\[\#0066b3\] { background-color: #0066b3; } + .bg-\[\#111\] { + background-color: #111; + } + .bg-\[\#222\] { + background-color: #222; + } + .bg-\[\#101827\] { + background-color: #101827; + } .bg-\[\#c0d4e6\] { background-color: #c0d4e6; } .bg-\[\#c8102e\] { background-color: #c8102e; } + .bg-\[\#dc2626\] { + background-color: #dc2626; + } + .bg-\[\#e0f7f6\] { + background-color: #e0f7f6; + } .bg-\[\#e2eefb\] { background-color: #e2eefb; } @@ -669,49 +1159,161 @@ .bg-\[\#e8f4f4\] { background-color: #e8f4f4; } + .bg-\[\#effaf3\] { + background-color: #effaf3; + } + .bg-\[\#f0f4f8\] { + background-color: #f0f4f8; + } + .bg-\[\#f3f8fd\] { + background-color: #f3f8fd; + } .bg-\[\#f4a300\] { background-color: #f4a300; } .bg-\[\#f4f7fb\] { background-color: #f4f7fb; } + .bg-\[\#f4f7fb\]\/40 { + background-color: color-mix(in oklab, #f4f7fb 40%, transparent); + } + .bg-\[\#f4f7fb\]\/50 { + background-color: color-mix(in oklab, #f4f7fb 50%, transparent); + } + .bg-\[\#f4f8fc\]\/50 { + background-color: color-mix(in oklab, #f4f8fc 50%, transparent); + } + .bg-\[\#f6fafe\] { + background-color: #f6fafe; + } + .bg-\[\#fafcfe\] { + background-color: #fafcfe; + } .bg-\[\#fde5e8\] { background-color: #fde5e8; } + .bg-\[\#fdeef0\] { + background-color: #fdeef0; + } .bg-\[\#fff4dd\] { background-color: #fff4dd; } + .bg-\[\#fff9ec\] { + background-color: #fff9ec; + } .bg-\[\#ffffe0\] { background-color: #ffffe0; } .bg-\[\#ffffff14\] { background-color: #ffffff14; } - .bg-blue-600 { - background-color: var(--color-blue-600); + .bg-black\/30 { + background-color: color-mix(in srgb, #000 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 30%, transparent); + } } - .bg-gray-50 { - background-color: var(--color-gray-50); + .bg-black\/50 { + background-color: color-mix(in srgb, #000 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 50%, transparent); + } + } + .bg-blue-500 { + background-color: var(--color-blue-500); + } + .bg-blue-950\/60 { + background-color: color-mix(in srgb, oklch(28.2% 0.091 267.935) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-blue-950) 60%, transparent); + } + } + .bg-emerald-50 { + background-color: var(--color-emerald-50); } .bg-gray-100 { background-color: var(--color-gray-100); } + .bg-gray-800 { + background-color: var(--color-gray-800); + } + .bg-gray-900 { + background-color: var(--color-gray-900); + } + .bg-gray-900\/80 { + background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-gray-900) 80%, transparent); + } + } + .bg-green-100 { + background-color: var(--color-green-100); + } + .bg-green-500 { + background-color: var(--color-green-500); + } + .bg-green-500\/20 { + background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-green-500) 20%, transparent); + } + } + .bg-green-600 { + background-color: var(--color-green-600); + } + .bg-red-50 { + background-color: var(--color-red-50); + } + .bg-red-100 { + background-color: var(--color-red-100); + } + .bg-red-600 { + background-color: var(--color-red-600); + } + .bg-slate-50 { + background-color: var(--color-slate-50); + } + .bg-slate-100 { + background-color: var(--color-slate-100); + } .bg-transparent { background-color: transparent; } .bg-white { background-color: var(--color-white); } + .bg-white\/5 { + background-color: color-mix(in srgb, #fff 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 5%, transparent); + } + } .bg-white\/10 { background-color: color-mix(in srgb, #fff 10%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } + .bg-white\/20 { + background-color: color-mix(in srgb, #fff 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 20%, transparent); + } + } + .bg-white\/70 { + background-color: color-mix(in srgb, #fff 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 70%, transparent); + } + } .bg-gradient-to-br { --tw-gradient-position: to bottom right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } + .bg-gradient-to-tr { + --tw-gradient-position: to top right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } .bg-\[url\(\'data\:image\/svg\+xml\,\%3csvg_xmlns\=\%27http\:\/\/www\.w3\.org\/2000\/svg\%27_viewBox\=\%270_0_24_24\%27_fill\=\%27none\%27_stroke\=\%27currentColor\%27_stroke-width\=\%272\%27\%3e\%3cline_x1\=\%273\%27_y1\=\%276\%27_x2\=\%2721\%27_y2\=\%276\%27\/\%3e\%3cline_x1\=\%273\%27_y1\=\%2712\%27_x2\=\%2721\%27_y2\=\%2712\%27\/\%3e\%3cline_x1\=\%273\%27_y1\=\%2718\%27_x2\=\%2721\%27_y2\=\%2718\%27\/\%3e\%3c\/svg\%3e\'\)\] { background-image: url('data:image/svg+xml,%3csvg_xmlns=%27http://www.w3.org/2000/svg%27_viewBox=%270_0_24_24%27_fill=%27none%27_stroke=%27currentColor%27_stroke-width=%272%27%3e%3cline_x1=%273%27_y1=%276%27_x2=%2721%27_y2=%276%27/%3e%3cline_x1=%273%27_y1=%2712%27_x2=%2721%27_y2=%2712%27/%3e%3cline_x1=%273%27_y1=%2718%27_x2=%2721%27_y2=%2718%27/%3e%3c/svg%3e'); } @@ -719,10 +1321,19 @@ --tw-gradient-from: #00a3a1; --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } + .from-\[\#003b71\] { + --tw-gradient-from: #003b71; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } .from-\[\#0066b3\] { --tw-gradient-from: #0066b3; --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } + .via-\[\#0066b3\] { + --tw-gradient-via: #0066b3; + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } .to-\[\#00a3a1\] { --tw-gradient-to: #00a3a1; --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -740,15 +1351,33 @@ .bg-no-repeat { background-repeat: no-repeat; } + .fill-current { + fill: currentcolor; + } .object-contain { object-fit: contain; } .p-0 { padding: 0; } + .p-0\.5 { + padding: calc(var(--spacing) * 0.5); + } + .p-1\.5 { + padding: calc(var(--spacing) * 1.5); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-2\.5 { + padding: calc(var(--spacing) * 2.5); + } .p-3 { padding: calc(var(--spacing) * 3); } + .p-3\.5 { + padding: calc(var(--spacing) * 3.5); + } .p-4 { padding: calc(var(--spacing) * 4); } @@ -761,12 +1390,21 @@ .p-8 { padding: calc(var(--spacing) * 8); } + .p-\[12px\] { + padding: 12px; + } .p-\[18px\] { padding: 18px; } .px-1 { padding-inline: var(--spacing); } + .px-1\.5 { + padding-inline: calc(var(--spacing) * 1.5); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } .px-2\.5 { padding-inline: calc(var(--spacing) * 2.5); } @@ -779,6 +1417,9 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } .px-6 { padding-inline: calc(var(--spacing) * 6); } @@ -788,12 +1429,18 @@ .px-\[7px\] { padding-inline: 7px; } + .px-\[10px\] { + padding-inline: 10px; + } .px-\[14px\] { padding-inline: 14px; } .px-\[18px\] { padding-inline: 18px; } + .py-0\.5 { + padding-block: calc(var(--spacing) * 0.5); + } .py-1 { padding-block: var(--spacing); } @@ -812,39 +1459,84 @@ .py-4 { padding-block: calc(var(--spacing) * 4); } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } .py-\[1px\] { padding-block: 1px; } .py-\[3px\] { padding-block: 3px; } + .py-\[9px\] { + padding-block: 9px; + } + .py-\[10px\] { + padding-block: 10px; + } + .pt-1 { + padding-top: var(--spacing); + } + .pt-1\.5 { + padding-top: calc(var(--spacing) * 1.5); + } .pt-2 { padding-top: calc(var(--spacing) * 2); } + .pt-3 { + padding-top: calc(var(--spacing) * 3); + } + .pt-3\.5 { + padding-top: calc(var(--spacing) * 3.5); + } .pt-4 { padding-top: calc(var(--spacing) * 4); } .pt-5 { padding-top: calc(var(--spacing) * 5); } + .pt-7 { + padding-top: calc(var(--spacing) * 7); + } .pt-9 { padding-top: calc(var(--spacing) * 9); } + .pr-0\.5 { + padding-right: calc(var(--spacing) * 0.5); + } + .pr-1 { + padding-right: var(--spacing); + } .pr-2 { padding-right: calc(var(--spacing) * 2); } .pr-3\.5 { padding-right: calc(var(--spacing) * 3.5); } + .pb-1 { + padding-bottom: var(--spacing); + } + .pb-1\.5 { + padding-bottom: calc(var(--spacing) * 1.5); + } .pb-2 { padding-bottom: calc(var(--spacing) * 2); } + .pb-2\.5 { + padding-bottom: calc(var(--spacing) * 2.5); + } .pb-3 { padding-bottom: calc(var(--spacing) * 3); } .pb-3\.5 { padding-bottom: calc(var(--spacing) * 3.5); } + .pb-4 { + padding-bottom: calc(var(--spacing) * 4); + } + .pb-5 { + padding-bottom: calc(var(--spacing) * 5); + } .pb-6 { padding-bottom: calc(var(--spacing) * 6); } @@ -860,10 +1552,30 @@ .text-left { text-align: left; } + .text-right { + text-align: right; + } + .align-middle { + vertical-align: middle; + } + .font-mono { + font-family: var(--font-mono); + } + .font-sans { + font-family: var(--font-sans); + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } .text-3xl { font-size: var(--text-3xl); line-height: var(--tw-leading, var(--text-3xl--line-height)); } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); @@ -876,6 +1588,19 @@ font-size: var(--text-xl); line-height: var(--tw-leading, var(--text-xl--line-height)); } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .text-\[7px\] { + font-size: 7px; + } + .text-\[8px\] { + font-size: 8px; + } + .text-\[9px\] { + font-size: 9px; + } .text-\[10px\] { font-size: 10px; } @@ -900,6 +1625,9 @@ .text-\[18px\] { font-size: 18px; } + .text-\[20px\] { + font-size: 20px; + } .text-\[22px\] { font-size: 22px; } @@ -910,10 +1638,22 @@ --tw-leading: 1.2; line-height: 1.2; } + .leading-\[1\.45\] { + --tw-leading: 1.45; + line-height: 1.45; + } .leading-none { --tw-leading: 1; line-height: 1; } + .leading-normal { + --tw-leading: var(--leading-normal); + line-height: var(--leading-normal); + } + .leading-relaxed { + --tw-leading: var(--leading-relaxed); + line-height: var(--leading-relaxed); + } .leading-snug { --tw-leading: var(--leading-snug); line-height: var(--leading-snug); @@ -922,22 +1662,42 @@ --tw-leading: var(--leading-tight); line-height: var(--leading-tight); } + .font-black { + --tw-font-weight: var(--font-weight-black); + font-weight: var(--font-weight-black); + } .font-bold { --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); } + .font-extrabold { + --tw-font-weight: var(--font-weight-extrabold); + font-weight: var(--font-weight-extrabold); + } .font-medium { --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } + .font-normal { + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + } .font-semibold { --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); } + .tracking-\[\.06em\] { + --tw-tracking: .06em; + letter-spacing: .06em; + } .tracking-\[0\.02em\] { --tw-tracking: 0.02em; letter-spacing: 0.02em; } + .tracking-\[0\.04em\] { + --tw-tracking: 0.04em; + letter-spacing: 0.04em; + } .tracking-\[0\.06em\] { --tw-tracking: 0.06em; letter-spacing: 0.06em; @@ -950,6 +1710,14 @@ --tw-tracking: 0.12em; letter-spacing: 0.12em; } + .tracking-\[3px\] { + --tw-tracking: 3px; + letter-spacing: 3px; + } + .tracking-\[4px\] { + --tw-tracking: 4px; + letter-spacing: 4px; + } .tracking-tight { --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); @@ -958,6 +1726,14 @@ --tw-tracking: var(--tracking-wide); letter-spacing: var(--tracking-wide); } + .tracking-wider { + --tw-tracking: var(--tracking-wider); + letter-spacing: var(--tracking-wider); + } + .tracking-widest { + --tw-tracking: var(--tracking-widest); + letter-spacing: var(--tracking-widest); + } .whitespace-nowrap { white-space: nowrap; } @@ -967,9 +1743,15 @@ .text-\[\#00d4d1\] { color: #00d4d1; } + .text-\[\#1b8054\] { + color: #1b8054; + } .text-\[\#1f2a3a\] { color: #1f2a3a; } + .text-\[\#2b7250\] { + color: #2b7250; + } .text-\[\#003b71\] { color: #003b71; } @@ -988,6 +1770,9 @@ .text-\[\#0066b3\] { color: #0066b3; } + .text-\[\#101827\] { + color: #101827; + } .text-\[\#647892\] { color: #647892; } @@ -1012,21 +1797,93 @@ .text-\[\#dc2626\] { color: #dc2626; } + .text-\[\#f4a300\] { + color: #f4a300; + } + .text-amber-600 { + color: var(--color-amber-600); + } + .text-blue-100 { + color: var(--color-blue-100); + } + .text-blue-200 { + color: var(--color-blue-200); + } + .text-blue-300 { + color: var(--color-blue-300); + } + .text-blue-400 { + color: var(--color-blue-400); + } + .text-emerald-500 { + color: var(--color-emerald-500); + } + .text-emerald-600 { + color: var(--color-emerald-600); + } + .text-emerald-800 { + color: var(--color-emerald-800); + } + .text-gray-200 { + color: var(--color-gray-200); + } + .text-gray-300 { + color: var(--color-gray-300); + } + .text-gray-400 { + color: var(--color-gray-400); + } .text-gray-500 { color: var(--color-gray-500); } + .text-gray-600 { + color: var(--color-gray-600); + } .text-gray-700 { color: var(--color-gray-700); } - .text-gray-900 { - color: var(--color-gray-900); + .text-gray-800 { + color: var(--color-gray-800); + } + .text-green-400 { + color: var(--color-green-400); + } + .text-green-600 { + color: var(--color-green-600); + } + .text-green-700 { + color: var(--color-green-700); + } + .text-red-400 { + color: var(--color-red-400); + } + .text-red-500 { + color: var(--color-red-500); } .text-red-600 { color: var(--color-red-600); } + .text-red-700 { + color: var(--color-red-700); + } + .text-sky-600 { + color: var(--color-sky-600); + } + .text-slate-700 { + color: var(--color-slate-700); + } + .text-slate-800 { + color: var(--color-slate-800); + } .text-white { color: var(--color-white); } + .text-yellow-500 { + color: var(--color-yellow-500); + } + .normal-case { + text-transform: none; + } .uppercase { text-transform: uppercase; } @@ -1036,17 +1893,45 @@ .underline { text-decoration-line: underline; } + .antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } .opacity-0 { opacity: 0%; } + .opacity-55 { + opacity: 55%; + } + .opacity-75 { + opacity: 75%; + } + .opacity-80 { + opacity: 80%; + } + .opacity-85 { + opacity: 85%; + } + .shadow-2xl { + --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-\[0_-1px_2px_rgba\(0\,0\,0\,0\.2\)\] { --tw-shadow: 0 -1px 2px var(--tw-shadow-color, rgba(0,0,0,0.2)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-\[0_1px_2px_rgba\(15\,23\,42\,\.06\)\,_0_1px_3px_rgba\(15\,23\,42\,\.04\)\] { + --tw-shadow: 0 1px 2px var(--tw-shadow-color, rgba(15,23,42,.06)), 0 1px 3px var(--tw-shadow-color, rgba(15,23,42,.04)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-\[0_1px_2px_rgba\(15\,23\,42\,\.06\)\] { --tw-shadow: 0 1px 2px var(--tw-shadow-color, rgba(15,23,42,.06)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-\[0_1px_2px_rgba\(15\,23\,42\,0\.06\)\,0_1px_3px_rgba\(15\,23\,42\,0\.04\)\] { + --tw-shadow: 0 1px 2px var(--tw-shadow-color, rgba(15,23,42,0.06)), 0 1px 3px var(--tw-shadow-color, rgba(15,23,42,0.04)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-inner { --tw-shadow: inset 0 2px 4px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -1055,6 +1940,10 @@ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-sm { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -1063,12 +1952,39 @@ --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .ring-1 { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + .shadow-xs { + --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .ring-black { - --tw-ring-color: var(--color-black); + .brightness-75 { + --tw-brightness: brightness(75%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .contrast-75 { + --tw-contrast: contrast(75%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .grayscale { + --tw-grayscale: grayscale(100%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur-\[1px\] { + --tw-backdrop-blur: blur(1px); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .backdrop-blur-xs { + --tw-backdrop-blur: blur(var(--blur-xs)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); } .transition-all { transition-property: all; @@ -1132,6 +2048,10 @@ --tw-duration: 200ms; transition-duration: 200ms; } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } .duration-500 { --tw-duration: 500ms; transition-duration: 500ms; @@ -1171,6 +2091,69 @@ } } } + .group-hover\:bg-\[\#00a3a1\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #00a3a1; + } + } + } + .group-hover\:bg-\[\#22a06b\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #22a06b; + } + } + } + .group-hover\:bg-\[\#0066b3\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #0066b3; + } + } + } + .group-hover\:bg-\[\#f4a300\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #f4a300; + } + } + } + .group-hover\:text-\[\#00a3a1\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: #00a3a1; + } + } + } + .group-hover\:text-\[\#22a06b\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: #22a06b; + } + } + } + .group-hover\:text-\[\#0066b3\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: #0066b3; + } + } + } + .group-hover\:text-\[\#f4a300\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: #f4a300; + } + } + } + .group-hover\:text-white { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-white); + } + } + } .group-hover\:opacity-100 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -1196,6 +2179,144 @@ } } } + .before\:absolute { + &::before { + content: var(--tw-content); + position: absolute; + } + } + .before\:-top-10 { + &::before { + content: var(--tw-content); + top: calc(var(--spacing) * -10); + } + } + .before\:top-0 { + &::before { + content: var(--tw-content); + top: 0; + } + } + .before\:-right-10 { + &::before { + content: var(--tw-content); + right: calc(var(--spacing) * -10); + } + } + .before\:left-0 { + &::before { + content: var(--tw-content); + left: 0; + } + } + .before\:h-40 { + &::before { + content: var(--tw-content); + height: calc(var(--spacing) * 40); + } + } + .before\:h-full { + &::before { + content: var(--tw-content); + height: 100%; + } + } + .before\:w-1 { + &::before { + content: var(--tw-content); + width: var(--spacing); + } + } + .before\:w-40 { + &::before { + content: var(--tw-content); + width: calc(var(--spacing) * 40); + } + } + .before\:rounded-full { + &::before { + content: var(--tw-content); + border-radius: calc(infinity * 1px); + } + } + .before\:bg-\[\#00a3a1\] { + &::before { + content: var(--tw-content); + background-color: #00a3a1; + } + } + .before\:bg-\[\#22a06b\] { + &::before { + content: var(--tw-content); + background-color: #22a06b; + } + } + .before\:bg-\[\#0066b3\] { + &::before { + content: var(--tw-content); + background-color: #0066b3; + } + } + .before\:bg-\[\#dc2626\] { + &::before { + content: var(--tw-content); + background-color: #dc2626; + } + } + .before\:bg-\[\#f4a300\] { + &::before { + content: var(--tw-content); + background-color: #f4a300; + } + } + .before\:bg-gradient-to-br { + &::before { + content: var(--tw-content); + --tw-gradient-position: to bottom right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + } + .before\:from-white\/10 { + &::before { + content: var(--tw-content); + --tw-gradient-from: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-from: color-mix(in oklab, var(--color-white) 10%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + .before\:to-transparent { + &::before { + content: var(--tw-content); + --tw-gradient-to: transparent; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + .before\:content-\[\'\'\] { + &::before { + --tw-content: ''; + content: var(--tw-content); + } + } + .last\:border-0 { + &:last-child { + border-style: var(--tw-border-style); + border-width: 0px; + } + } + .last\:border-b-0 { + &:last-child { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 0px; + } + } + .last\:border-none { + &:last-child { + --tw-border-style: none; + border-style: none; + } + } .open\:animate-\[slideUp_1\.5s_cubic-bezier\(0\.05\,0\.89\,0\.25\,1\.02\)_0\.3s\,fadeIn_0\.5s_ease-in-out_0\.3s\] { &:is([open], :popover-open, :open) { animation: slideUp 1.5s cubic-bezier(0.05,0.89,0.25,1.02) 0.3s,fadeIn 0.5s ease-in-out 0.3s; @@ -1206,6 +2327,55 @@ border-color: #0066b3; } } + .hover\:border-\[\#00a3a1\] { + &:hover { + @media (hover: hover) { + border-color: #00a3a1; + } + } + } + .hover\:border-\[\#22a06b\] { + &:hover { + @media (hover: hover) { + border-color: #22a06b; + } + } + } + .hover\:border-\[\#0066b3\] { + &:hover { + @media (hover: hover) { + border-color: #0066b3; + } + } + } + .hover\:border-\[\#f4a300\] { + &:hover { + @media (hover: hover) { + border-color: #f4a300; + } + } + } + .hover\:border-gray-700 { + &:hover { + @media (hover: hover) { + border-color: var(--color-gray-700); + } + } + } + .hover\:border-red-200 { + &:hover { + @media (hover: hover) { + border-color: var(--color-red-200); + } + } + } + .hover\:bg-\[\#1b8054\] { + &:hover { + @media (hover: hover) { + background-color: #1b8054; + } + } + } .hover\:bg-\[\#003b71\] { &:hover { @media (hover: hover) { @@ -1220,6 +2390,41 @@ } } } + .hover\:bg-\[\#00264d\] { + &:hover { + @media (hover: hover) { + background-color: #00264d; + } + } + } + .hover\:bg-\[\#008280\] { + &:hover { + @media (hover: hover) { + background-color: #008280; + } + } + } + .hover\:bg-\[\#b4d3f5\] { + &:hover { + @media (hover: hover) { + background-color: #b4d3f5; + } + } + } + .hover\:bg-\[\#b91c1c\] { + &:hover { + @media (hover: hover) { + background-color: #b91c1c; + } + } + } + .hover\:bg-\[\#e2eefb\] { + &:hover { + @media (hover: hover) { + background-color: #e2eefb; + } + } + } .hover\:bg-\[\#e8eef7\] { &:hover { @media (hover: hover) { @@ -1227,6 +2432,20 @@ } } } + .hover\:bg-\[\#edfbf4\]\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #edfbf4 50%, transparent); + } + } + } + .hover\:bg-\[\#f2faf9\]\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #f2faf9 50%, transparent); + } + } + } .hover\:bg-\[\#f4f7fb\] { &:hover { @media (hover: hover) { @@ -1234,6 +2453,20 @@ } } } + .hover\:bg-\[\#f4f8fc\] { + &:hover { + @media (hover: hover) { + background-color: #f4f8fc; + } + } + } + .hover\:bg-\[\#f4f8fc\]\/40 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #f4f8fc 40%, transparent); + } + } + } .hover\:bg-\[\#fafcfe\] { &:hover { @media (hover: hover) { @@ -1241,6 +2474,13 @@ } } } + .hover\:bg-\[\#fffbf2\]\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #fffbf2 50%, transparent); + } + } + } .hover\:bg-\[\#ffffff2e\] { &:hover { @media (hover: hover) { @@ -1248,13 +2488,6 @@ } } } - .hover\:bg-blue-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-blue-700); - } - } - } .hover\:bg-gray-50 { &:hover { @media (hover: hover) { @@ -1269,6 +2502,41 @@ } } } + .hover\:bg-gray-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-200); + } + } + } + .hover\:bg-red-50 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-50); + } + } + } + .hover\:bg-red-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-100); + } + } + } + .hover\:bg-red-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-700); + } + } + } + .hover\:bg-slate-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-200); + } + } + } .hover\:bg-white\/5 { &:hover { @media (hover: hover) { @@ -1279,6 +2547,27 @@ } } } + .hover\:text-\[\#1f2a3a\] { + &:hover { + @media (hover: hover) { + color: #1f2a3a; + } + } + } + .hover\:text-emerald-700 { + &:hover { + @media (hover: hover) { + color: var(--color-emerald-700); + } + } + } + .hover\:text-red-600 { + &:hover { + @media (hover: hover) { + color: var(--color-red-600); + } + } + } .hover\:text-white { &:hover { @media (hover: hover) { @@ -1286,6 +2575,19 @@ } } } + .hover\:shadow-md { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + .focus\:border-\[\#0066b3\] { + &:focus { + border-color: #0066b3; + } + } .focus\:outline-none { &:focus { --tw-outline-style: none; @@ -1297,6 +2599,26 @@ background-color: #6b9ed2; } } + .disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } + } + .disabled\:cursor-not-allowed { + &:disabled { + cursor: not-allowed; + } + } + .disabled\:bg-gray-400 { + &:disabled { + background-color: var(--color-gray-400); + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } .sm\:flex { @media (width >= 40rem) { display: flex; @@ -1307,11 +2629,21 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } } + .sm\:grid-cols-3 { + @media (width >= 40rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } .sm\:flex-row { @media (width >= 40rem) { flex-direction: row; } } + .sm\:items-center { + @media (width >= 40rem) { + align-items: center; + } + } .sm\:items-end { @media (width >= 40rem) { align-items: flex-end; @@ -1352,6 +2684,11 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } } + .md\:grid-cols-3 { + @media (width >= 48rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } .md\:flex-row { @media (width >= 48rem) { flex-direction: row; @@ -1362,6 +2699,11 @@ align-items: center; } } + .md\:items-end { + @media (width >= 48rem) { + align-items: flex-end; + } + } .md\:gap-0 { @media (width >= 48rem) { gap: 0; @@ -1377,6 +2719,11 @@ padding: calc(var(--spacing) * 7); } } + .md\:p-8 { + @media (width >= 48rem) { + padding: calc(var(--spacing) * 8); + } + } .md\:px-7 { @media (width >= 48rem) { padding-inline: calc(var(--spacing) * 7); @@ -1392,16 +2739,61 @@ grid-column: span 1 / span 1; } } + .lg\:col-span-4 { + @media (width >= 64rem) { + grid-column: span 4 / span 4; + } + } + .lg\:col-span-5 { + @media (width >= 64rem) { + grid-column: span 5 / span 5; + } + } + .lg\:col-span-7 { + @media (width >= 64rem) { + grid-column: span 7 / span 7; + } + } + .lg\:col-span-8 { + @media (width >= 64rem) { + grid-column: span 8 / span 8; + } + } .lg\:flex { @media (width >= 64rem) { display: flex; } } + .lg\:grid-cols-2 { + @media (width >= 64rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .lg\:grid-cols-3 { + @media (width >= 64rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } .lg\:grid-cols-4 { @media (width >= 64rem) { grid-template-columns: repeat(4, minmax(0, 1fr)); } } + .lg\:grid-cols-6 { + @media (width >= 64rem) { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + } + .lg\:grid-cols-12 { + @media (width >= 64rem) { + grid-template-columns: repeat(12, minmax(0, 1fr)); + } + } + .lg\:grid-cols-\[1fr_1fr_1fr_1fr_auto\] { + @media (width >= 64rem) { + grid-template-columns: 1fr 1fr 1fr 1fr auto; + } + } .lg\:grid-cols-\[2fr_1fr\] { @media (width >= 64rem) { grid-template-columns: 2fr 1fr; @@ -1417,6 +2809,16 @@ align-items: center; } } + .lg\:items-end { + @media (width >= 64rem) { + align-items: flex-end; + } + } + .lg\:p-7 { + @media (width >= 64rem) { + padding: calc(var(--spacing) * 7); + } + } .lg\:pr-4 { @media (width >= 64rem) { padding-right: calc(var(--spacing) * 4); @@ -1451,6 +2853,21 @@ } } } +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-rotate-x { syntax: "*"; inherits: false; @@ -1605,6 +3022,95 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} @property --tw-duration { syntax: "*"; inherits: false; @@ -1613,15 +3119,43 @@ syntax: "*"; inherits: false; } +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} @keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; } } +@keyframes pulse { + 50% { + opacity: 0.5; + } +} +@keyframes bounce { + 0%, 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; --tw-rotate-x: initial; --tw-rotate-y: initial; --tw-rotate-z: initial; @@ -1656,8 +3190,31 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; --tw-duration: initial; --tw-ease: initial; + --tw-content: ""; } } }