diff --git a/.gitignore b/.gitignore index 276ae74..b009ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -368,3 +368,5 @@ PlanTempus.Application/tmpclaude* PlanTempus.Application/wwwroot/js/app.js + +PlanTempus.Application/wwwroot/js/app.js.map diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml index b8d7f3c..aa95640 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml @@ -88,16 +88,6 @@ @Model.ToggleNo - - - @Model.SettingSmsReminders - @Model.SettingSmsRemindersDesc - - - @Model.ToggleYes - @Model.ToggleNo - - @Model.SettingEditCalendar @@ -114,38 +104,48 @@ @Model.LabelNotifications @Model.NotificationsIntro - - - - - - @Model.NotifOnlineBooking - - - - - - @Model.NotifManualBooking - - - - - - @Model.NotifCancellation - - - - - - @Model.NotifWaitlist - - - - - - @Model.NotifDailySummary - - + + @Model.SettingSmsReminders + + @Model.ToggleYes + @Model.ToggleNo + + + + @Model.NotifOnlineBooking + + @Model.ToggleYes + @Model.ToggleNo + + + + @Model.NotifManualBooking + + @Model.ToggleYes + @Model.ToggleNo + + + + @Model.NotifCancellation + + @Model.ToggleYes + @Model.ToggleNo + + + + @Model.NotifWaitlist + + @Model.ToggleYes + @Model.ToggleNo + + + + @Model.NotifDailySummary + + @Model.ToggleYes + @Model.ToggleNo + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml index 64c92bd..14b6b3f 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml @@ -1,37 +1,36 @@ @model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHoursViewModel - - - @Model.LabelWeeklySchedule - - - @Model.LabelMonday - 09:00 - 17:00 - - - @Model.LabelTuesday - 09:00 - 17:00 - - - @Model.LabelWednesday - 09:00 - 17:00 - - - @Model.LabelThursday - 09:00 - 19:00 - - - @Model.LabelFriday - 09:00 - 16:00 - - - @Model.LabelSaturday - Fri - - - @Model.LabelSunday - Fri - - - - +@{ + string GetBadgeClass(string status) => status switch + { + "work" => "", + "off" => "off", + "vacation" => "vacation", + "sick" => "sick", + _ => "off" + }; +} + + + + + + @foreach (var dayName in Model.DayNames) + { + @dayName + } + + + @foreach (var week in Model.Weeks) + { + + Uge @week.WeekNumber + @week.TotalHours @Model.LabelHours + + @foreach (var day in week.Days) + { + @day.Display + } + } + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs index e81e771..b530b2d 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Microsoft.AspNetCore.Mvc; using PlanTempus.Application.Features.Localization.Services; @@ -6,38 +7,132 @@ namespace PlanTempus.Application.Features.Employees.Components; public class EmployeeDetailHoursViewComponent : ViewComponent { private readonly ILocalizationService _localization; + private readonly IWebHostEnvironment _environment; - public EmployeeDetailHoursViewComponent(ILocalizationService localization) + public EmployeeDetailHoursViewComponent(ILocalizationService localization, IWebHostEnvironment environment) { _localization = localization; + _environment = environment; } public IViewComponentResult Invoke(string key) { + var weekSchedule = LoadMockData(); + var employee = weekSchedule.Employees.FirstOrDefault(e => e.EmployeeId == key); + var weeks = GenerateWeeks(weekSchedule, employee); + var model = new EmployeeDetailHoursViewModel { - LabelWeeklySchedule = _localization.Get("employees.detail.hours.weekly"), - LabelMonday = _localization.Get("employees.detail.hours.monday"), - LabelTuesday = _localization.Get("employees.detail.hours.tuesday"), - LabelWednesday = _localization.Get("employees.detail.hours.wednesday"), - LabelThursday = _localization.Get("employees.detail.hours.thursday"), - LabelFriday = _localization.Get("employees.detail.hours.friday"), - LabelSaturday = _localization.Get("employees.detail.hours.saturday"), - LabelSunday = _localization.Get("employees.detail.hours.sunday") + EmployeeId = key, + Weeks = weeks, + DayNames = new[] { "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag" }, + LabelHours = _localization.Get("employees.detail.hours.label") }; return View(model); } + + private WeekScheduleData LoadMockData() + { + var jsonPath = Path.Combine(_environment.ContentRootPath, "Features", "Employees", "Data", "workScheduleMock.json"); + var json = System.IO.File.ReadAllText(jsonPath); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + })!; + } + + private List GenerateWeeks(WeekScheduleData weekSchedule, EmployeeScheduleData? employee) + { + var weeks = new List(); + var startDate = DateTime.Parse(weekSchedule.StartDate); + + // Generate 6 weeks of data (current week + 5 more) + for (int w = 0; w < 6; w++) + { + var weekStart = startDate.AddDays(w * 7); + var weekNumber = GetWeekNumber(weekStart); + var days = new List(); + var totalMinutes = 0; + + for (int d = 0; d < 7; d++) + { + var date = weekStart.AddDays(d); + var dateKey = date.ToString("yyyy-MM-dd"); + var shift = employee?.Schedule.GetValueOrDefault(dateKey); + + string status = "off"; + string display = "—"; + + if (shift != null) + { + status = shift.Status; + if (shift.Status == "work" && shift.Start != null && shift.End != null) + { + display = $"{shift.Start} - {shift.End}"; + totalMinutes += CalculateMinutes(shift.Start, shift.End); + } + else if (shift.Status == "vacation") + { + display = "Ferie"; + } + else if (shift.Status == "sick") + { + display = "Syg"; + } + } + + days.Add(new DayHoursData + { + Date = dateKey, + Status = status, + Display = display + }); + } + + weeks.Add(new WeekHoursData + { + WeekNumber = weekNumber, + TotalHours = totalMinutes / 60, + Days = days + }); + } + + return weeks; + } + + private int GetWeekNumber(DateTime date) + { + var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar; + return cal.GetWeekOfYear(date, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); + } + + private int CalculateMinutes(string start, string end) + { + var startTime = TimeSpan.Parse(start); + var endTime = TimeSpan.Parse(end); + return (int)(endTime - startTime).TotalMinutes; + } } public class EmployeeDetailHoursViewModel { - public required string LabelWeeklySchedule { get; init; } - public required string LabelMonday { get; init; } - public required string LabelTuesday { get; init; } - public required string LabelWednesday { get; init; } - public required string LabelThursday { get; init; } - public required string LabelFriday { get; init; } - public required string LabelSaturday { get; init; } - public required string LabelSunday { get; init; } + public required string EmployeeId { get; init; } + public required List Weeks { get; init; } + public required string[] DayNames { get; init; } + public required string LabelHours { get; init; } +} + +public class WeekHoursData +{ + public int WeekNumber { get; init; } + public int TotalHours { get; init; } + public required List Days { get; init; } +} + +public class DayHoursData +{ + public required string Date { get; init; } + public required string Status { get; init; } + public required string Display { get; init; } } diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeWorkSchedule/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeWorkSchedule/Default.cshtml index 2562270..c1dadc0 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeWorkSchedule/Default.cshtml +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeWorkSchedule/Default.cshtml @@ -67,7 +67,7 @@ data-employee-id="@employee.EmployeeId" data-date="@day.Date" data-day="@day.ShortDayName"> - @GetTimeDisplay(shift) + @GetTimeDisplay(shift) } } diff --git a/PlanTempus.Application/Features/Employees/Data/workScheduleMock.json b/PlanTempus.Application/Features/Employees/Data/workScheduleMock.json index 9995b5e..b8ede0d 100644 --- a/PlanTempus.Application/Features/Employees/Data/workScheduleMock.json +++ b/PlanTempus.Application/Features/Employees/Data/workScheduleMock.json @@ -3,10 +3,10 @@ "year": 2025, "startDate": "2025-12-23", "endDate": "2025-12-29", - "closedDays": ["2025-12-25"], + "closedDays": ["2025-12-25", "2026-01-01"], "employees": [ { - "employeeId": "emp-1", + "employeeId": "employee-1", "name": "Anna Sørensen", "weeklyHours": 32, "schedule": { @@ -16,11 +16,39 @@ "2025-12-26": { "status": "off" }, "2025-12-27": { "status": "work", "start": "09:00", "end": "17:00" }, "2025-12-28": { "status": "work", "start": "10:00", "end": "14:00" }, - "2025-12-29": { "status": "off" } + "2025-12-29": { "status": "off" }, + "2025-12-30": { "status": "work", "start": "09:00", "end": "17:00" }, + "2025-12-31": { "status": "work", "start": "09:00", "end": "14:00" }, + "2026-01-01": { "status": "off" }, + "2026-01-02": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-03": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-04": { "status": "work", "start": "10:00", "end": "14:00" }, + "2026-01-05": { "status": "off" }, + "2026-01-06": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-07": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-08": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-09": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-10": { "status": "off" }, + "2026-01-11": { "status": "off" }, + "2026-01-12": { "status": "off" }, + "2026-01-13": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-14": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-15": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-16": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-17": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-18": { "status": "off" }, + "2026-01-19": { "status": "off" }, + "2026-01-20": { "status": "vacation" }, + "2026-01-21": { "status": "vacation" }, + "2026-01-22": { "status": "vacation" }, + "2026-01-23": { "status": "vacation" }, + "2026-01-24": { "status": "vacation" }, + "2026-01-25": { "status": "off" }, + "2026-01-26": { "status": "off" } } }, { - "employeeId": "emp-2", + "employeeId": "employee-2", "name": "Mette Jensen", "weeklyHours": 40, "schedule": { @@ -30,11 +58,39 @@ "2025-12-26": { "status": "vacation" }, "2025-12-27": { "status": "vacation" }, "2025-12-28": { "status": "off" }, - "2025-12-29": { "status": "off" } + "2025-12-29": { "status": "off" }, + "2025-12-30": { "status": "vacation" }, + "2025-12-31": { "status": "vacation" }, + "2026-01-01": { "status": "off" }, + "2026-01-02": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-03": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-04": { "status": "off" }, + "2026-01-05": { "status": "off" }, + "2026-01-06": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-07": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-08": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-09": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-10": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-11": { "status": "off" }, + "2026-01-12": { "status": "off" }, + "2026-01-13": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-14": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-15": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-16": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-17": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-18": { "status": "off" }, + "2026-01-19": { "status": "off" }, + "2026-01-20": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-21": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-22": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-23": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-24": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-25": { "status": "off" }, + "2026-01-26": { "status": "off" } } }, { - "employeeId": "emp-3", + "employeeId": "employee-3", "name": "Louise Nielsen", "weeklyHours": 37, "schedule": { @@ -44,11 +100,39 @@ "2025-12-26": { "status": "off" }, "2025-12-27": { "status": "work", "start": "09:00", "end": "17:00" }, "2025-12-28": { "status": "work", "start": "09:00", "end": "14:00" }, - "2025-12-29": { "status": "off" } + "2025-12-29": { "status": "off" }, + "2025-12-30": { "status": "work", "start": "09:00", "end": "17:00" }, + "2025-12-31": { "status": "work", "start": "09:00", "end": "13:00" }, + "2026-01-01": { "status": "off" }, + "2026-01-02": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-03": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-04": { "status": "off" }, + "2026-01-05": { "status": "off" }, + "2026-01-06": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-07": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-08": { "status": "sick" }, + "2026-01-09": { "status": "sick" }, + "2026-01-10": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-11": { "status": "off" }, + "2026-01-12": { "status": "off" }, + "2026-01-13": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-14": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-15": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-16": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-17": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-18": { "status": "off" }, + "2026-01-19": { "status": "off" }, + "2026-01-20": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-21": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-22": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-23": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-24": { "status": "work", "start": "09:00", "end": "17:00" }, + "2026-01-25": { "status": "off" }, + "2026-01-26": { "status": "off" } } }, { - "employeeId": "emp-4", + "employeeId": "employee-4", "name": "Katrine Pedersen", "weeklyHours": 24, "schedule": { @@ -58,11 +142,39 @@ "2025-12-26": { "status": "off" }, "2025-12-27": { "status": "work", "start": "12:00", "end": "20:00" }, "2025-12-28": { "status": "work", "start": "10:00", "end": "18:00" }, - "2025-12-29": { "status": "off" } + "2025-12-29": { "status": "off" }, + "2025-12-30": { "status": "work", "start": "12:00", "end": "20:00" }, + "2025-12-31": { "status": "off" }, + "2026-01-01": { "status": "off" }, + "2026-01-02": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-03": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-04": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-05": { "status": "off" }, + "2026-01-06": { "status": "off" }, + "2026-01-07": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-08": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-09": { "status": "off" }, + "2026-01-10": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-11": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-12": { "status": "off" }, + "2026-01-13": { "status": "off" }, + "2026-01-14": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-15": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-16": { "status": "off" }, + "2026-01-17": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-18": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-19": { "status": "off" }, + "2026-01-20": { "status": "off" }, + "2026-01-21": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-22": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-23": { "status": "off" }, + "2026-01-24": { "status": "work", "start": "12:00", "end": "20:00" }, + "2026-01-25": { "status": "work", "start": "10:00", "end": "18:00" }, + "2026-01-26": { "status": "off" } } }, { - "employeeId": "emp-5", + "employeeId": "employee-5", "name": "Sofie Andersen", "weeklyHours": 20, "schedule": { @@ -72,7 +184,35 @@ "2025-12-26": { "status": "off" }, "2025-12-27": { "status": "work", "start": "09:00", "end": "15:00" }, "2025-12-28": { "status": "off" }, - "2025-12-29": { "status": "off" } + "2025-12-29": { "status": "off" }, + "2025-12-30": { "status": "work", "start": "09:00", "end": "15:00" }, + "2025-12-31": { "status": "off" }, + "2026-01-01": { "status": "off" }, + "2026-01-02": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-03": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-04": { "status": "off" }, + "2026-01-05": { "status": "off" }, + "2026-01-06": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-07": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-08": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-09": { "status": "off" }, + "2026-01-10": { "status": "off" }, + "2026-01-11": { "status": "off" }, + "2026-01-12": { "status": "off" }, + "2026-01-13": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-14": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-15": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-16": { "status": "off" }, + "2026-01-17": { "status": "off" }, + "2026-01-18": { "status": "off" }, + "2026-01-19": { "status": "off" }, + "2026-01-20": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-21": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-22": { "status": "work", "start": "09:00", "end": "15:00" }, + "2026-01-23": { "status": "off" }, + "2026-01-24": { "status": "off" }, + "2026-01-25": { "status": "off" }, + "2026-01-26": { "status": "off" } } } ] diff --git a/PlanTempus.Application/Features/Localization/Translations/da.json b/PlanTempus.Application/Features/Localization/Translations/da.json index 1cdf225..d3efb9f 100644 --- a/PlanTempus.Application/Features/Localization/Translations/da.json +++ b/PlanTempus.Application/Features/Localization/Translations/da.json @@ -317,6 +317,7 @@ "rating": "rating", "employedsince": "ansat siden", "hours": { + "label": "timer", "weekly": "Ugentlig arbejdstid", "monday": "Mandag", "tuesday": "Tirsdag", @@ -409,8 +410,8 @@ "desc": "Kunder kan vælge denne medarbejder" }, "smsreminders": { - "label": "Modtag SMS-påmindelser", - "desc": "Få besked om nye bookinger" + "label": "Få notifikation via App'en om nye bookinger", + "desc": "" }, "editcalendar": { "label": "Kan redigere egen kalender", @@ -419,12 +420,12 @@ }, "notifications": { "label": "Notifikationer", - "intro": "Vælg hvilke email-notifikationer medarbejderen skal modtage.", - "onlinebooking": "Modtag email ved online booking", - "manualbooking": "Modtag email ved manuel booking", - "cancellation": "Modtag email ved aflysning", - "waitlist": "Modtag email ved opskrivning til venteliste", - "dailysummary": "Modtag daglig oversigt over morgendagens bookinger" + "intro": "Vælg hvilke notifikationer der skal sendes.", + "onlinebooking": "Email ved online booking", + "manualbooking": "Email ved manuel booking", + "cancellation": "Email ved aflysning", + "waitlist": "Email ved opskrivning til venteliste", + "dailysummary": "Email med daglig oversigt" } } } diff --git a/PlanTempus.Application/Features/Localization/Translations/en.json b/PlanTempus.Application/Features/Localization/Translations/en.json index 5dc90f0..dec65aa 100644 --- a/PlanTempus.Application/Features/Localization/Translations/en.json +++ b/PlanTempus.Application/Features/Localization/Translations/en.json @@ -317,6 +317,7 @@ "rating": "rating", "employedsince": "employed since", "hours": { + "label": "hours", "weekly": "Weekly working hours", "monday": "Monday", "tuesday": "Tuesday", @@ -409,8 +410,8 @@ "desc": "Customers can select this employee" }, "smsreminders": { - "label": "Receive SMS reminders", - "desc": "Get notified about new bookings" + "label": "Get notified via the App about new bookings", + "desc": "" }, "editcalendar": { "label": "Can edit own calendar", @@ -419,12 +420,12 @@ }, "notifications": { "label": "Notifications", - "intro": "Choose which email notifications the employee should receive.", - "onlinebooking": "Receive email for online booking", - "manualbooking": "Receive email for manual booking", - "cancellation": "Receive email for cancellation", - "waitlist": "Receive email for waitlist signup", - "dailysummary": "Receive daily summary of tomorrow's bookings" + "intro": "Choose which notifications to send.", + "onlinebooking": "Email on online booking", + "manualbooking": "Email on manual booking", + "cancellation": "Email on cancellation", + "waitlist": "Email on waitlist signup", + "dailysummary": "Email with daily summary" } } } diff --git a/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml b/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml index 9e42620..dd9c3bb 100644 --- a/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml +++ b/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml @@ -35,7 +35,7 @@ - + diff --git a/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml index 68f1020..5c44e33 100644 --- a/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml +++ b/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml @@ -35,7 +35,7 @@ - + diff --git a/PlanTempus.Application/tmpclaude-7b53-cwd b/PlanTempus.Application/tmpclaude-7b53-cwd deleted file mode 100644 index b31e703..0000000 --- a/PlanTempus.Application/tmpclaude-7b53-cwd +++ /dev/null @@ -1 +0,0 @@ -/c/Users/Janus Knudsen/source/swp-repos/PlanTempus/PlanTempus.Application diff --git a/PlanTempus.Application/wwwroot/css/controls.css b/PlanTempus.Application/wwwroot/css/controls.css index 01c3985..ef9f927 100644 --- a/PlanTempus.Application/wwwroot/css/controls.css +++ b/PlanTempus.Application/wwwroot/css/controls.css @@ -35,7 +35,7 @@ swp-toggle-slider { display: inline-flex; width: fit-content; background: var(--color-background); - border-radius: var(--radius-md); + border-radius: 6px; border: 1px solid var(--color-border); overflow: hidden; position: relative; @@ -48,36 +48,71 @@ swp-toggle-slider::before { left: 2px; width: calc(50% - 4px); height: calc(100% - 4px); - background: var(--bg-green-strong); - border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--color-green) 18%, white); + border-radius: 4px; transition: transform 200ms ease, background 200ms ease; } swp-toggle-slider[data-value="no"]::before { - transform: translateX(100%); - background: var(--bg-red-strong); + transform: translateX(calc(100% + 4px)); + background: color-mix(in srgb, var(--color-red) 18%, white); } swp-toggle-option { position: relative; z-index: 1; - padding: var(--spacing-2) var(--spacing-5); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); + padding: 5px 16px; + font-size: 12px; + font-weight: 500; color: var(--color-text-secondary); cursor: pointer; - transition: color var(--transition-fast); + transition: color 150ms ease; user-select: none; } swp-toggle-slider[data-value="yes"] swp-toggle-option:first-child { color: var(--color-green); - font-weight: var(--font-weight-semibold); + font-weight: 600; } swp-toggle-slider[data-value="no"] swp-toggle-option:last-child { color: var(--color-red); - font-weight: var(--font-weight-semibold); + font-weight: 600; +} + +/* =========================================== + TOGGLE OPTIONS (Tab-style selector) + =========================================== */ +swp-toggle-options { + display: flex; + gap: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + overflow: hidden; + + swp-toggle-option { + flex: 1; + padding: 10px 16px; + text-align: center; + font-size: var(--font-size-sm); + cursor: pointer; + background: var(--color-surface); + border-right: 1px solid var(--color-border); + transition: all var(--transition-fast); + + &:last-child { + border-right: none; + } + + &:hover { + background: var(--color-background-alt); + } + + &.selected { + background: var(--color-teal); + color: white; + } + } } /* =========================================== diff --git a/PlanTempus.Application/wwwroot/css/drawers.css b/PlanTempus.Application/wwwroot/css/drawers.css index 902ae5d..a8c1b2e 100644 --- a/PlanTempus.Application/wwwroot/css/drawers.css +++ b/PlanTempus.Application/wwwroot/css/drawers.css @@ -227,7 +227,7 @@ swp-toggle-switch input { height: 0; } -swp-toggle-slider { +swp-toggle-track { position: absolute; cursor: pointer; inset: 0; @@ -236,7 +236,7 @@ swp-toggle-slider { transition: background var(--transition-fast); } -swp-toggle-slider::before { +swp-toggle-track::before { content: ''; position: absolute; width: 18px; @@ -248,11 +248,11 @@ swp-toggle-slider::before { transition: transform var(--transition-fast); } -swp-toggle-switch input:checked + swp-toggle-slider { +swp-toggle-switch input:checked + swp-toggle-track { background: var(--color-teal); } -swp-toggle-switch input:checked + swp-toggle-slider::before { +swp-toggle-switch input:checked + swp-toggle-track::before { transform: translateX(20px); } diff --git a/PlanTempus.Application/wwwroot/css/employees.css b/PlanTempus.Application/wwwroot/css/employees.css index 6bef67b..f62013d 100644 --- a/PlanTempus.Application/wwwroot/css/employees.css +++ b/PlanTempus.Application/wwwroot/css/employees.css @@ -889,11 +889,10 @@ swp-schedule-scroll { overflow-x: auto; } -/* Når drawer er åben: page-container styles (animation via JS) */ +/* Når drawer er åben: page-container styles (padding animeres via JS) */ body.schedule-drawer-open swp-tab-content[data-tab="schedule"] swp-page-container { max-width: none; margin: 0; - padding-right: var(--drawer-width, 420px); } /* =========================================== @@ -975,7 +974,7 @@ swp-schedule-cell.day.closed-day { border-left: 2px solid #f59e0b; border-right: 2px solid #f59e0b; - swp-time-display { + swp-time-badge { opacity: 0.5; } } @@ -1011,13 +1010,13 @@ swp-day-date { } /* Time display variants */ -swp-time-display { +swp-time-badge { font-family: var(--font-mono); font-size: 12px; - font-weight: var(--font-weight-medium); + font-weight: 500; padding: 4px 8px; border-radius: 4px; - background: var(--bg-teal-light); + background: color-mix(in srgb, var(--color-teal) 10%, white); color: var(--color-text); white-space: nowrap; min-width: 90px; @@ -1025,22 +1024,22 @@ swp-time-display { display: inline-block; } -swp-time-display.off { +swp-time-badge.off { background: transparent; color: var(--color-text-muted); } -swp-time-display.off.off-override { +swp-time-badge.off.off-override { background: color-mix(in srgb, #7c3aed 12%, white); color: #6d28d9; } -swp-time-display.vacation { +swp-time-badge.vacation { background: color-mix(in srgb, #f59e0b 15%, white); color: #b45309; } -swp-time-display.sick { +swp-time-badge.sick { background: color-mix(in srgb, #ef4444 15%, white); color: #dc2626; } @@ -1222,38 +1221,6 @@ swp-time-range-duration { white-space: nowrap; } -/* Toggle options (Enkelt/Gentagelse) */ -swp-toggle-options { - display: flex; - gap: 0; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - overflow: hidden; -} - -swp-toggle-option { - flex: 1; - padding: 10px 16px; - text-align: center; - font-size: var(--font-size-sm); - cursor: pointer; - background: var(--color-surface); - border-right: 1px solid var(--color-border); - transition: all var(--transition-fast); - - &:last-child { - border-right: none; - } - - &:hover { - background: var(--color-background-alt); - } - - &.selected { - background: var(--color-teal); - color: white; - } -} /* Schedule drawer employee display */ swp-employee-display { diff --git a/PlanTempus.Application/wwwroot/ts/app.ts b/PlanTempus.Application/wwwroot/ts/app.ts index f8067d2..d07676f 100644 --- a/PlanTempus.Application/wwwroot/ts/app.ts +++ b/PlanTempus.Application/wwwroot/ts/app.ts @@ -11,6 +11,7 @@ import { SearchController } from './modules/search'; import { LockScreenController } from './modules/lockscreen'; import { CashController } from './modules/cash'; import { EmployeesController } from './modules/employees'; +import { ControlsController } from './modules/controls'; /** * Main application class @@ -23,6 +24,7 @@ export class App { readonly lockScreen: LockScreenController; readonly cash: CashController; readonly employees: EmployeesController; + readonly controls: ControlsController; constructor() { // Initialize controllers @@ -33,6 +35,7 @@ export class App { this.lockScreen = new LockScreenController(this.drawers); this.cash = new CashController(); this.employees = new EmployeesController(); + this.controls = new ControlsController(); } } diff --git a/PlanTempus.Application/wwwroot/ts/modules/controls.ts b/PlanTempus.Application/wwwroot/ts/modules/controls.ts new file mode 100644 index 0000000..d9366fe --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/controls.ts @@ -0,0 +1,36 @@ +/** + * Controls Module + * + * Handles generic UI controls functionality: + * - Toggle sliders (Ja/Nej switches) + */ + +/** + * Controller for generic UI controls + */ +export class ControlsController { + constructor() { + this.initToggleSliders(); + } + + /** + * Initialize all toggle sliders on the page + * Toggle slider: Ja/Nej button switch with data-value attribute + * Clicking anywhere on the slider toggles the value + */ + private initToggleSliders(): void { + document.querySelectorAll('swp-toggle-slider').forEach(slider => { + slider.addEventListener('click', () => { + const el = slider as HTMLElement; + const newValue = el.dataset.value === 'yes' ? 'no' : 'yes'; + el.dataset.value = newValue; + + // Dispatch custom event for listeners + slider.dispatchEvent(new CustomEvent('toggle', { + bubbles: true, + detail: { value: newValue } + })); + }); + }); + } +} diff --git a/PlanTempus.Application/wwwroot/ts/modules/employees.ts b/PlanTempus.Application/wwwroot/ts/modules/employees.ts index be31799..6e09d35 100644 --- a/PlanTempus.Application/wwwroot/ts/modules/employees.ts +++ b/PlanTempus.Application/wwwroot/ts/modules/employees.ts @@ -1028,55 +1028,66 @@ class ScheduleController { * Open the schedule drawer (no overlay - user can still interact with table) */ private openDrawer(): void { - // Lås tabelbredde før drawer åbner for at undgå "hop" + const container = document.querySelector('swp-tab-content[data-tab="schedule"] swp-page-container') as HTMLElement; const table = document.getElementById('scheduleTable'); + + // Gem nuværende padding FØR klasser tilføjes + const startPadding = container ? getComputedStyle(container).paddingRight : '0px'; + + // Lås tabelbredde før drawer åbner for at undgå "hop" if (table) { const rect = table.getBoundingClientRect(); table.style.width = `${rect.width}px`; } - // Animate container med Web Animations API - const container = document.querySelector('swp-tab-content[data-tab="schedule"] swp-page-container') as HTMLElement; - if (container) { - const currentStyles = getComputedStyle(container); - const currentMaxWidth = currentStyles.maxWidth; - const currentMargin = currentStyles.margin; - const currentPaddingRight = currentStyles.paddingRight; + // Tilføj klasser med det samme (maxWidth og margin ændres instant) + this.drawer?.classList.add('open'); + document.body.classList.add('schedule-drawer-open'); + // Animate kun padding fra gemt værdi + if (container) { container.animate([ - { maxWidth: currentMaxWidth, margin: currentMargin, paddingRight: currentPaddingRight }, - { maxWidth: 'none', margin: '0px', paddingRight: '420px' } + { paddingRight: startPadding }, + { paddingRight: '420px' } ], { - duration: 300, + duration: 200, easing: 'ease', fill: 'forwards' }); } - - this.drawer?.classList.add('open'); - document.body.classList.add('schedule-drawer-open'); } /** * Close the schedule drawer */ private closeDrawer(): void { + // Luk drawer med det samme (visuelt) + this.drawer?.classList.remove('open'); + const container = document.querySelector('swp-tab-content[data-tab="schedule"] swp-page-container') as HTMLElement; + const table = document.getElementById('scheduleTable'); + if (container) { - // Hent nuværende computed styles for animation const animation = container.getAnimations()[0]; if (animation) { - animation.cancel(); + // Afspil animationen baglæns + animation.reverse(); + animation.onfinish = () => { + animation.cancel(); + // Fjern klasser og låst bredde når animation er færdig + document.body.classList.remove('schedule-drawer-open'); + if (table) { + table.style.width = ''; + } + }; + return; // Exit early - cleanup happens in onfinish } } - // Fjern låst bredde så tabellen kan tilpasse sig igen - const table = document.getElementById('scheduleTable'); + // Ingen animation, fjern klasser og låst bredde med det samme + document.body.classList.remove('schedule-drawer-open'); if (table) { table.style.width = ''; } - - this.drawer?.classList.remove('open'); - document.body.classList.remove('schedule-drawer-open'); } } diff --git a/PlanTempus.Application/wwwroot/ts/modules/theme.ts b/PlanTempus.Application/wwwroot/ts/modules/theme.ts index b14e4b5..9f7b562 100644 --- a/PlanTempus.Application/wwwroot/ts/modules/theme.ts +++ b/PlanTempus.Application/wwwroot/ts/modules/theme.ts @@ -13,10 +13,12 @@ export class ThemeController { private root: HTMLElement; private themeOptions: NodeListOf; + private themeCheckbox: HTMLInputElement | null; constructor() { this.root = document.documentElement; this.themeOptions = document.querySelectorAll('swp-theme-option'); + this.themeCheckbox = document.getElementById('themeCheckbox') as HTMLInputElement | null; this.applyTheme(this.current); this.updateUI(); @@ -77,15 +79,19 @@ export class ThemeController { } private updateUI(): void { - if (!this.themeOptions) return; - const darkActive = this.isDark; - this.themeOptions.forEach(option => { + // Update theme options + this.themeOptions?.forEach(option => { const theme = option.dataset.theme as Theme; const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive); option.classList.toggle('active', isActive); }); + + // Update checkbox (checked = dark mode) + if (this.themeCheckbox) { + this.themeCheckbox.checked = darkActive; + } } private setupListeners(): void { @@ -94,6 +100,11 @@ export class ThemeController { option.addEventListener('click', (e) => this.handleOptionClick(e)); }); + // Theme checkbox toggle + this.themeCheckbox?.addEventListener('change', () => { + this.set(this.themeCheckbox!.checked ? 'dark' : 'light'); + }); + // System theme changes window.matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => this.handleSystemChange());