feat: add WorkUnitEditorModal component for managing work units
- Implemented WorkUnitEditorModal.razor for creating and editing work units. - Added necessary services and parameters for data handling. - Included computed values for calculated hours, gross income, and net income. - Enhanced UI with modal structure and styling. fix: update _Imports.razor to include Shared components - Added reference to WorkUnitEditorModal in _Imports.razor for accessibility. feat: extend CalendarEventDocument with StartDate and EndDate properties - Updated CalendarEventDocument.cs to include StartDate and EndDate for better event management. feat: create CalendarEventFormatter for event description formatting - Introduced CalendarEventFormatter.cs to handle display logic for calendar events. fix: enhance CouchbaseLiteWorkDayService for calendar event management - Updated methods to handle new StartDate and EndDate properties in calendar events. - Improved event saving and deletion logic. test: add Playwright tests for date locale functionality - Created date-locale.spec.ts to verify date picker behavior and formatting. style: enhance app.css with modal and date input styles - Added styles for calendar modal, date input, and related components for improved UI.
This commit is contained in:
parent
0d003903cf
commit
bc28d869eb
14 changed files with 1638 additions and 150 deletions
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
@inject IWorkDayService WorkDayService
|
||||
@inject IItalianFestivitySource FestivitySource
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<PageTitle>Calendar</PageTitle>
|
||||
|
|
@ -50,76 +49,75 @@ else
|
|||
<tr>
|
||||
@foreach (var cell in week)
|
||||
{
|
||||
@if (cell is null)
|
||||
{
|
||||
<td class="calendar-cell calendar-cell-empty"></td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="calendar-cell @GetCellClass(cell) @(IsToday(cell.Date) ? "calendar-cell-today" : string.Empty) @(IsActiveCell(cell.Date) ? "calendar-cell-active" : string.Empty)" @onclick="() => TogglePopup(cell.Date)" role="button">
|
||||
<div class="calendar-day-number">@cell.Date.Day</div>
|
||||
|
||||
@foreach (var workUnit in cell.Entry?.WorkUnits ?? [])
|
||||
<td class="calendar-cell @GetCellClass(cell) @(cell.IsCurrentMonth ? string.Empty : "calendar-cell-outside-month") @(IsToday(cell.Date) ? "calendar-cell-today" : string.Empty) @(IsActiveCell(cell.Date) ? "calendar-cell-active" : string.Empty)" @onclick="() => TogglePopup(cell.Date)" role="button">
|
||||
<div class="d-flex align-items-start justify-content-between gap-2">
|
||||
<div class="calendar-day-number @(cell.IsCurrentMonth ? string.Empty : "calendar-day-number-outside")">@cell.Date.Day</div>
|
||||
@if (!cell.IsCurrentMonth)
|
||||
{
|
||||
<button type="button" class="calendar-item calendar-item-work @GetWorkUnitClass(workUnit)" @onclick:stopPropagation="true" @onclick="() => OpenWorkUnit(cell.Date, workUnit.Id)">
|
||||
<span>@workUnit.Label</span>
|
||||
<span>@FormatWorkUnit(workUnit)</span>
|
||||
</button>
|
||||
<div class="calendar-day-month-label">@cell.Date.ToString("MMM")</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@foreach (var calendarEvent in cell.Entry?.CalendarEvents ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-item calendar-item-event @GetCalendarEventClass(calendarEvent)" @onclick:stopPropagation="true" @onclick="() => OpenCalendarEvent(cell.Date, calendarEvent.Id)">
|
||||
<span>@calendarEvent.Description</span>
|
||||
@if (calendarEvent.StartTime.HasValue)
|
||||
{
|
||||
<span>@calendarEvent.StartTime.Value.ToString("HH:mm")</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@foreach (var workUnit in cell.Entry?.WorkUnits ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-item calendar-item-work @GetWorkUnitClass(workUnit)" @onclick:stopPropagation="true" @onclick="() => OpenWorkUnit(cell.Date, workUnit.Id)">
|
||||
<span>@workUnit.Label</span>
|
||||
<span>@FormatWorkUnit(workUnit)</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<div class="calendar-day-total">@FormatHours(GetDayTotalHours(cell.Entry, includePreviewTotals))</div>
|
||||
@foreach (var calendarEvent in cell.Entry?.CalendarEvents ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-item calendar-item-event @GetCalendarEventClass(calendarEvent)" @onclick:stopPropagation="true" @onclick="() => OpenCalendarEvent(GetCalendarEventOwnerDate(calendarEvent, cell.Date), calendarEvent.Id)">
|
||||
<span>@CalendarEventFormatter.GetDisplayDescription(calendarEvent)</span>
|
||||
@if (calendarEvent.StartTime.HasValue)
|
||||
{
|
||||
<span>@calendarEvent.StartTime.Value.ToString("HH:mm")</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (IsActiveCell(cell.Date))
|
||||
{
|
||||
<div class="calendar-popup @GetPopupClass(cell)" @onclick:stopPropagation="true">
|
||||
<div class="d-flex align-items-start justify-content-between gap-2 mb-2">
|
||||
<div>
|
||||
<div class="fw-semibold">@cell.Date.ToString("dddd d MMMM")</div>
|
||||
<div class="small text-muted">Select an existing entry or create a new one.</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-sm" aria-label="Close" @onclick="ClosePopup"></button>
|
||||
</div>
|
||||
|
||||
<div class="calendar-popup-section">
|
||||
@if ((cell.Entry?.WorkUnits.Count ?? 0) == 0 && (cell.Entry?.CalendarEvents.Count ?? 0) == 0)
|
||||
{
|
||||
<div class="small text-muted">No entries for this day.</div>
|
||||
}
|
||||
|
||||
@foreach (var workUnit in cell.Entry?.WorkUnits ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-popup-link" @onclick="() => OpenWorkUnit(cell.Date, workUnit.Id)">
|
||||
Work unit: @workUnit.Label (@FormatWorkUnit(workUnit))
|
||||
</button>
|
||||
}
|
||||
|
||||
@foreach (var calendarEvent in cell.Entry?.CalendarEvents ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-popup-link" @onclick="() => OpenCalendarEvent(cell.Date, calendarEvent.Id)">
|
||||
Calendar event: @calendarEvent.Description
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="button" class="btn btn-sm btn-primary" @onclick="() => CreateWorkUnit(cell.Date)">New Work Unit</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" @onclick="() => CreateCalendarEvent(cell.Date)">New Calendar Event</button>
|
||||
<div class="calendar-day-total">@FormatHours(GetDayTotalHours(cell.Entry, includePreviewTotals))</div>
|
||||
|
||||
@if (IsActiveCell(cell.Date))
|
||||
{
|
||||
<div class="calendar-popup @GetPopupClass(cell)" @onclick:stopPropagation="true">
|
||||
<div class="d-flex align-items-start justify-content-between gap-2 mb-2">
|
||||
<div>
|
||||
<div class="fw-semibold">@FormatDisplayDate(cell.Date)</div>
|
||||
<div class="small text-muted">Select an existing entry or create a new one.</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-sm" aria-label="Close" @onclick="ClosePopup"></button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
|
||||
<div class="calendar-popup-section">
|
||||
@if ((cell.Entry?.WorkUnits.Count ?? 0) == 0 && (cell.Entry?.CalendarEvents.Count ?? 0) == 0)
|
||||
{
|
||||
<div class="small text-muted">No entries for this day.</div>
|
||||
}
|
||||
|
||||
@foreach (var workUnit in cell.Entry?.WorkUnits ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-popup-link" @onclick="() => OpenWorkUnit(cell.Date, workUnit.Id)">
|
||||
Work unit: @workUnit.Label (@FormatWorkUnit(workUnit))
|
||||
</button>
|
||||
}
|
||||
|
||||
@foreach (var calendarEvent in cell.Entry?.CalendarEvents ?? [])
|
||||
{
|
||||
<button type="button" class="calendar-popup-link" @onclick="() => OpenCalendarEvent(GetCalendarEventOwnerDate(calendarEvent, cell.Date), calendarEvent.Id)">
|
||||
Calendar event: @CalendarEventFormatter.GetDisplayDescription(calendarEvent)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="button" class="btn btn-sm btn-primary" @onclick="() => CreateWorkUnit(cell.Date)">New Work Unit</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" @onclick="() => CreateCalendarEvent(cell.Date)">New Calendar Event</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
|
|
@ -194,6 +192,16 @@ else
|
|||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (workUnitModalDate.HasValue)
|
||||
{
|
||||
<WorkUnitEditorModal Date="@workUnitModalDate.Value" UnitId="@workUnitModalId" OnSaved="HandleEditorSavedAsync" OnClosed="CloseWorkUnitModal" />
|
||||
}
|
||||
|
||||
@if (calendarEventModalDate.HasValue)
|
||||
{
|
||||
<CalendarEventEditorModal Date="@calendarEventModalDate.Value" EventId="@calendarEventModalId" OnSaved="HandleEditorSavedAsync" OnClosed="CloseCalendarEventModal" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
|
@ -203,12 +211,16 @@ else
|
|||
|
||||
private DateOnly firstOfMonth;
|
||||
private bool loading = true;
|
||||
private List<CalendarCell?[]> weeks = [];
|
||||
private IReadOnlyCollection<DateOnly> festivities = [];
|
||||
private List<CalendarCell[]> weeks = [];
|
||||
private readonly Dictionary<int, IReadOnlyCollection<DateOnly>> festivitiesByYear = [];
|
||||
private DateOnly? activeDate;
|
||||
private bool includePreviewTotals;
|
||||
private MonthlySummaryModel? monthTotals;
|
||||
private string? statusMessage;
|
||||
private DateOnly? workUnitModalDate;
|
||||
private string? workUnitModalId;
|
||||
private DateOnly? calendarEventModalDate;
|
||||
private string? calendarEventModalId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
|
@ -244,41 +256,32 @@ else
|
|||
{
|
||||
loading = true;
|
||||
activeDate = null;
|
||||
festivities = FestivitySource.GetFestivities(firstOfMonth.Year);
|
||||
|
||||
var lastDay = firstOfMonth.AddMonths(1).AddDays(-1);
|
||||
var entries = await WorkDayService.GetRangeAsync(firstOfMonth, lastDay);
|
||||
var gridStart = GetGridStart(firstOfMonth);
|
||||
var gridEnd = GetGridEnd(lastDay);
|
||||
var entries = await WorkDayService.GetRangeAsync(gridStart, gridEnd);
|
||||
var lookup = entries.ToDictionary(e => e.Date);
|
||||
monthTotals = await WorkDayService.GetMonthlySummaryAsync(firstOfMonth.Year, firstOfMonth.Month, includePreviewTotals);
|
||||
|
||||
// Build calendar grid (ISO weeks: Monday = 0)
|
||||
weeks = [];
|
||||
var currentWeek = new CalendarCell?[7];
|
||||
var dayOfWeek = ((int)firstOfMonth.DayOfWeek + 6) % 7; // Mon=0
|
||||
|
||||
for (var d = firstOfMonth; d <= lastDay; d = d.AddDays(1))
|
||||
for (var weekStart = gridStart; weekStart <= gridEnd; weekStart = weekStart.AddDays(7))
|
||||
{
|
||||
currentWeek[dayOfWeek] = new CalendarCell
|
||||
var week = new CalendarCell[7];
|
||||
for (var columnIndex = 0; columnIndex < 7; columnIndex++)
|
||||
{
|
||||
Date = d,
|
||||
ColumnIndex = dayOfWeek,
|
||||
IsWeekend = d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
|
||||
IsFestivity = festivities.Contains(d),
|
||||
Entry = lookup.GetValueOrDefault(d)
|
||||
};
|
||||
|
||||
dayOfWeek++;
|
||||
if (dayOfWeek == 7)
|
||||
{
|
||||
weeks.Add(currentWeek);
|
||||
currentWeek = new CalendarCell?[7];
|
||||
dayOfWeek = 0;
|
||||
var date = weekStart.AddDays(columnIndex);
|
||||
week[columnIndex] = new CalendarCell
|
||||
{
|
||||
Date = date,
|
||||
ColumnIndex = columnIndex,
|
||||
IsCurrentMonth = date.Month == firstOfMonth.Month && date.Year == firstOfMonth.Year,
|
||||
IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
|
||||
IsFestivity = GetFestivities(date.Year).Contains(date),
|
||||
Entry = lookup.GetValueOrDefault(date)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (dayOfWeek > 0)
|
||||
{
|
||||
weeks.Add(currentWeek);
|
||||
weeks.Add(week);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
|
|
@ -331,22 +334,46 @@ else
|
|||
|
||||
private void CreateWorkUnit(DateOnly date)
|
||||
{
|
||||
Navigation.NavigateTo($"/work-unit/{date:yyyy-MM-dd}");
|
||||
OpenWorkUnit(date, null);
|
||||
}
|
||||
|
||||
private void CreateCalendarEvent(DateOnly date)
|
||||
{
|
||||
Navigation.NavigateTo($"/calendar-event/{date:yyyy-MM-dd}");
|
||||
OpenCalendarEvent(date, null);
|
||||
}
|
||||
|
||||
private void OpenWorkUnit(DateOnly date, string workUnitId)
|
||||
private void OpenWorkUnit(DateOnly date, string? workUnitId)
|
||||
{
|
||||
Navigation.NavigateTo($"/work-unit/{date:yyyy-MM-dd}/{workUnitId}");
|
||||
activeDate = null;
|
||||
workUnitModalDate = date;
|
||||
workUnitModalId = workUnitId;
|
||||
}
|
||||
|
||||
private void OpenCalendarEvent(DateOnly date, string eventId)
|
||||
private void OpenCalendarEvent(DateOnly date, string? eventId)
|
||||
{
|
||||
Navigation.NavigateTo($"/calendar-event/{date:yyyy-MM-dd}/{eventId}");
|
||||
activeDate = null;
|
||||
calendarEventModalDate = date;
|
||||
calendarEventModalId = eventId;
|
||||
}
|
||||
|
||||
private async Task HandleEditorSavedAsync()
|
||||
{
|
||||
CloseWorkUnitModal();
|
||||
CloseCalendarEventModal();
|
||||
statusMessage = null;
|
||||
await LoadMonth();
|
||||
}
|
||||
|
||||
private void CloseWorkUnitModal()
|
||||
{
|
||||
workUnitModalDate = null;
|
||||
workUnitModalId = null;
|
||||
}
|
||||
|
||||
private void CloseCalendarEventModal()
|
||||
{
|
||||
calendarEventModalDate = null;
|
||||
calendarEventModalId = null;
|
||||
}
|
||||
|
||||
private async Task GeneratePreviewWorkUnitsAsync()
|
||||
|
|
@ -465,10 +492,44 @@ else
|
|||
return DurationFormatter.FormatHours(value);
|
||||
}
|
||||
|
||||
private static string FormatDisplayDate(DateOnly date)
|
||||
{
|
||||
return date.ToString("dddd dd/MM/yyyy");
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<DateOnly> GetFestivities(int year)
|
||||
{
|
||||
if (!festivitiesByYear.TryGetValue(year, out var festivities))
|
||||
{
|
||||
festivities = FestivitySource.GetFestivities(year);
|
||||
festivitiesByYear[year] = festivities;
|
||||
}
|
||||
|
||||
return festivities;
|
||||
}
|
||||
|
||||
private static DateOnly GetGridStart(DateOnly firstDayOfMonth)
|
||||
{
|
||||
var offset = ((int)firstDayOfMonth.DayOfWeek + 6) % 7;
|
||||
return firstDayOfMonth.AddDays(-offset);
|
||||
}
|
||||
|
||||
private static DateOnly GetGridEnd(DateOnly lastDayOfMonth)
|
||||
{
|
||||
var offset = (7 - (((int)lastDayOfMonth.DayOfWeek + 6) % 7) - 1 + 7) % 7;
|
||||
return lastDayOfMonth.AddDays(offset);
|
||||
}
|
||||
|
||||
private static DateOnly GetCalendarEventOwnerDate(CalendarEventDocument calendarEvent, DateOnly fallbackDate)
|
||||
{
|
||||
return calendarEvent.StartDate == default ? fallbackDate : calendarEvent.StartDate;
|
||||
}
|
||||
|
||||
private sealed class CalendarCell
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int ColumnIndex { get; set; }
|
||||
public bool IsCurrentMonth { get; set; }
|
||||
public bool IsWeekend { get; set; }
|
||||
public bool IsFestivity { get; set; }
|
||||
public WorkDayDocument? Entry { get; set; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue