From 7f8b2c4b8c919d3d5038fec6df263a728984087a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 Aug 2025 19:47:12 +0200 Subject: [PATCH] Enhance shift handling: Move storage to file system and add month selection (#36) * Initial plan * Initial setup: Downgrade .NET version to 8.0 for compatibility Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Implement enhanced shift handling with file system storage and month selection Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Move component styles from global app.css to Index.razor.css for proper component isolation Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> --- Client/Pages/Index.razor | 14 ++-- Client/Pages/Index.razor.cs | 75 +++++++++++++-------- Client/Pages/Index.razor.css | 97 ++++++++++++++++++++------- Server/Controllers/ShiftController.cs | 49 ++++++++++++++ Services/ConfigurationService.cs | 74 ++++++++++++++++++++ 5 files changed, 252 insertions(+), 57 deletions(-) diff --git a/Client/Pages/Index.razor b/Client/Pages/Index.razor index cd3d8a3..9ad264a 100644 --- a/Client/Pages/Index.razor +++ b/Client/Pages/Index.razor @@ -2,10 +2,16 @@ @using ShiftScheduler.Client.Components
- - - - +
+ + +
+
+ + + + +

Shift Scheduler - @CurrentMonthYear

diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index fc11afb..4a55be2 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -18,6 +18,7 @@ namespace ShiftScheduler.Client.Pages private Dictionary SelectedShiftsWithTransport { get; set; } = new(); private int EditMonth { get; set; } private int EditYear { get; set; } + private bool _isCurrentMonth = false; private bool _isLoadingTransport = false; private bool _isLoadingInitial = false; private string _errorMessage = string.Empty; @@ -31,9 +32,7 @@ namespace ShiftScheduler.Client.Pages (EditMonth, EditYear) = GetNextMonthAndYear(); - DaysInMonth = Enumerable.Range(1, DateTime.DaysInMonth(EditYear, EditMonth)) - .Select(d => new DateTime(EditYear, EditMonth, d)) - .ToList(); + UpdateDaysInMonth(); Shifts = await HttpClient.GetFromJsonAsync>("api/shift/shifts") ?? new(); @@ -43,6 +42,13 @@ namespace ShiftScheduler.Client.Pages StateHasChanged(); } + private void UpdateDaysInMonth() + { + DaysInMonth = Enumerable.Range(1, DateTime.DaysInMonth(EditYear, EditMonth)) + .Select(d => new DateTime(EditYear, EditMonth, d)) + .ToList(); + } + private (int nextMonth, int year) GetNextMonthAndYear() { var today = DateTime.Today; @@ -51,6 +57,26 @@ namespace ShiftScheduler.Client.Pages return (nextMonth, year); } + private async Task SelectCurrentMonth() + { + var today = DateTime.Today; + EditMonth = today.Month; + EditYear = today.Year; + _isCurrentMonth = true; + UpdateDaysInMonth(); + await LoadScheduleFromStorage(); + StateHasChanged(); + } + + private async Task SelectNextMonth() + { + (EditMonth, EditYear) = GetNextMonthAndYear(); + _isCurrentMonth = false; + UpdateDaysInMonth(); + await LoadScheduleFromStorage(); + StateHasChanged(); + } + private async Task SelectShift(DateTime day, string shiftName) { SelectedSchedule[day] = shiftName; @@ -167,15 +193,14 @@ namespace ShiftScheduler.Client.Pages { try { - var storageKey = $"ShiftSchedule_{EditYear}_{EditMonth:D2}"; + var request = new + { + Year = EditYear, + Month = EditMonth, + Schedule = SelectedSchedule + }; - var serializableSchedule = SelectedSchedule.ToDictionary( - kvp => kvp.Key.ToString("yyyy-MM-dd"), - kvp => kvp.Value - ); - - var json = JsonSerializer.Serialize(serializableSchedule); - await JSRuntime.InvokeVoidAsync("localStorage.setItem", storageKey, json); + await HttpClient.PostAsJsonAsync("api/shift/save_schedule", request); } catch (Exception) { @@ -187,22 +212,19 @@ namespace ShiftScheduler.Client.Pages { try { - var storageKey = $"ShiftSchedule_{EditYear}_{EditMonth:D2}"; - var json = await JSRuntime.InvokeAsync("localStorage.getItem", storageKey); + var response = await HttpClient.GetAsync($"api/shift/load_schedule/{EditYear}/{EditMonth}"); - if (!string.IsNullOrEmpty(json)) + if (response.IsSuccessStatusCode) { - var serializableSchedule = JsonSerializer.Deserialize>(json); - if (serializableSchedule != null) - { - SelectedSchedule = serializableSchedule.ToDictionary( - kvp => DateTime.Parse(kvp.Key), - kvp => kvp.Value - ); - - // Reload transport data for all selected shifts - await ReloadTransportDataForSchedule(); - } + SelectedSchedule = await response.Content.ReadFromJsonAsync>() ?? new(); + + // Reload transport data for all selected shifts + await ReloadTransportDataForSchedule(); + } + else + { + SelectedSchedule = new Dictionary(); + SelectedShiftsWithTransport = new Dictionary(); } } catch (Exception) @@ -259,8 +281,7 @@ namespace ShiftScheduler.Client.Pages try { - var storageKey = $"ShiftSchedule_{EditYear}_{EditMonth:D2}"; - await JSRuntime.InvokeVoidAsync("localStorage.removeItem", storageKey); + await HttpClient.DeleteAsync($"api/shift/delete_schedule/{EditYear}/{EditMonth}"); } catch (Exception) { diff --git a/Client/Pages/Index.razor.css b/Client/Pages/Index.razor.css index a466d3c..9a7d49d 100644 --- a/Client/Pages/Index.razor.css +++ b/Client/Pages/Index.razor.css @@ -1,13 +1,49 @@ /* Top navigation buttons */ .top-nav-buttons { - position: fixed; - top: 60px; - right: 24px; display: flex; - gap: 10px; - z-index: 1000; - padding: 14px 0; - background-color: transparent; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + flex-wrap: wrap; + gap: 1rem; +} + +/* Month selection buttons */ +.month-selection { + display: flex; + gap: 0.5rem; +} + +/* Action buttons container */ +.action-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Month navigation buttons */ +.month-btn { + padding: 0.5rem 1rem; + border: 2px solid #1b6ec2; + background-color: #fff; + color: #1b6ec2; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.month-btn:hover { + background-color: #f8f9fa; +} + +.month-btn.selected { + background-color: #1b6ec2; + color: #fff; +} + +.month-btn:disabled { + opacity: 0.6; + cursor: not-allowed; } /* Loading overlay */ @@ -41,47 +77,56 @@ /* Calendar container */ .calendar-grid { display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 10px; - margin: 20px 0; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; } /* Each day cell */ .day-cell { - border: 1px solid #ccc; + border: 1px solid #dee2e6; border-radius: 8px; - padding: 10px; - text-align: center; - background-color: #f9f9f9; + padding: 1rem; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* Day number */ -.day-cell > div:first-child { +.day-header { font-weight: bold; - margin-bottom: 5px; + margin-bottom: 0.5rem; + color: #333; + font-size: 0.9rem; } /* Shift buttons container */ .shifts { - display: flex; - justify-content: center; - gap: 5px; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + margin-bottom: 0.5rem; } /* Individual shift buttons */ .shift-button { - border: 1px solid #888; - border-radius: 5px; - padding: 5px 8px; - background-color: white; + border: 2px solid #ddd; + border-radius: 6px; + padding: 0.5rem; + background-color: #fff; cursor: pointer; - font-size: 18px; + font-size: 1.2rem; transition: all 0.2s ease; + text-align: center; + min-height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; } .shift-button:hover { - background-color: #e0e0e0; + background-color: #f8f9fa; + border-color: #007bff; + transform: translateY(-1px); } /* Selected shift */ diff --git a/Server/Controllers/ShiftController.cs b/Server/Controllers/ShiftController.cs index 7d750f5..a051151 100644 --- a/Server/Controllers/ShiftController.cs +++ b/Server/Controllers/ShiftController.cs @@ -143,6 +143,48 @@ namespace ShiftScheduler.Server.Controllers var pdf = _pdfExportService.GenerateMonthlySchedulePdf(shiftsWithTransport); return File(pdf, "application/pdf", "schedule.pdf"); } + + [HttpPost("save_schedule")] + public async Task SaveSchedule([FromBody] SaveScheduleRequest request) + { + try + { + await _configurationService.SaveScheduleAsync(request.Year, request.Month, request.Schedule); + return Ok(); + } + catch (Exception ex) + { + return StatusCode(500, $"Error saving schedule: {ex.Message}"); + } + } + + [HttpGet("load_schedule/{year}/{month}")] + public async Task LoadSchedule(int year, int month) + { + try + { + var schedule = await _configurationService.LoadScheduleAsync(year, month); + return Ok(schedule); + } + catch (Exception ex) + { + return StatusCode(500, $"Error loading schedule: {ex.Message}"); + } + } + + [HttpDelete("delete_schedule/{year}/{month}")] + public async Task DeleteSchedule(int year, int month) + { + try + { + await _configurationService.DeleteScheduleAsync(year, month); + return Ok(); + } + catch (Exception ex) + { + return StatusCode(500, $"Error deleting schedule: {ex.Message}"); + } + } } public class ShiftTransportRequest @@ -150,4 +192,11 @@ namespace ShiftScheduler.Server.Controllers public string ShiftName { get; set; } = string.Empty; public DateTime Date { get; set; } } + + public class SaveScheduleRequest + { + public int Year { get; set; } + public int Month { get; set; } + public Dictionary Schedule { get; set; } = new(); + } } diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs index 7aac9d8..03901b5 100644 --- a/Services/ConfigurationService.cs +++ b/Services/ConfigurationService.cs @@ -11,6 +11,11 @@ namespace ShiftScheduler.Services Task ImportConfigurationAsync(string jsonContent); List GetShifts(); TransportConfiguration GetTransportConfiguration(); + + // Schedule persistence methods + Task SaveScheduleAsync(int year, int month, Dictionary schedule); + Task> LoadScheduleAsync(int year, int month); + Task DeleteScheduleAsync(int year, int month); } public class ConfigurationService : IConfigurationService @@ -128,6 +133,75 @@ namespace ShiftScheduler.Services return GetConfiguration().Transport; } + // Schedule persistence methods + public async Task SaveScheduleAsync(int year, int month, Dictionary schedule) + { + try + { + var scheduleFileName = $"schedule-{year:D4}-{month:D2}.json"; + var scheduleFilePath = Path.Combine(_configDirectory, scheduleFileName); + + var serializableSchedule = schedule.ToDictionary( + kvp => kvp.Key.ToString("yyyy-MM-dd"), + kvp => kvp.Value + ); + + var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(serializableSchedule, jsonOptions); + await File.WriteAllTextAsync(scheduleFilePath, json); + } + catch + { + // Log error if needed, but don't throw to avoid breaking the application + } + } + + public async Task> LoadScheduleAsync(int year, int month) + { + try + { + var scheduleFileName = $"schedule-{year:D4}-{month:D2}.json"; + var scheduleFilePath = Path.Combine(_configDirectory, scheduleFileName); + + if (!File.Exists(scheduleFilePath)) + return new Dictionary(); + + var json = await File.ReadAllTextAsync(scheduleFilePath); + var serializableSchedule = JsonSerializer.Deserialize>(json); + + if (serializableSchedule == null) + return new Dictionary(); + + return serializableSchedule.ToDictionary( + kvp => DateTime.Parse(kvp.Key), + kvp => kvp.Value + ); + } + catch + { + return new Dictionary(); + } + } + + public async Task DeleteScheduleAsync(int year, int month) + { + try + { + var scheduleFileName = $"schedule-{year:D4}-{month:D2}.json"; + var scheduleFilePath = Path.Combine(_configDirectory, scheduleFileName); + + if (File.Exists(scheduleFilePath)) + { + File.Delete(scheduleFilePath); + } + await Task.CompletedTask; + } + catch + { + // Log error if needed, but don't throw + } + } + private ApplicationConfiguration LoadConfigurationFromFiles(ApplicationConfiguration fallbackConfiguration) { try