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
264
Components/Shared/CalendarEventEditorModal.razor
Normal file
264
Components/Shared/CalendarEventEditorModal.razor
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
@inject IWorkDayService WorkDayService
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="calendar-modal-backdrop">
|
||||
<div class="calendar-modal-shell calendar-modal-shell-compact">
|
||||
<div class="calendar-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="calendar-event-modal-title" @onclick:stopPropagation="true">
|
||||
<div class="calendar-modal-header">
|
||||
<div>
|
||||
<h2 id="calendar-event-modal-title" class="h5 mb-1">@(isExistingEvent ? "Edit Calendar Event" : "New Calendar Event")</h2>
|
||||
<div class="small text-muted">@FormatDisplayDate(selectedDate)</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseAsync"></button>
|
||||
</div>
|
||||
|
||||
<div class="calendar-modal-body">
|
||||
@if (!loaded)
|
||||
{
|
||||
<p class="mb-0"><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Start Date</label>
|
||||
<LocalizedDateInput InputId="calendar-event-modal-start-date" TestId="calendar-event-start-date" Value="@selectedDate" ValueChanged="OnDateChangedAsync" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">End Date</label>
|
||||
<LocalizedDateInput InputId="calendar-event-modal-end-date" TestId="calendar-event-end-date" Value="@endDate" ValueChanged="OnEndDateChangedAsync" AllowEmpty="true" />
|
||||
<div class="form-text">Optional. Leave empty for a single-day event.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Entry Type</label>
|
||||
<select class="form-select" @bind="eventType">
|
||||
@foreach (var item in Enum.GetValues<CalendarEventType>())
|
||||
{
|
||||
<option value="@item">@CalendarEventFormatter.GetEventTypeName(item)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Title</label>
|
||||
<input class="form-control" @bind="description" maxlength="120" placeholder="Optional" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Start Time</label>
|
||||
<input type="time" lang="it-IT" class="form-control" value="@startTimeStr" @onchange="OnStartTimeChanged" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">End Time</label>
|
||||
<input type="time" lang="it-IT" class="form-control" value="@endTimeStr" @onchange="OnEndTimeChanged" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label text-muted">Duration</label>
|
||||
<div class="form-control-plaintext fw-bold">@FormatDuration()</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(statusMessage))
|
||||
{
|
||||
<div class="alert alert-danger py-2 mt-3 mb-0">@statusMessage</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="calendar-modal-actions">
|
||||
<button type="button" class="btn btn-primary" @onclick="SaveAsync" disabled="@(!loaded)">Save</button>
|
||||
@if (isExistingEvent)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-danger" @onclick="DeleteAsync">Delete</button>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-secondary ms-auto" @onclick="CloseAsync">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public DateOnly Date { get; set; }
|
||||
[Parameter] public string? EventId { get; set; }
|
||||
[Parameter] public EventCallback OnSaved { get; set; }
|
||||
[Parameter] public EventCallback OnClosed { get; set; }
|
||||
|
||||
private bool loaded;
|
||||
private bool isExistingEvent;
|
||||
private DateOnly selectedDate;
|
||||
private string eventId = string.Empty;
|
||||
private CalendarEventType eventType = CalendarEventType.Generic;
|
||||
private string description = string.Empty;
|
||||
private string? startTimeStr;
|
||||
private string? endTimeStr;
|
||||
private DateOnly? endDate;
|
||||
private string? statusMessage;
|
||||
private string? loadKey;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
var nextKey = $"{Date:yyyy-MM-dd}|{EventId}";
|
||||
if (nextKey == loadKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loadKey = nextKey;
|
||||
loaded = false;
|
||||
selectedDate = Date;
|
||||
statusMessage = null;
|
||||
await LoadEventAsync();
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
private async Task LoadEventAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EventId))
|
||||
{
|
||||
SetDefaults();
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = await WorkDayService.GetCalendarEventAsync(selectedDate, EventId);
|
||||
if (existing is not null)
|
||||
{
|
||||
eventId = existing.Id;
|
||||
selectedDate = existing.StartDate == default ? selectedDate : existing.StartDate;
|
||||
eventType = existing.EventType;
|
||||
description = existing.Description;
|
||||
startTimeStr = existing.StartTime?.ToString("HH:mm");
|
||||
endTimeStr = existing.EndTime?.ToString("HH:mm");
|
||||
endDate = existing.EndDate;
|
||||
isExistingEvent = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetDefaults();
|
||||
statusMessage = "The selected calendar event was not found. A new event will be created.";
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDefaults()
|
||||
{
|
||||
eventId = string.Empty;
|
||||
eventType = CalendarEventType.Generic;
|
||||
description = string.Empty;
|
||||
startTimeStr = null;
|
||||
endTimeStr = null;
|
||||
endDate = null;
|
||||
isExistingEvent = false;
|
||||
}
|
||||
|
||||
private Task OnDateChangedAsync(DateOnly? value)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
selectedDate = value.Value;
|
||||
statusMessage = null;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnEndDateChangedAsync(DateOnly? value)
|
||||
{
|
||||
endDate = value;
|
||||
statusMessage = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnStartTimeChanged(ChangeEventArgs e)
|
||||
{
|
||||
startTimeStr = e.Value?.ToString();
|
||||
statusMessage = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnEndTimeChanged(ChangeEventArgs e)
|
||||
{
|
||||
endTimeStr = e.Value?.ToString();
|
||||
statusMessage = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
var calendarEvent = new CalendarEventDocument
|
||||
{
|
||||
Id = eventId,
|
||||
StartDate = selectedDate,
|
||||
EndDate = endDate,
|
||||
EventType = eventType,
|
||||
Description = description,
|
||||
StartTime = ParseTime(startTimeStr),
|
||||
EndTime = ParseTime(endTimeStr)
|
||||
};
|
||||
|
||||
await WorkDayService.SaveCalendarEventAsync(Date, calendarEvent);
|
||||
await OnSaved.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
if (!isExistingEvent || string.IsNullOrWhiteSpace(eventId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventName = CalendarEventFormatter.GetDisplayDescription(eventType, description);
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Delete calendar event '{eventName}' starting on {FormatDisplayDate(selectedDate)}?\nThis cannot be undone.");
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deleted = await WorkDayService.DeleteCalendarEventAsync(Date, eventId);
|
||||
if (deleted)
|
||||
{
|
||||
await OnSaved.InvokeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
statusMessage = "Unable to delete the calendar event.";
|
||||
}
|
||||
|
||||
private Task CloseAsync() => OnClosed.InvokeAsync();
|
||||
|
||||
private decimal? GetDuration()
|
||||
{
|
||||
var start = ParseTime(startTimeStr);
|
||||
var end = ParseTime(endTimeStr);
|
||||
if (!start.HasValue || !end.HasValue || end <= start)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.Round((decimal)(end.Value - start.Value).TotalHours, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private string FormatDuration() => GetDuration() is { } duration ? FormatDurationHours(duration) : "—";
|
||||
|
||||
private static string FormatDurationHours(decimal value)
|
||||
{
|
||||
var totalMinutes = (int)Math.Round(value * 60m, MidpointRounding.AwayFromZero);
|
||||
var hours = totalMinutes / 60;
|
||||
var minutes = totalMinutes % 60;
|
||||
return $"{hours:00}:{minutes:00}";
|
||||
}
|
||||
|
||||
private static TimeOnly? ParseTime(string? value)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(value) && TimeOnly.TryParse(value, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string FormatDisplayDate(DateOnly date)
|
||||
{
|
||||
return date.ToString("dddd dd/MM/yyyy");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue