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:
parent
a9c5937ce4
commit
1493c459bd
4 changed files with 231 additions and 48 deletions
|
|
@ -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,54 +43,108 @@
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
||||||
<div class="calendar-grid">
|
<div class="calendar-container">
|
||||||
@foreach (var day in SelectedDate.DaysInMonth())
|
<!-- Desktop week-based layout -->
|
||||||
{
|
<div class="calendar-grid desktop-calendar">
|
||||||
<div class="day-cell">
|
@foreach (var week in SelectedDate.GetWeeksInMonth())
|
||||||
<div class="day-header">
|
{
|
||||||
@day.ToString("dd.MM.yyyy") (@day.ToString("dddd"))
|
@foreach (var day in week.Value)
|
||||||
</div>
|
{
|
||||||
<div class="shifts">
|
<div class="day-cell">
|
||||||
@foreach (var shift in Shifts)
|
<div class="day-header">
|
||||||
{
|
@day.ToString("dd.MM.yyyy") (@day.ToString("dddd"))
|
||||||
<div class="tooltip-wrapper">
|
</div>
|
||||||
<button class="shift-button @(SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name ? "selected" : "")"
|
<div class="shifts">
|
||||||
@onclick="() => SelectShift(day, shift.Name)"
|
@foreach (var shift in Shifts)
|
||||||
disabled="@_isLoadingTransport">
|
{
|
||||||
@if (_isLoadingTransport && SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name)
|
<div class="tooltip-wrapper">
|
||||||
{
|
<button class="shift-button @(SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name ? "selected" : "")"
|
||||||
<span class="loading-spinner">⏳</span>
|
@onclick="() => SelectShift(day, shift.Name)"
|
||||||
}
|
disabled="@_isLoadingTransport">
|
||||||
else
|
@if (_isLoadingTransport && SelectedSchedule.ContainsKey(day) && SelectedSchedule[day] == shift.Name)
|
||||||
{
|
{
|
||||||
@if (shift.IsPngIcon)
|
<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())
|
||||||
|
{
|
||||||
|
<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)
|
||||||
{
|
{
|
||||||
<img src="icons\@shift.Icon" alt="@shift.Name" style="width: 24px; height: 24px;" />
|
<span class="loading-spinner">⏳</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@shift.Icon
|
@if (shift.IsPngIcon)
|
||||||
|
{
|
||||||
|
<img src="icons\@shift.Icon" alt="@shift.Name" style="width: 24px; height: 24px;" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@shift.Icon
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
</button>
|
||||||
</button>
|
<span class="tooltip">@shift.Name</span>
|
||||||
<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>
|
</div>
|
||||||
@if (SelectedSchedule.ContainsKey(day))
|
}
|
||||||
{
|
</div>
|
||||||
<div class="shift-times">
|
|
||||||
<small>@GetShiftTimes(day)</small>
|
|
||||||
</div>
|
|
||||||
<div class="transport-info">
|
|
||||||
<small>@GetTransportSummary(day)</small>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfigurationDialog IsVisible="_showConfigDialog"
|
<ConfigurationDialog IsVisible="_showConfigDialog"
|
||||||
OnClose="HideConfiguration"
|
OnClose="HideConfiguration"
|
||||||
OnConfigurationChanged="OnConfigurationChanged" />
|
OnConfigurationChanged="OnConfigurationChanged" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue