Stable main pages
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveServer
|
||||
@using System
|
||||
@using System.Collections.Generic
|
||||
@using System.Linq
|
||||
@using System.Threading.Tasks
|
||||
|
||||
<PageTitle>Overview Dashboard</PageTitle>
|
||||
|
||||
@@ -19,7 +23,7 @@
|
||||
</svg>
|
||||
Export
|
||||
</button>
|
||||
<button class="flex items-center gap-1.5 px-[14px] py-2 bg-[#0066b3] text-white rounded-lg text-[13px] font-semibold hover:bg-[#003b71] transition-colors">
|
||||
<button @onclick="@(() => OpenBookingModal())" class="flex items-center gap-1.5 px-[14px] py-2 bg-[#0066b3] text-white rounded-lg text-[13px] font-semibold hover:bg-[#003b71] transition-colors cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
@@ -271,7 +275,7 @@
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="text-[15px] font-bold text-[#1f2a3a]">Recent Bookings</h3>
|
||||
<p class="text-[12px] text-[#8a96a8] mt-0.5">Last 5 confirmed trips</p>
|
||||
<p class="text-[12px] text-[#8a96a8] mt-0.5">Last confirmed trips</p>
|
||||
</div>
|
||||
<button class="px-[14px] py-2 bg-white border border-[#e4eaf2] text-[#1f2a3a] rounded-lg text-[13px] font-semibold hover:bg-[#f4f7fb] transition-colors">View all</button>
|
||||
</div>
|
||||
@@ -288,66 +292,59 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="hover:bg-[#fafcfe] transition-colors">
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">JM</div>
|
||||
<div><strong class="font-bold">J. Mokoena</strong><span class="block text-[#8a96a8] text-[11px]">Executive</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">JNB → CPT, 02 Jun</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Flight</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 3,240</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#fafcfe] transition-colors">
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">NN</div>
|
||||
<div><strong class="font-bold">N. Ndlovu</strong><span class="block text-[#8a96a8] text-[11px]">Manager</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">Premier Hotel OR Tambo</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Hotel</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 1,895</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#fafcfe] transition-colors">
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">SP</div>
|
||||
<div><strong class="font-bold">S. Pillay</strong><span class="block text-[#8a96a8] text-[11px]">Coordinator</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">Avis Group B, 3 days</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Car</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Pending</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 1,420</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#fafcfe] transition-colors">
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">TM</div>
|
||||
<div><strong class="font-bold">T. Maluleke</strong><span class="block text-[#8a96a8] text-[11px]">Director</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">JNB → LHR, 14 Jun</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Flight</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]"><span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span></td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R 18,940</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#fafcfe] transition-colors">
|
||||
<td class="p-3 text-[13px] text-[#1f2a3a]">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">LK</div>
|
||||
<div><strong class="font-bold">L. Khumalo</strong><span class="block text-[#8a96a8] text-[11px]">Officer</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 text-[13px] text-[#1f2a3a]">Shuttle: Sandton ⇄ OR Tambo</td>
|
||||
<td class="p-3 text-[13px]"><span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Shuttle</span></td>
|
||||
<td class="p-3 text-[13px]"><span class="bg-[#fde5e8] text-[#c8102e] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Cancelled</span></td>
|
||||
<td class="p-3 text-[13px] text-[#1f2a3a]">R 380</td>
|
||||
</tr>
|
||||
@foreach (var item in _recentBookings)
|
||||
{
|
||||
<tr class="hover:bg-[#fafcfe] transition-colors">
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center text-[10px] font-semibold">@item.Initials</div>
|
||||
<div><strong class="font-bold">@item.Traveller</strong><span class="block text-[#8a96a8] text-[11px]">@item.Role</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">@item.Details</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]">
|
||||
@if (item.Type == "Flight")
|
||||
{
|
||||
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Flight</span>
|
||||
}
|
||||
else if (item.Type == "Hotel")
|
||||
{
|
||||
<span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Hotel</span>
|
||||
}
|
||||
else if (item.Type == "Car")
|
||||
{
|
||||
<span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Car</span>
|
||||
}
|
||||
else if (item.Type == "Shuttle")
|
||||
{
|
||||
<span class="bg-[#e8f4f4] text-[#00a3a1] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Shuttle</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="bg-[#f4f7fb] text-[#5a6577] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">@item.Type</span>
|
||||
}
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px]">
|
||||
@if (item.Status == "Confirmed")
|
||||
{
|
||||
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Confirmed</span>
|
||||
}
|
||||
else if (item.Status == "Pending")
|
||||
{
|
||||
<span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Pending</span>
|
||||
}
|
||||
else if (item.Status == "Cancelled")
|
||||
{
|
||||
<span class="bg-[#fde5e8] text-[#c8102e] px-2.5 py-[3px] rounded-full text-[11px] font-semibold">Cancelled</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="bg-gray-100 text-gray-800 px-2.5 py-[3px] rounded-full text-[11px] font-semibold">@item.Status</span>
|
||||
}
|
||||
</td>
|
||||
<td class="p-3 border-b border-[#e4eaf2] text-[13px] text-[#1f2a3a]">R @item.Cost.ToString("N0")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -402,6 +399,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Create Booking Modal *@
|
||||
@if (_showNewBookingModal)
|
||||
{
|
||||
<div class="fixed inset-0 bg-[#1f2a3a]/60 backdrop-blur-xs z-50 flex items-center justify-center p-4 text-left">
|
||||
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6 animate-in fade-in duration-150">
|
||||
<div class="flex items-center justify-between mb-4 pb-3 border-b border-[#e4eaf2]">
|
||||
<h3 class="text-base font-bold text-[#003b71] flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-[#0066b3]">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
New Travel Booking
|
||||
</h3>
|
||||
<button @onclick="@(() => _showNewBookingModal = false)" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 text-xs text-[#5a6577]">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Traveller Name</label>
|
||||
<input type="text" @bind="_newTraveller" placeholder="e.g. J. Mokoena" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Role / Grade</label>
|
||||
<select @bind="_newRole" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]">
|
||||
<option value="Executive">Executive</option>
|
||||
<option value="Director">Director</option>
|
||||
<option value="Manager">Manager</option>
|
||||
<option value="Officer">Officer</option>
|
||||
<option value="Coordinator">Coordinator</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Booking Type</label>
|
||||
<select @bind="_newType" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]">
|
||||
<option value="Flight">Flight</option>
|
||||
<option value="Hotel">Hotel</option>
|
||||
<option value="Car">Car</option>
|
||||
<option value="Shuttle">Shuttle</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Route / Stay Details</label>
|
||||
<input type="text" @bind="_newDetails" placeholder="e.g. JNB → CPT, 18 Jun or Premier Hotel, 3 nights" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase text-[#8a96a8] mb-1">Booking Cost (ZAR)</label>
|
||||
<input type="number" @bind="_newCost" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-xs bg-white text-[#1f2a3a] outline-none focus:border-[#0066b3]" />
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-[#e2eefb] text-[#0066b3] border border-blue-100 rounded-lg text-[11px] leading-relaxed">
|
||||
This booking will be automatically synced with the global GDS network and reconciled against the corporate Lodge Card parameters.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-end pt-4 mt-4 border-t border-[#e4eaf2]">
|
||||
<button @onclick="@(() => _showNewBookingModal = false)" class="px-4 py-2 rounded-lg text-xs font-bold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">Discard</button>
|
||||
<button @onclick="@(() => CreateBooking())" class="px-5 py-2 rounded-lg text-xs font-bold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer shadow-xs">Generate Booking</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@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<HomeBookingItem> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user