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