Implement responsive mobile/desktop UI layout for shift scheduler (#38)

* Initial plan

* Implement responsive mobile/desktop UI layout

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* Clean up demo files and finalize mobile UI implementation

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* Fix desktop calendar week grouping to properly start weeks on Monday and end on Sunday

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* Remove button for shift delete

* Fix desktop week grouping to start weeks on Monday instead of grouping consecutive days

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* UI improvements

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>
Co-authored-by: Claudio Schaad <c.schaad@pog.ch>
This commit is contained in:
Copilot 2025-08-29 20:41:25 +02:00 committed by GitHub
parent a9c5937ce4
commit 1493c459bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 231 additions and 48 deletions

View file

@ -3,11 +3,10 @@
<div class="top-nav-buttons"> <div class="top-nav-buttons">
<div class="month-selection"> <div class="month-selection">
<button class="btn month-btn @(_isCurrentMonth ? "" : "selected")" @onclick="SelectNextMonth" disabled="@_isLoadingInitial">Next Month</button> <button class="btn month-btn @(_isCurrentMonth ? "selected" : "")" @onclick="SelectCurrentMonth" disabled="@_isLoadingInitial">@MonthAndYear.CurrentMonthName</button>
<button class="btn month-btn @(_isCurrentMonth ? "selected" : "")" @onclick="SelectCurrentMonth" disabled="@_isLoadingInitial">Current Month</button> <button class="btn month-btn @(_isCurrentMonth ? "" : "selected")" @onclick="SelectNextMonth" disabled="@_isLoadingInitial">@MonthAndYear.NextMonthName</button>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button class="reset-btn" @onclick="ResetSchedule" disabled="@_isLoadingInitial">Reset Schedule</button>
<button class="btn export-btn" @onclick="ExportToIcs" disabled="@_isLoadingInitial">Export to ICS</button> <button class="btn export-btn" @onclick="ExportToIcs" disabled="@_isLoadingInitial">Export to ICS</button>
<button class="btn export-btn" @onclick="ExportToPdf" disabled="@_isLoadingInitial">Export to PDF</button> <button class="btn export-btn" @onclick="ExportToPdf" disabled="@_isLoadingInitial">Export to PDF</button>
<button class="btn config-btn" @onclick="ShowConfiguration" disabled="@_isLoadingInitial">⚙️ Configuration</button> <button class="btn config-btn" @onclick="ShowConfiguration" disabled="@_isLoadingInitial">⚙️ Configuration</button>
@ -44,7 +43,60 @@
else else
{ {
<div class="calendar-grid"> <div class="calendar-container">
<!-- Desktop week-based layout -->
<div class="calendar-grid desktop-calendar">
@foreach (var week in SelectedDate.GetWeeksInMonth())
{
@foreach (var day in week.Value)
{
<div class="day-cell">
<div class="day-header">
@day.ToString("dd.MM.yyyy") (@day.ToString("dddd"))
</div>
<div class="shifts">
@foreach (var shift in Shifts)
{
<div class="tooltip-wrapper">
<button class="shift-button @(SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name ? "selected" : "")"
@onclick="() => SelectShift(day, shift.Name)"
disabled="@_isLoadingTransport">
@if (_isLoadingTransport && SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name)
{
<span class="loading-spinner">⏳</span>
}
else
{
@if (shift.IsPngIcon)
{
<img src="icons\@shift.Icon" alt="@shift.Name" style="width: 24px; height: 24px;" />
}
else
{
@shift.Icon
}
}
</button>
<span class="tooltip">@shift.Name</span>
</div>
}
</div>
@if (SelectedSchedule.ContainsKey(day))
{
<div class="shift-times">
@GetShiftTimes(day)
</div>
<div class="transport-info">
@GetTransportSummary(day)
</div>
}
</div>
}
}
</div>
<!-- Mobile single-column layout -->
<div class="calendar-grid mobile-calendar">
@foreach (var day in SelectedDate.DaysInMonth()) @foreach (var day in SelectedDate.DaysInMonth())
{ {
<div class="day-cell"> <div class="day-cell">
@ -81,14 +133,15 @@ else
@if (SelectedSchedule.ContainsKey(day)) @if (SelectedSchedule.ContainsKey(day))
{ {
<div class="shift-times"> <div class="shift-times">
<small>@GetShiftTimes(day)</small> @GetShiftTimes(day)
</div> </div>
<div class="transport-info"> <div class="transport-info">
<small>@GetTransportSummary(day)</small> @GetTransportSummary(day)
</div> </div>
} }
</div> </div>
} }
</div>
</div> </div>
} }

View file

@ -320,5 +320,35 @@ namespace ShiftScheduler.Client.Pages
Shifts = await HttpClient.GetFromJsonAsync<List<Shift>>("api/shift/shifts") ?? new(); Shifts = await HttpClient.GetFromJsonAsync<List<Shift>>("api/shift/shifts") ?? new();
StateHasChanged(); StateHasChanged();
} }
// Helper method to group days by week for desktop calendar view
// Each week starts with Monday and ends with Sunday
private List<List<DateTime>> GetWeeksInMonth()
{
var days = SelectedDate.DaysInMonth();
var weeks = new List<List<DateTime>>();
var currentWeek = new List<DateTime>();
foreach (var day in days)
{
// Start a new week if this is Monday and we have days in current week
if (day.DayOfWeek == DayOfWeek.Monday && currentWeek.Count > 0)
{
weeks.Add(currentWeek);
currentWeek = new List<DateTime>();
}
// Add the day to current week
currentWeek.Add(day);
}
// Add any remaining days as the last week
if (currentWeek.Count > 0)
{
weeks.Add(currentWeek);
}
return weeks;
}
} }
} }

View file

@ -6,6 +6,26 @@
margin-bottom: 1rem; margin-bottom: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
/* Make buttons scrollable instead of fixed */
position: static;
overflow: visible;
}
/* Mobile top navigation - stack vertically */
@media (max-width: 768px) {
.top-nav-buttons {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.month-selection {
justify-content: center;
}
.action-buttons {
justify-content: center;
}
} }
/* Month selection buttons */ /* Month selection buttons */
@ -75,11 +95,43 @@
} }
/* Calendar container */ /* Calendar container */
.calendar-container {
margin-top: 1rem;
}
.calendar-grid { .calendar-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; gap: 1rem;
margin-top: 1rem; }
/* Desktop calendar grid - week-based layout with 7 columns max */
.desktop-calendar {
grid-template-columns: repeat(7, 1fr);
}
/* Mobile calendar grid - single column */
.mobile-calendar {
grid-template-columns: 1fr;
}
/* Show desktop calendar only on larger screens */
@media (min-width: 769px) {
.desktop-calendar {
display: grid;
}
.mobile-calendar {
display: none;
}
}
/* Show mobile calendar only on smaller screens */
@media (max-width: 768px) {
.desktop-calendar {
display: none;
}
.mobile-calendar {
display: grid;
}
} }
/* Each day cell */ /* Each day cell */
@ -91,6 +143,14 @@
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
/* Mobile day cell adjustments */
@media (max-width: 768px) {
.day-cell {
padding: 0.75rem;
margin-bottom: 0.5rem;
}
}
/* Day number */ /* Day number */
.day-header { .day-header {
font-weight: bold; font-weight: bold;
@ -102,11 +162,25 @@
/* Shift buttons container */ /* Shift buttons container */
.shifts { .shifts {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
/* Desktop shift buttons - 3 columns */
@media (min-width: 769px) {
.shifts {
grid-template-columns: repeat(3, 1fr);
}
}
/* Mobile shift buttons - single row */
@media (max-width: 768px) {
.shifts {
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
gap: 0.25rem;
}
}
/* Individual shift buttons */ /* Individual shift buttons */
.shift-button { .shift-button {
border: 2px solid #ddd; border: 2px solid #ddd;
@ -123,6 +197,15 @@
justify-content: center; justify-content: center;
} }
/* Mobile shift button adjustments */
@media (max-width: 768px) {
.shift-button {
min-height: 2rem;
font-size: 1rem;
padding: 0.4rem;
}
}
.shift-button:hover { .shift-button:hover {
background-color: #f8f9fa; background-color: #f8f9fa;
border-color: #007bff; border-color: #007bff;

View file

@ -1,8 +1,11 @@
namespace ShiftScheduler.Client.Pages; using System.Globalization;
namespace ShiftScheduler.Client.Pages;
public record MonthAndYear(int Month, int Year) public record MonthAndYear(int Month, int Year)
{ {
public static MonthAndYear Current() => new MonthAndYear(DateTime.Now.Month, DateTime.Now.Year); public static MonthAndYear Current() => new (DateTime.Now.Month, DateTime.Now.Year);
public static MonthAndYear Next() public static MonthAndYear Next()
{ {
var today = DateTime.Today; var today = DateTime.Today;
@ -11,7 +14,12 @@ public record MonthAndYear(int Month, int Year)
return new MonthAndYear(nextMonth, year); return new MonthAndYear(nextMonth, year);
} }
public string Title => new DateTime(Year, Month, 1).ToString("MMMM yyyy"); public static string CurrentMonthName => Current().ToDateTime.ToString("MMMM");
public static string NextMonthName => Next().ToDateTime.ToString("MMMM");
public string Title => ToDateTime.ToString("MMMM yyyy");
private DateTime ToDateTime => new DateTime(Year, Month, 1);
public IReadOnlyList<DateTime> DaysInMonth() public IReadOnlyList<DateTime> DaysInMonth()
{ {
@ -19,4 +27,13 @@ public record MonthAndYear(int Month, int Year)
.Select(d => new DateTime(Year, Month, d)) .Select(d => new DateTime(Year, Month, d))
.ToList(); .ToList();
} }
public IReadOnlyDictionary<int, List<DateTime>> GetWeeksInMonth()
{
var days = DaysInMonth();
var weeks = days
.GroupBy(d => CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(d, CalendarWeekRule.FirstDay, DayOfWeek.Monday))
.ToDictionary(d => d.Key, d => d.ToList());
return weeks;
}
} }