diff --git a/Client/Components/ConfigurationDialog.razor b/Client/Components/ConfigurationDialog.razor new file mode 100644 index 0000000..ca3279d --- /dev/null +++ b/Client/Components/ConfigurationDialog.razor @@ -0,0 +1,338 @@ +@using ShiftScheduler.Shared +@using System.Text.Json +@inject HttpClient HttpClient +@inject IJSRuntime JSRuntime + + + +@if (IsVisible) +{ + +} + + + +@code { + [Parameter] public bool IsVisible { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public EventCallback OnConfigurationChanged { get; set; } + + private ApplicationConfiguration _config = new(); + private bool _isLoading = false; + private bool _isSaving = false; + private bool _showImportSection = false; + private string _errorMessage = string.Empty; + private string _successMessage = string.Empty; + private ElementReference _fileInput; + + protected override async Task OnParametersSetAsync() + { + if (IsVisible && (_config.Shifts.Count == 0 || _config.Transport.StartStation == string.Empty)) + { + await LoadConfiguration(); + } + } + + private async Task LoadConfiguration() + { + _isLoading = true; + _errorMessage = string.Empty; + StateHasChanged(); + + try + { + var response = await HttpClient.GetAsync("api/configuration"); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + _config = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }) ?? new ApplicationConfiguration(); + } + else + { + _errorMessage = "Failed to load configuration"; + } + } + catch (Exception ex) + { + _errorMessage = $"Error loading configuration: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task Save() + { + _isSaving = true; + _errorMessage = string.Empty; + _successMessage = string.Empty; + StateHasChanged(); + + try + { + var response = await HttpClient.PutAsJsonAsync("api/configuration", _config); + if (response.IsSuccessStatusCode) + { + _successMessage = "Configuration saved successfully!"; + await OnConfigurationChanged.InvokeAsync(); + await Task.Delay(1000); // Show success message briefly + await Close(); + } + else + { + _errorMessage = "Failed to save configuration"; + } + } + catch (Exception ex) + { + _errorMessage = $"Error saving configuration: {ex.Message}"; + } + finally + { + _isSaving = false; + StateHasChanged(); + } + } + + private async Task ExportConfiguration() + { + try + { + var response = await HttpClient.GetAsync("api/configuration/export"); + if (response.IsSuccessStatusCode) + { + var bytes = await response.Content.ReadAsByteArrayAsync(); + var base64 = Convert.ToBase64String(bytes); + await JSRuntime.InvokeVoidAsync("downloadFile", "ShiftScheduler-Configuration.json", "application/json", base64); + } + } + catch (Exception ex) + { + _errorMessage = $"Error exporting configuration: {ex.Message}"; + StateHasChanged(); + } + } + + private async Task ImportConfiguration() + { + try + { + var result = await JSRuntime.InvokeAsync("readFileAsText", _fileInput); + if (!string.IsNullOrEmpty(result)) + { + var request = new { JsonContent = result }; + var response = await HttpClient.PostAsJsonAsync("api/configuration/import", request); + if (response.IsSuccessStatusCode) + { + await LoadConfiguration(); + _successMessage = "Configuration imported successfully!"; + _showImportSection = false; + } + else + { + _errorMessage = "Failed to import configuration"; + } + } + } + catch (Exception ex) + { + _errorMessage = $"Error importing configuration: {ex.Message}"; + } + StateHasChanged(); + } + + private void AddShift() + { + _config.Shifts.Add(new Shift + { + Name = "New Shift", + Icon = "⭐", + MorningTime = "", + AfternoonTime = "" + }); + StateHasChanged(); + } + + private void RemoveShift(int index) + { + if (index >= 0 && index < _config.Shifts.Count) + { + _config.Shifts.RemoveAt(index); + StateHasChanged(); + } + } + + private async Task Close() + { + _errorMessage = string.Empty; + _successMessage = string.Empty; + _showImportSection = false; + await OnClose.InvokeAsync(); + } +} \ No newline at end of file diff --git a/Client/Pages/Index.razor b/Client/Pages/Index.razor index bd65808..17c7f58 100644 --- a/Client/Pages/Index.razor +++ b/Client/Pages/Index.razor @@ -1,9 +1,11 @@ @page "/" +@using ShiftScheduler.Client.Components
+

Shift Scheduler - @CurrentMonthYear

@@ -67,3 +69,7 @@ else } } + + diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index c1f9f75..d4c548a 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -259,5 +259,26 @@ namespace ShiftScheduler.Client.Pages return string.Join(" | ", times); } + + private bool _showConfigDialog = false; + + private void ShowConfiguration() + { + _showConfigDialog = true; + StateHasChanged(); + } + + private void HideConfiguration() + { + _showConfigDialog = false; + StateHasChanged(); + } + + private async Task OnConfigurationChanged() + { + // Reload shifts after configuration changes + Shifts = await HttpClient.GetFromJsonAsync>("api/shift/shifts") ?? new(); + StateHasChanged(); + } } } diff --git a/Client/Pages/Index.razor.css b/Client/Pages/Index.razor.css index 8e602ee..d2e07a9 100644 --- a/Client/Pages/Index.razor.css +++ b/Client/Pages/Index.razor.css @@ -127,6 +127,24 @@ border-color: #FF69B4; } +/* Configuration button */ +.btn.config-btn { + margin: 5px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + background-color: #DDA0DD; /* Light purple/plum */ + color: #333; + border: 2px solid #DA70D6; /* Slightly darker purple border */ + border-radius: 5px; + transition: background-color 0.2s ease; +} + +.btn.config-btn:hover { + background-color: #DA70D6; + border-color: #BA55D3; +} + /* Reset section - separate from export buttons */ .reset-section { margin: 30px 0 20px 0; diff --git a/Client/wwwroot/js/fileDownload.js b/Client/wwwroot/js/fileDownload.js index a0c444b..4b398fa 100644 --- a/Client/wwwroot/js/fileDownload.js +++ b/Client/wwwroot/js/fileDownload.js @@ -15,4 +15,19 @@ window.downloadFile = (filename, contentType, base64Data) => { a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); +}; + +window.readFileAsText = (fileInput) => { + return new Promise((resolve, reject) => { + const file = fileInput.files[0]; + if (!file) { + resolve(''); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => resolve(e.target.result); + reader.onerror = (e) => reject(e); + reader.readAsText(file); + }); }; \ No newline at end of file diff --git a/Server/Controllers/ConfigurationController.cs b/Server/Controllers/ConfigurationController.cs new file mode 100644 index 0000000..93a3e7a --- /dev/null +++ b/Server/Controllers/ConfigurationController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Mvc; +using ShiftScheduler.Services; +using ShiftScheduler.Shared; +using System.Text; + +namespace ShiftScheduler.Server.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ConfigurationController : ControllerBase + { + private readonly IConfigurationService _configurationService; + + public ConfigurationController(IConfigurationService configurationService) + { + _configurationService = configurationService; + } + + [HttpGet] + public IActionResult GetConfiguration() + { + return Ok(_configurationService.GetConfiguration()); + } + + [HttpPut] + public IActionResult UpdateConfiguration([FromBody] ApplicationConfiguration configuration) + { + try + { + _configurationService.UpdateConfiguration(configuration); + return Ok(); + } + catch (Exception ex) + { + return BadRequest($"Failed to update configuration: {ex.Message}"); + } + } + + [HttpGet("export")] + public async Task ExportConfiguration() + { + try + { + var json = await _configurationService.ExportConfigurationAsync(); + var bytes = Encoding.UTF8.GetBytes(json); + return File(bytes, "application/json", "ShiftScheduler-Configuration.json"); + } + catch (Exception ex) + { + return BadRequest($"Failed to export configuration: {ex.Message}"); + } + } + + [HttpPost("import")] + public async Task ImportConfiguration([FromBody] ImportConfigurationRequest request) + { + try + { + await _configurationService.ImportConfigurationAsync(request.JsonContent); + return Ok(); + } + catch (Exception ex) + { + return BadRequest($"Failed to import configuration: {ex.Message}"); + } + } + } + + public class ImportConfigurationRequest + { + public string JsonContent { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs index 7e52165..90b29e2 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -7,9 +7,17 @@ var builder = WebApplication.CreateBuilder(args); var shifts = builder.Configuration.GetSection("Shifts").Get>() ?? new(); var transportConfig = builder.Configuration.GetSection("Transport").Get() ?? new(); +// Create application configuration +var appConfiguration = new ApplicationConfiguration +{ + Transport = transportConfig, + Shifts = shifts +}; + // Register services -builder.Services.AddSingleton(shifts); -builder.Services.AddSingleton(transportConfig); +builder.Services.AddSingleton(new ConfigurationService(appConfiguration)); +builder.Services.AddSingleton>(provider => provider.GetRequiredService().GetShifts()); +builder.Services.AddSingleton(provider => provider.GetRequiredService().GetTransportConfiguration()); builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddSingleton(); diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs new file mode 100644 index 0000000..7903f61 --- /dev/null +++ b/Services/ConfigurationService.cs @@ -0,0 +1,111 @@ +using ShiftScheduler.Shared; +using System.Text.Json; + +namespace ShiftScheduler.Services +{ + public interface IConfigurationService + { + ApplicationConfiguration GetConfiguration(); + void UpdateConfiguration(ApplicationConfiguration configuration); + Task ExportConfigurationAsync(); + Task ImportConfigurationAsync(string jsonContent); + List GetShifts(); + TransportConfiguration GetTransportConfiguration(); + } + + public class ConfigurationService : IConfigurationService + { + private ApplicationConfiguration _configuration; + private readonly object _lock = new object(); + + public ConfigurationService(ApplicationConfiguration initialConfiguration) + { + _configuration = initialConfiguration; + } + + public ApplicationConfiguration GetConfiguration() + { + lock (_lock) + { + return new ApplicationConfiguration + { + Transport = new TransportConfiguration + { + StartStation = _configuration.Transport.StartStation, + EndStation = _configuration.Transport.EndStation, + ApiBaseUrl = _configuration.Transport.ApiBaseUrl, + SafetyBufferMinutes = _configuration.Transport.SafetyBufferMinutes, + MinBreakMinutes = _configuration.Transport.MinBreakMinutes, + MaxEarlyArrivalMinutes = _configuration.Transport.MaxEarlyArrivalMinutes, + MaxLateArrivalMinutes = _configuration.Transport.MaxLateArrivalMinutes, + CacheDurationDays = _configuration.Transport.CacheDurationDays + }, + Shifts = _configuration.Shifts.Select(s => new Shift + { + Name = s.Name, + Icon = s.Icon, + MorningTime = s.MorningTime, + AfternoonTime = s.AfternoonTime + }).ToList() + }; + } + } + + public void UpdateConfiguration(ApplicationConfiguration configuration) + { + lock (_lock) + { + _configuration = new ApplicationConfiguration + { + Transport = new TransportConfiguration + { + StartStation = configuration.Transport.StartStation, + EndStation = configuration.Transport.EndStation, + ApiBaseUrl = configuration.Transport.ApiBaseUrl, + SafetyBufferMinutes = configuration.Transport.SafetyBufferMinutes, + MinBreakMinutes = configuration.Transport.MinBreakMinutes, + MaxEarlyArrivalMinutes = configuration.Transport.MaxEarlyArrivalMinutes, + MaxLateArrivalMinutes = configuration.Transport.MaxLateArrivalMinutes, + CacheDurationDays = configuration.Transport.CacheDurationDays + }, + Shifts = configuration.Shifts.Select(s => new Shift + { + Name = s.Name, + Icon = s.Icon, + MorningTime = s.MorningTime, + AfternoonTime = s.AfternoonTime + }).ToList() + }; + } + } + + public async Task ExportConfigurationAsync() + { + var config = GetConfiguration(); + return await Task.FromResult(JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + })); + } + + public async Task ImportConfigurationAsync(string jsonContent) + { + var configuration = JsonSerializer.Deserialize(jsonContent); + if (configuration != null) + { + UpdateConfiguration(configuration); + } + await Task.CompletedTask; + } + + public List GetShifts() + { + return GetConfiguration().Shifts; + } + + public TransportConfiguration GetTransportConfiguration() + { + return GetConfiguration().Transport; + } + } +} \ No newline at end of file diff --git a/Shared/Configuration.cs b/Shared/Configuration.cs new file mode 100644 index 0000000..737d7c9 --- /dev/null +++ b/Shared/Configuration.cs @@ -0,0 +1,8 @@ +namespace ShiftScheduler.Shared +{ + public class ApplicationConfiguration + { + public TransportConfiguration Transport { get; set; } = new(); + public List Shifts { get; set; } = new(); + } +} \ No newline at end of file