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:
copilot-swe-agent[bot] 2025-08-19 18:43:41 +00:00
parent dff63aa424
commit 246e90d493
5 changed files with 279 additions and 4 deletions

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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); }
}

View file

@ -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; }
}
}

View file

@ -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}";
}
}
}