Files
2026-07-21 13:52:43 +07:00

208 lines
10 KiB
Plaintext

@page "/utilities/booking-scheduler"
@using Indotalent.Features.Utilities.BookingScheduler
@using Indotalent.Features.Utilities.BookingScheduler.Cqrs
@using MudBlazor
@inject BookingSchedulerService SchedulerService
<MudPaper Elevation="0" Class="pa-6 mb-3 d-flex align-center justify-space-between" Style="border: 1px solid #DCEBFA;">
<div>
<MudText Typo="Typo.h5" Style="font-weight: 900; color: #1a1a1a;">Booking Scheduler</MudText>
<MudText Typo="Typo.body2" Style="color: #64748b;">Visual schedule of resource allocations and time slots.</MudText>
</div>
<div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.Home" Size="Size.Small" Color="Color.Default" />
<MudText Typo="Typo.caption" Style="color: #94a3b8;">/</MudText>
<MudText Typo="Typo.caption" Style="color: #94a3b8;">Utilities</MudText>
<MudText Typo="Typo.caption" Style="color: #94a3b8;">/</MudText>
<MudText Typo="Typo.caption" Style="font-weight: 600; color: #1a1a1a;">Scheduler</MudText>
</div>
</MudPaper>
<MudGrid Spacing="3">
<MudItem xs="12" lg="4">
<MudPaper Elevation="0" Outlined="true" Class="pa-0 rounded-0" Style="border-color: #DCEBFA; background-color: #ffffff; overflow: hidden;">
<div class="pa-4 d-flex align-center justify-space-between" style="background-color: #F8FAFC; border-bottom: 1px solid #DCEBFA;">
<MudText Typo="Typo.subtitle2" Style="font-weight: 900; color: #1e293b; text-transform: uppercase;">Date Navigation</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" OnClick="LoadData" Disabled="_loading" Color="Color.Primary" />
</div>
<div class="pa-2">
<MudDatePicker PickerVariant="PickerVariant.Static"
@bind-Date="_selectedDate"
Color="Color.Primary"
Rounded="false"
Elevation="0" />
</div>
<MudDivider />
<div class="pa-4">
<MudText Typo="Typo.subtitle2" Class="mb-4" Style="font-weight: 900; color: #1e293b; text-transform: uppercase;">Summary for @(_selectedDate?.ToString("dd MMM yyyy") ?? "Today")</MudText>
<div class="d-flex flex-column gap-3">
<div class="d-flex justify-space-between align-center">
<MudText Typo="Typo.body2" Style="color: #64748b;">Total Bookings</MudText>
<MudText Typo="Typo.body2" Style="font-weight: 800; color: #0D47A1;">@GetFilteredData().Count()</MudText>
</div>
<div class="d-flex justify-space-between align-center">
<MudText Typo="Typo.body2" Style="color: #64748b;">Active Resources</MudText>
<MudText Typo="Typo.body2" Style="font-weight: 800; color: #0D47A1;">@GetFilteredData().Select(x => x.ResourceName).Distinct().Count()</MudText>
</div>
</div>
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" lg="8">
@if (_loading)
{
<div class="d-flex justify-center pa-20">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
</div>
}
else
{
<MudPaper Elevation="0" Outlined="true" Class="pa-6 rounded-0" Style="border-color: #DCEBFA; background-color: #ffffff; min-height: 600px;">
<div class="d-flex align-center gap-2 mb-8">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight: 900; color: #1a1a1a;">Timeline Details</MudText>
</div>
@if (!GetFilteredData().Any())
{
<div class="d-flex flex-column align-center justify-center pa-10" style="border: 2px dashed #E2E8F0;">
<MudIcon Icon="@Icons.Material.Filled.EventBusy" Size="Size.Large" Style="color: #cbd5e1;" />
<MudText Typo="Typo.body1" Class="mt-2" Style="color: #94a3b8; font-weight: 600;">No bookings scheduled for this date.</MudText>
</div>
}
else
{
<MudTimeline TimelinePosition="TimelinePosition.Start" Dense="true">
@foreach (var item in GetFilteredData())
{
<MudTimelineItem Color="@GetStatusColor(item.Status)" Size="Size.Small" Elevation="0">
<ItemOpposite>
<div style="text-align: right; padding-right: 16px;">
<MudText Typo="Typo.body2" Style="font-weight: 900; color: #1e293b; line-height: 1;">@item.StartTime?.ToString("HH:mm")</MudText>
<MudText Typo="Typo.caption" Style="color: #94a3b8; font-weight: 700;">@item.EndTime?.ToString("HH:mm")</MudText>
</div>
</ItemOpposite>
<ItemContent>
<MudPaper Elevation="0" Class="pa-4 mb-4 rounded-0" Style="@GetCardStyle(item.Status)">
<div class="d-flex justify-space-between align-start">
<div style="flex: 1;">
<div class="d-flex align-center gap-2 mb-1">
<MudText Typo="Typo.caption" Style="font-weight: 900; color: #94a3b8; text-transform: uppercase;">@item.ResourceGroupName</MudText>
</div>
<MudText Typo="Typo.subtitle1" Style="font-weight: 900; color: #0D47A1; line-height: 1.2;">@item.Subject</MudText>
<div class="d-flex align-center gap-2 mt-2">
<MudIcon Icon="@Icons.Material.Filled.Label" Size="Size.Small" Style="color: #64748b;" />
<MudText Typo="Typo.caption" Style="font-weight: 800; color: #475569;">@item.ResourceName</MudText>
</div>
</div>
<div style="text-align: right;">
<MudText Typo="Typo.caption" Style="@GetStatusTextStyle(item.Status)">@item.Status?.ToUpper()</MudText>
<div class="mt-1">
<MudIcon Icon="@Icons.Material.Filled.Circle" Size="Size.Small" Color="@GetStatusColor(item.Status)" />
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(item.Description))
{
<MudText Typo="Typo.body2" Class="mt-3 pa-2" Style="color: #64748b; background-color: #F8FAFC; border-radius: 4px; font-style: italic;">@item.Description</MudText>
}
<div class="d-flex align-center gap-4 mt-4 pt-3" style="border-top: 1px solid #F1F5F9;">
<div class="d-flex align-center gap-1">
<MudIcon Icon="@Icons.Material.Filled.Timer" Size="Size.Small" Style="color: #94a3b8;" />
<MudText Typo="Typo.caption" Style="color: #64748b; font-weight: 700;">@GetDuration(item.StartTime, item.EndTime)</MudText>
</div>
</div>
</MudPaper>
</ItemContent>
</MudTimelineItem>
}
</MudTimeline>
}
</MudPaper>
}
</MudItem>
</MudGrid>
<style>
.mud-timeline-item-opposite {
flex: 0 0 100px !important;
max-width: 100px !important;
}
.mud-picker-static {
width: 100% !important;
}
</style>
@code {
private List<GetBookingSchedulerListResponse> _data = new();
private bool _loading = true;
private DateTime? _selectedDate = DateTime.Today;
protected override async Task OnInitializedAsync() => await LoadData();
private async Task LoadData()
{
_loading = true;
StateHasChanged();
try
{
var response = await SchedulerService.GetBookingSchedulerListAsync();
await Task.Delay(500);
if (response != null && response.IsSuccess)
{
_data = response.Value ?? new();
}
}
finally
{
_loading = false;
StateHasChanged();
}
}
private IEnumerable<GetBookingSchedulerListResponse> GetFilteredData()
{
if (!_selectedDate.HasValue) return _data;
return _data.Where(x => x.StartTime.HasValue && x.StartTime.Value.Date == _selectedDate.Value.Date);
}
private Color GetStatusColor(string? status) => status switch
{
"Confirmed" => Color.Success,
"Done" => Color.Primary,
"OnProgress" => Color.Info,
"Cancelled" => Color.Error,
_ => Color.Default
};
private string GetStatusHex(string? status) => status switch
{
"Confirmed" => "#2E7D32",
"Done" => "#1565C0",
"OnProgress" => "#0288D1",
"Cancelled" => "#C62828",
_ => "#64748B"
};
private string GetStatusTextStyle(string? status)
{
var hex = GetStatusHex(status);
return $"font-weight: 900; color: {hex};";
}
private string GetCardStyle(string? status)
{
var hex = GetStatusHex(status);
return $"border: 1px solid #E2E8F0; border-left: 6px solid {hex}; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.02);";
}
private string GetDuration(DateTime? start, DateTime? end)
{
if (!start.HasValue || !end.HasValue) return "N/A";
var diff = end.Value - start.Value;
return $"{(int)diff.TotalHours}h {diff.Minutes}m";
}
}