diff --git a/Client/Pages/Index.razor b/Client/Pages/Index.razor index fa4ad60..b444288 100644 --- a/Client/Pages/Index.razor +++ b/Client/Pages/Index.razor @@ -14,13 +14,27 @@ {
@shift.Name
} + @if (SelectedSchedule.ContainsKey(day)) + { +
+ @GetTransportSummary(day) +
+ } } diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index ea90886..5868049 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -15,8 +15,10 @@ namespace ShiftScheduler.Client.Pages private List DaysInMonth { get; set; } = new(); private List Shifts { get; set; } = new(); private Dictionary SelectedSchedule { get; set; } = new(); + private Dictionary 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(); + 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(); + SelectedShiftsWithTransport = new Dictionary(); + } + } + + 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(); + 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(); + + 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); + } } } diff --git a/Client/Pages/Index.razor.css b/Client/Pages/Index.razor.css index c823f7e..1a04a16 100644 --- a/Client/Pages/Index.razor.css +++ b/Client/Pages/Index.razor.css @@ -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); } +} diff --git a/Server/Controllers/ShiftController.cs b/Server/Controllers/ShiftController.cs index 51898e2..61378c4 100644 --- a/Server/Controllers/ShiftController.cs +++ b/Server/Controllers/ShiftController.cs @@ -41,6 +41,66 @@ namespace ShiftScheduler.Server.Controllers return Ok(connection); } + [HttpPost("shift_transport")] + public async Task 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 ExportIcs([FromBody] Dictionary 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 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 ExportPdf([FromBody] Dictionary 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 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; } + } } diff --git a/Shared/TransportModels.cs b/Shared/TransportModels.cs index a31f824..1179b66 100644 --- a/Shared/TransportModels.cs +++ b/Shared/TransportModels.cs @@ -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}"; + } + } } \ No newline at end of file