From 2530e729ceae14773bf66d3e21fcbeeebd8fb145 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 20:04:53 +0200 Subject: [PATCH] Implement transport time improvements: departure station display and break time logic (#17) * Initial plan * Implement transport time improvements: departure station display and break time logic Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Enhance code --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> Co-authored-by: Claudio Schaad --- Client/Pages/Index.razor.cs | 2 +- Server/Controllers/ShiftController.cs | 63 +++++++++++++++------- Server/Program.cs | 1 - Server/appsettings.json | 3 +- Services/PdfExportService.cs | 2 +- Services/ShiftEnrichmentService.cs | 77 --------------------------- Services/TransportService.cs | 25 ++------- Shared/ShiftWithTransport.cs | 2 +- Shared/TransportModels.cs | 18 ++++--- 9 files changed, 64 insertions(+), 129 deletions(-) delete mode 100644 Services/ShiftEnrichmentService.cs diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index 1400c9b..7110f75 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -112,7 +112,7 @@ namespace ShiftScheduler.Client.Pages var response = await HttpClient.PostAsJsonAsync("api/shift/export_pdf", shiftsWithTransportList); var bytes = await response.Content.ReadAsByteArrayAsync(); var base64 = Convert.ToBase64String(bytes); - await JSRuntime.InvokeVoidAsync("downloadFile", $"Schedule {EditYear}-{EditMonth:00}.pdf", "application/pdf", base64); + await JSRuntime.InvokeVoidAsync("downloadFile", $"Schedule {EditYear}-{EditMonth:D2}.pdf", "application/pdf", base64); } private async Task SaveScheduleToStorage() diff --git a/Server/Controllers/ShiftController.cs b/Server/Controllers/ShiftController.cs index 5c8ca09..6664d70 100644 --- a/Server/Controllers/ShiftController.cs +++ b/Server/Controllers/ShiftController.cs @@ -12,20 +12,20 @@ namespace ShiftScheduler.Server.Controllers private readonly IcsExportService _icsService; private readonly PdfExportService _pdfExportService; private readonly TransportService _transportService; - private readonly ShiftEnrichmentService _enrichmentService; + private readonly TransportConfiguration _transportConfig; public ShiftController( ShiftService shiftService, IcsExportService icsService, PdfExportService pdfExportService, TransportService transportService, - ShiftEnrichmentService enrichmentService) + TransportConfiguration transportConfig) { _shiftService = shiftService; _icsService = icsService; _pdfExportService = pdfExportService; _transportService = transportService; - _enrichmentService = enrichmentService; + _transportConfig = transportConfig; } [HttpGet("shifts")] @@ -34,13 +34,6 @@ namespace ShiftScheduler.Server.Controllers return Ok(_shiftService.GetShifts()); } - [HttpPost("transport_connection")] - public async Task GetTransportConnection([FromBody] TransportConnectionRequest request) - { - var connection = await _transportService.GetConnectionAsync(request.ArrivalTime, request.EndStation); - return Ok(connection); - } - [HttpPost("shift_transport")] public async Task GetShiftTransport([FromBody] ShiftTransportRequest request) { @@ -64,12 +57,31 @@ namespace ShiftScheduler.Server.Controllers } // Get transport for afternoon shift if it has afternoon time + // But only if the break between morning and afternoon is long enough if (!string.IsNullOrEmpty(shift.AfternoonTime)) { - var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime); - if (afternoonStartTime.HasValue) + var shouldLoadAfternoonTransport = true; + + // If both morning and afternoon shifts exist, check break duration + if (!string.IsNullOrEmpty(shift.MorningTime) && !string.IsNullOrEmpty(shift.AfternoonTime)) { - afternoonTransport = await _transportService.GetConnectionAsync(afternoonStartTime.Value); + var morningEndTime = ParseShiftEndTime(request.Date, shift.MorningTime); + var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime); + + if (morningEndTime.HasValue && afternoonStartTime.HasValue) + { + var breakDurationMinutes = (afternoonStartTime.Value - morningEndTime.Value).TotalMinutes; + shouldLoadAfternoonTransport = breakDurationMinutes >= _transportConfig.MinBreakMinutes; + } + } + + if (shouldLoadAfternoonTransport) + { + var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime); + if (afternoonStartTime.HasValue) + { + afternoonTransport = await _transportService.GetConnectionAsync(afternoonStartTime.Value); + } } } @@ -78,7 +90,7 @@ namespace ShiftScheduler.Server.Controllers Date = request.Date, Shift = shift, MorningTransport = morningTransport, - AfternoonTransport = afternoonTransport + AfternoonTransport = afternoonTransport, }; return Ok(shiftWithTransport); @@ -101,6 +113,23 @@ namespace ShiftScheduler.Server.Controllers return null; } + private static DateTime? ParseShiftEndTime(DateTime date, string timeRange) + { + try + { + var times = timeRange.Split('-'); + if (times.Length > 1 && TimeSpan.TryParse(times[1], out var endTime)) + { + return date.Add(endTime); + } + } + catch + { + // Ignore parsing errors + } + return null; + } + [HttpPost("export_ics")] public IActionResult ExportIcsWithTransport([FromBody] List shiftsWithTransport) { @@ -116,12 +145,6 @@ namespace ShiftScheduler.Server.Controllers } } - public class TransportConnectionRequest - { - public DateTime ArrivalTime { get; set; } - public string? EndStation { get; set; } - } - public class ShiftTransportRequest { public string ShiftName { get; set; } = string.Empty; diff --git a/Server/Program.cs b/Server/Program.cs index 715efa4..80455b3 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -15,7 +15,6 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); diff --git a/Server/appsettings.json b/Server/appsettings.json index 4f52399..1d67124 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -11,7 +11,8 @@ "StartStation": "Zurich", "EndStation": "Basel", "ApiBaseUrl": "http://transport.opendata.ch/v1", - "SafetyBufferMinutes": 30 + "SafetyBufferMinutes": 30, + "MinBreakMinutes": 60 }, "Shifts": [ diff --git a/Services/PdfExportService.cs b/Services/PdfExportService.cs index 607fabe..02f570f 100644 --- a/Services/PdfExportService.cs +++ b/Services/PdfExportService.cs @@ -175,7 +175,7 @@ namespace ShiftScheduler.Services var mainJourney = transport.Sections?.FirstOrDefault()?.Journey; var trainInfo = mainJourney != null ? $"{mainJourney.Category} {mainJourney.Number}" : "Train"; - + return $"{timeOfDay}: {trainInfo} {departure}→{arrival}"; } diff --git a/Services/ShiftEnrichmentService.cs b/Services/ShiftEnrichmentService.cs deleted file mode 100644 index 26ecb4e..0000000 --- a/Services/ShiftEnrichmentService.cs +++ /dev/null @@ -1,77 +0,0 @@ -using ShiftScheduler.Shared.Models; - -namespace ShiftScheduler.Services -{ - public class ShiftEnrichmentService - { - private readonly TransportService _transportService; - - public ShiftEnrichmentService(TransportService transportService) - { - _transportService = transportService; - } - - public async Task> EnrichShiftsWithTransportAsync(List shifts, Dictionary schedule) - { - var shiftTransports = new List(); - - foreach (var shift in shifts) - { - var shiftTransport = new ShiftTransport - { - ShiftName = shift.Name - }; - - // Find dates where this shift is scheduled - var scheduledDates = schedule.Where(kvp => kvp.Value == shift.Name).Select(kvp => kvp.Key).ToList(); - - if (scheduledDates.Count > 0) - { - // Use the first scheduled date to calculate transport times - var sampleDate = scheduledDates.First(); - - // Get transport for morning shift - if (!string.IsNullOrEmpty(shift.MorningTime)) - { - var morningStartTime = ParseShiftTime(sampleDate, shift.MorningTime); - if (morningStartTime.HasValue) - { - shiftTransport.MorningTransport = await _transportService.GetConnectionAsync(morningStartTime.Value); - } - } - - // Get transport for afternoon shift - if (!string.IsNullOrEmpty(shift.AfternoonTime)) - { - var afternoonStartTime = ParseShiftTime(sampleDate, shift.AfternoonTime); - if (afternoonStartTime.HasValue) - { - shiftTransport.AfternoonTransport = await _transportService.GetConnectionAsync(afternoonStartTime.Value); - } - } - } - - shiftTransports.Add(shiftTransport); - } - - return shiftTransports; - } - - private 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; - } - } -} \ No newline at end of file diff --git a/Services/TransportService.cs b/Services/TransportService.cs index 6213bbf..b388beb 100644 --- a/Services/TransportService.cs +++ b/Services/TransportService.cs @@ -23,13 +23,12 @@ namespace ShiftScheduler.Services // Calculate the latest acceptable arrival time (shift start time - safety buffer) var latestArrivalTime = shiftStartTime.AddMinutes(-_config.SafetyBufferMinutes); - // Search for connections starting from a reasonable time before the latest acceptable arrival - // Start searching from 90 minutes before the latest acceptable time - var searchTime = latestArrivalTime.AddMinutes(-90); + // Search for arrival time + var searchTime = shiftStartTime; var searchTimeStr = searchTime.ToString("yyyy-MM-dd"); var searchHourStr = searchTime.ToString("HH:mm"); - var url = $"{_config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(_config.StartStation)}&to={Uri.EscapeDataString(endStation)}&date={searchTimeStr}&time={searchHourStr}&limit=15"; + var url = $"{_config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(_config.StartStation)}&to={Uri.EscapeDataString(endStation)}&date={searchTimeStr}&time={searchHourStr}&isArrivalTime=1&limit=5"; var response = await _httpClient.GetStringAsync(url); var apiResponse = JsonSerializer.Deserialize(response, new JsonSerializerOptions @@ -121,25 +120,11 @@ namespace ShiftScheduler.Services if (validConnections.Count > 0) { - // Sort by arrival time (earliest first) and take the one that arrives - // with reasonable margin but not too early var sortedValid = validConnections .OrderBy(c => DateTime.Parse(c.To?.Arrival ?? "00:00")) .ToList(); - - // Prefer a connection that arrives with 10-45 minutes margin - var preferredMarginMin = 10; - var preferredMarginMax = 45; - - var idealConnection = sortedValid.LastOrDefault(c => - { - var arrivalTime = DateTime.Parse(c.To?.Arrival ?? "00:00"); - var marginMinutes = (latestArrivalTime - arrivalTime).TotalMinutes; - return marginMinutes >= preferredMarginMin && marginMinutes <= preferredMarginMax; - }); - - // If we found an ideal connection, use it; otherwise use the latest valid one - return idealConnection ?? sortedValid.Last(); + + return sortedValid.Last(); } // If no valid connections, return the earliest available diff --git a/Shared/ShiftWithTransport.cs b/Shared/ShiftWithTransport.cs index 2c794cd..5bfa2b7 100644 --- a/Shared/ShiftWithTransport.cs +++ b/Shared/ShiftWithTransport.cs @@ -18,7 +18,7 @@ namespace ShiftScheduler.Shared.Models return AfternoonTransport != null ? FormatTransportSummary(AfternoonTransport) : string.Empty; } - private static string FormatTransportSummary(TransportConnection connection) + private string FormatTransportSummary(TransportConnection connection) { if (string.IsNullOrEmpty(connection.DepartureTime)) return string.Empty; diff --git a/Shared/TransportModels.cs b/Shared/TransportModels.cs index db85461..c826316 100644 --- a/Shared/TransportModels.cs +++ b/Shared/TransportModels.cs @@ -1,18 +1,12 @@ namespace ShiftScheduler.Shared.Models { - public class ShiftTransport - { - public string ShiftName { get; set; } = string.Empty; - public TransportConnection? MorningTransport { get; set; } - public TransportConnection? AfternoonTransport { get; set; } - } - public class TransportConfiguration { public string StartStation { get; set; } = string.Empty; public string EndStation { get; set; } = string.Empty; public string ApiBaseUrl { get; set; } = string.Empty; public int SafetyBufferMinutes { get; set; } = 30; + public int MinBreakMinutes { get; set; } = 60; } public class TransportConnection @@ -64,6 +58,11 @@ namespace ShiftScheduler.Shared.Models public TransportApiCheckpoint? To { get; set; } public string Duration { get; set; } = string.Empty; public List Sections { get; set; } = new(); + + public override string ToString() + { + return $"{From} - {To}"; + } } public class TransportApiCheckpoint @@ -72,6 +71,11 @@ namespace ShiftScheduler.Shared.Models public string Departure { get; set; } = string.Empty; public string Arrival { get; set; } = string.Empty; public string Platform { get; set; } = string.Empty; + + public override string ToString() + { + return $"{Station?.Name}: {Departure} - {Arrival}"; + } } public class TransportApiStation