Implement new approach: real-time transport data per shift selection with ShiftWithTransport view model
Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>
This commit is contained in:
parent
dff63aa424
commit
246e90d493
5 changed files with 279 additions and 4 deletions
|
|
@ -14,13 +14,27 @@
|
|||
{
|
||||
<div class="tooltip-wrapper">
|
||||
<button class="shift-button @(SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name ? "selected" : "")"
|
||||
@onclick="() => SelectShift(day, shift.Name)">
|
||||
@onclick="() => SelectShift(day, shift.Name)"
|
||||
disabled="@_isLoadingTransport">
|
||||
@if (_isLoadingTransport && SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name)
|
||||
{
|
||||
<span class="loading-spinner">⏳</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@shift.Icon
|
||||
}
|
||||
</button>
|
||||
<span class="tooltip">@shift.Name</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (SelectedSchedule.ContainsKey(day))
|
||||
{
|
||||
<div class="transport-info">
|
||||
<small>@GetTransportSummary(day)</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ namespace ShiftScheduler.Client.Pages
|
|||
private List<DateTime> DaysInMonth { get; set; } = new();
|
||||
private List<Shift> Shifts { get; set; } = new();
|
||||
private Dictionary<DateTime, string> SelectedSchedule { get; set; } = new();
|
||||
private Dictionary<DateTime, ShiftWithTransport> SelectedShiftsWithTransport { get; set; } = new();
|
||||
private int EditMonth { get; set; }
|
||||
private int EditYear { get; set; }
|
||||
private bool _isLoadingTransport = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
|
@ -42,12 +44,54 @@ namespace ShiftScheduler.Client.Pages
|
|||
private async Task SelectShift(DateTime day, string shiftName)
|
||||
{
|
||||
SelectedSchedule[day] = shiftName;
|
||||
|
||||
// Get transport information for this shift and date
|
||||
_isLoadingTransport = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new { ShiftName = shiftName, Date = day };
|
||||
var response = await HttpClient.PostAsJsonAsync("api/shift/shift_transport", request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var shiftWithTransport = await response.Content.ReadFromJsonAsync<ShiftWithTransport>();
|
||||
if (shiftWithTransport != null)
|
||||
{
|
||||
SelectedShiftsWithTransport[day] = shiftWithTransport;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error fetching transport data: {ex.Message}");
|
||||
// Create a fallback with just the shift data
|
||||
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
||||
if (shift != null)
|
||||
{
|
||||
SelectedShiftsWithTransport[day] = new ShiftWithTransport
|
||||
{
|
||||
Date = day,
|
||||
Shift = shift,
|
||||
MorningTransport = null,
|
||||
AfternoonTransport = null
|
||||
};
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingTransport = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
await SaveScheduleToStorage();
|
||||
}
|
||||
|
||||
private async Task ExportToIcs()
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync("api/shift/export_ics", SelectedSchedule);
|
||||
var shiftsWithTransportList = SelectedShiftsWithTransport.Values.ToList();
|
||||
var response = await HttpClient.PostAsJsonAsync("api/shift/export_ics_with_transport", shiftsWithTransportList);
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var fileUrl = $"data:text/calendar;base64,{base64}";
|
||||
|
|
@ -56,7 +100,8 @@ namespace ShiftScheduler.Client.Pages
|
|||
|
||||
private async Task ExportToPdf()
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync("api/shift/export_pdf", SelectedSchedule);
|
||||
var shiftsWithTransportList = SelectedShiftsWithTransport.Values.ToList();
|
||||
var response = await HttpClient.PostAsJsonAsync("api/shift/export_pdf_with_transport", shiftsWithTransportList);
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var fileUrl = $"data:application/octet-stream;base64,{base64}";
|
||||
|
|
@ -99,18 +144,63 @@ namespace ShiftScheduler.Client.Pages
|
|||
kvp => DateTime.Parse(kvp.Key),
|
||||
kvp => kvp.Value
|
||||
);
|
||||
|
||||
// Reload transport data for all selected shifts
|
||||
await ReloadTransportDataForSchedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
SelectedSchedule = new Dictionary<DateTime, string>();
|
||||
SelectedShiftsWithTransport = new Dictionary<DateTime, ShiftWithTransport>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadTransportDataForSchedule()
|
||||
{
|
||||
foreach (var kvp in SelectedSchedule)
|
||||
{
|
||||
var day = kvp.Key;
|
||||
var shiftName = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
var request = new { ShiftName = shiftName, Date = day };
|
||||
var response = await HttpClient.PostAsJsonAsync("api/shift/shift_transport", request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var shiftWithTransport = await response.Content.ReadFromJsonAsync<ShiftWithTransport>();
|
||||
if (shiftWithTransport != null)
|
||||
{
|
||||
SelectedShiftsWithTransport[day] = shiftWithTransport;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error reloading transport data for {day}: {ex.Message}");
|
||||
// Create fallback without transport
|
||||
var shift = Shifts.FirstOrDefault(s => s.Name == shiftName);
|
||||
if (shift != null)
|
||||
{
|
||||
SelectedShiftsWithTransport[day] = new ShiftWithTransport
|
||||
{
|
||||
Date = day,
|
||||
Shift = shift,
|
||||
MorningTransport = null,
|
||||
AfternoonTransport = null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetSchedule()
|
||||
{
|
||||
SelectedSchedule.Clear();
|
||||
SelectedShiftsWithTransport.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -122,5 +212,24 @@ namespace ShiftScheduler.Client.Pages
|
|||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get transport summary for display
|
||||
public string GetTransportSummary(DateTime date)
|
||||
{
|
||||
if (!SelectedShiftsWithTransport.TryGetValue(date, out var shiftWithTransport))
|
||||
return string.Empty;
|
||||
|
||||
var summaries = new List<string>();
|
||||
|
||||
var morningTransport = shiftWithTransport.GetMorningTransportSummary();
|
||||
if (!string.IsNullOrEmpty(morningTransport))
|
||||
summaries.Add($"🌅 {morningTransport}");
|
||||
|
||||
var afternoonTransport = shiftWithTransport.GetAfternoonTransportSummary();
|
||||
if (!string.IsNullOrEmpty(afternoonTransport))
|
||||
summaries.Add($"🌆 {afternoonTransport}");
|
||||
|
||||
return string.Join(" | ", summaries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,3 +141,25 @@
|
|||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Transport information styling */
|
||||
.transport-info {
|
||||
margin-top: 8px;
|
||||
padding: 4px;
|
||||
background-color: #e8f4fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: #2c5aa0;
|
||||
word-wrap: break-word;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,66 @@ namespace ShiftScheduler.Server.Controllers
|
|||
return Ok(connection);
|
||||
}
|
||||
|
||||
[HttpPost("shift_transport")]
|
||||
public async Task<IActionResult> GetShiftTransport([FromBody] ShiftTransportRequest request)
|
||||
{
|
||||
var shift = _shiftService.GetShifts().FirstOrDefault(s => s.Name == request.ShiftName);
|
||||
if (shift == null)
|
||||
{
|
||||
return NotFound($"Shift '{request.ShiftName}' not found");
|
||||
}
|
||||
|
||||
TransportConnection? morningTransport = null;
|
||||
TransportConnection? afternoonTransport = null;
|
||||
|
||||
// Get transport for morning shift if it has morning time
|
||||
if (!string.IsNullOrEmpty(shift.MorningTime))
|
||||
{
|
||||
var morningStartTime = ParseShiftTime(request.Date, shift.MorningTime);
|
||||
if (morningStartTime.HasValue)
|
||||
{
|
||||
morningTransport = await _transportService.GetConnectionAsync(morningStartTime.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Get transport for afternoon shift if it has afternoon time
|
||||
if (!string.IsNullOrEmpty(shift.AfternoonTime))
|
||||
{
|
||||
var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime);
|
||||
if (afternoonStartTime.HasValue)
|
||||
{
|
||||
afternoonTransport = await _transportService.GetConnectionAsync(afternoonStartTime.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var shiftWithTransport = new ShiftWithTransport
|
||||
{
|
||||
Date = request.Date,
|
||||
Shift = shift,
|
||||
MorningTransport = morningTransport,
|
||||
AfternoonTransport = afternoonTransport
|
||||
};
|
||||
|
||||
return Ok(shiftWithTransport);
|
||||
}
|
||||
|
||||
private static DateTime? ParseShiftTime(DateTime date, string timeRange)
|
||||
{
|
||||
try
|
||||
{
|
||||
var times = timeRange.Split('-');
|
||||
if (times.Length > 0 && TimeSpan.TryParse(times[0], out var startTime))
|
||||
{
|
||||
return date.Add(startTime);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore parsing errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost("export_ics")]
|
||||
public async Task<IActionResult> ExportIcs([FromBody] Dictionary<DateTime, string> schedule)
|
||||
{
|
||||
|
|
@ -50,6 +110,22 @@ namespace ShiftScheduler.Server.Controllers
|
|||
return File(System.Text.Encoding.UTF8.GetBytes(ics), "text/calendar", "schedule.ics");
|
||||
}
|
||||
|
||||
[HttpPost("export_ics_with_transport")]
|
||||
public IActionResult ExportIcsWithTransport([FromBody] List<ShiftWithTransport> shiftsWithTransport)
|
||||
{
|
||||
var schedule = shiftsWithTransport.ToDictionary(s => s.Date, s => s.Shift.Name);
|
||||
var shifts = shiftsWithTransport.Select(s => s.Shift).DistinctBy(s => s.Name).ToList();
|
||||
var shiftTransports = shiftsWithTransport.Select(s => new ShiftTransport
|
||||
{
|
||||
ShiftName = s.Shift.Name,
|
||||
MorningTransport = s.MorningTransport,
|
||||
AfternoonTransport = s.AfternoonTransport
|
||||
}).DistinctBy(s => s.ShiftName).ToList();
|
||||
|
||||
var ics = _icsService.GenerateIcs(schedule, shifts, shiftTransports);
|
||||
return File(System.Text.Encoding.UTF8.GetBytes(ics), "text/calendar", "schedule.ics");
|
||||
}
|
||||
|
||||
[HttpPost("export_pdf")]
|
||||
public async Task<IActionResult> ExportPdf([FromBody] Dictionary<DateTime, string> schedule)
|
||||
{
|
||||
|
|
@ -58,6 +134,22 @@ namespace ShiftScheduler.Server.Controllers
|
|||
var pdf = _pdfExportService.GenerateMonthlySchedulePdf(schedule, shifts, shiftTransports);
|
||||
return File(pdf, "application/pdf", "schedule.pdf");
|
||||
}
|
||||
|
||||
[HttpPost("export_pdf_with_transport")]
|
||||
public IActionResult ExportPdfWithTransport([FromBody] List<ShiftWithTransport> shiftsWithTransport)
|
||||
{
|
||||
var schedule = shiftsWithTransport.ToDictionary(s => s.Date, s => s.Shift.Name);
|
||||
var shifts = shiftsWithTransport.Select(s => s.Shift).DistinctBy(s => s.Name).ToList();
|
||||
var shiftTransports = shiftsWithTransport.Select(s => new ShiftTransport
|
||||
{
|
||||
ShiftName = s.Shift.Name,
|
||||
MorningTransport = s.MorningTransport,
|
||||
AfternoonTransport = s.AfternoonTransport
|
||||
}).DistinctBy(s => s.ShiftName).ToList();
|
||||
|
||||
var pdf = _pdfExportService.GenerateMonthlySchedulePdf(schedule, shifts, shiftTransports);
|
||||
return File(pdf, "application/pdf", "schedule.pdf");
|
||||
}
|
||||
}
|
||||
|
||||
public class TransportConnectionRequest
|
||||
|
|
@ -65,4 +157,10 @@ namespace ShiftScheduler.Server.Controllers
|
|||
public DateTime ArrivalTime { get; set; }
|
||||
public string? EndStation { get; set; }
|
||||
}
|
||||
|
||||
public class ShiftTransportRequest
|
||||
{
|
||||
public string ShiftName { get; set; } = string.Empty;
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,4 +93,36 @@ namespace ShiftScheduler.Shared.Models
|
|||
public string Category { get; set; } = string.Empty;
|
||||
public string Number { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// View model that combines shift information with transport data for a specific date
|
||||
public record ShiftWithTransport
|
||||
{
|
||||
public DateTime Date { get; init; }
|
||||
public Shift Shift { get; init; } = new();
|
||||
public TransportConnection? MorningTransport { get; init; }
|
||||
public TransportConnection? AfternoonTransport { get; init; }
|
||||
|
||||
public string GetMorningTransportSummary()
|
||||
{
|
||||
return MorningTransport != null ? FormatTransportSummary(MorningTransport) : string.Empty;
|
||||
}
|
||||
|
||||
public string GetAfternoonTransportSummary()
|
||||
{
|
||||
return AfternoonTransport != null ? FormatTransportSummary(AfternoonTransport) : string.Empty;
|
||||
}
|
||||
|
||||
private static string FormatTransportSummary(TransportConnection connection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connection.DepartureTime)) return string.Empty;
|
||||
|
||||
var departure = DateTime.TryParse(connection.DepartureTime, out var dep) ? dep.ToString("HH:mm") : connection.DepartureTime;
|
||||
var arrival = DateTime.TryParse(connection.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : connection.ArrivalTime;
|
||||
|
||||
var mainJourney = connection.Sections?.FirstOrDefault()?.Journey;
|
||||
var trainInfo = mainJourney != null ? $"{mainJourney.Category}{mainJourney.Number}" : "Train";
|
||||
|
||||
return $"{trainInfo} {departure}→{arrival}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue