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
246
Components/Shared/LocalizedDateInput.razor
Normal file
246
Components/Shared/LocalizedDateInput.razor
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
@using System.Globalization
|
||||
|
||||
<div class="localized-date-input" @onkeydown="HandleKeyDown">
|
||||
<div class="input-group">
|
||||
<input id="@InputId"
|
||||
data-testid="@GetInputTestId()"
|
||||
type="text"
|
||||
class="form-control"
|
||||
value="@displayValue"
|
||||
@onchange="OnTextChangedAsync"
|
||||
@onfocus="OpenPopup"
|
||||
placeholder="dd/MM/yyyy"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
disabled="@Disabled" />
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary localized-date-input-toggle"
|
||||
aria-label="Open calendar"
|
||||
@onclick="TogglePopup"
|
||||
disabled="@Disabled">
|
||||
Cal
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (isOpen && !Disabled)
|
||||
{
|
||||
<div class="localized-date-input-popover" data-testid="@GetPopoverTestId()" @onclick:stopPropagation="true">
|
||||
<div class="localized-date-input-header">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="ShowPreviousMonth" aria-label="Previous month">‹</button>
|
||||
<div class="localized-date-input-month">@visibleMonth.ToDateTime(TimeOnly.MinValue).ToString("MMMM yyyy", ItalianCulture)</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="ShowNextMonth" aria-label="Next month">›</button>
|
||||
</div>
|
||||
|
||||
<div class="localized-date-input-weekdays">
|
||||
@foreach (var weekday in mondayFirstWeekdays)
|
||||
{
|
||||
<div class="localized-date-input-weekday" data-testid="date-picker-weekday">@weekday</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="localized-date-input-grid">
|
||||
@foreach (var day in calendarDays)
|
||||
{
|
||||
<button type="button"
|
||||
class="localized-date-input-day @(day.IsCurrentMonth ? string.Empty : "localized-date-input-day-outside") @(day.Date == Value ? "localized-date-input-day-selected" : string.Empty)"
|
||||
data-testid="@GetDayTestId(day.Date)"
|
||||
@onclick="() => SelectDateAsync(day.Date)">
|
||||
@day.Date.Day
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (AllowEmpty)
|
||||
{
|
||||
<div class="localized-date-input-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ms-auto" @onclick="ClearAsync">Clear</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private static readonly CultureInfo ItalianCulture = CultureInfo.GetCultureInfo("it-IT");
|
||||
private static readonly string[] SupportedFormats = ["dd/MM/yyyy", "d/M/yyyy", "dd/M/yyyy", "d/MM/yyyy"];
|
||||
private static readonly string[] mondayFirstWeekdays = BuildMondayFirstWeekdays();
|
||||
|
||||
[Parameter] public DateOnly? Value { get; set; }
|
||||
[Parameter] public EventCallback<DateOnly?> ValueChanged { get; set; }
|
||||
[Parameter] public bool Disabled { get; set; }
|
||||
[Parameter] public bool AllowEmpty { get; set; }
|
||||
[Parameter] public string? InputId { get; set; }
|
||||
[Parameter] public string? TestId { get; set; }
|
||||
|
||||
private DateOnly? lastValue;
|
||||
private string displayValue = string.Empty;
|
||||
private bool isOpen;
|
||||
private DateOnly visibleMonth;
|
||||
private IReadOnlyList<CalendarDayCell> calendarDays = [];
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (visibleMonth == default)
|
||||
{
|
||||
visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
|
||||
if (Value != lastValue)
|
||||
{
|
||||
lastValue = Value;
|
||||
displayValue = FormatValue(Value);
|
||||
visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenPopup(FocusEventArgs _)
|
||||
{
|
||||
if (Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen = true;
|
||||
visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
|
||||
private void TogglePopup()
|
||||
{
|
||||
if (Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen = !isOpen;
|
||||
if (isOpen)
|
||||
{
|
||||
visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnTextChangedAsync(ChangeEventArgs e)
|
||||
{
|
||||
displayValue = e.Value?.ToString() ?? string.Empty;
|
||||
|
||||
if (TryParseInput(displayValue, out var parsedDate))
|
||||
{
|
||||
await SetValueAsync(parsedDate, closePopup: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (AllowEmpty && string.IsNullOrWhiteSpace(displayValue))
|
||||
{
|
||||
await SetValueAsync(null, closePopup: false);
|
||||
return;
|
||||
}
|
||||
|
||||
displayValue = FormatValue(Value);
|
||||
}
|
||||
|
||||
private async Task SelectDateAsync(DateOnly date)
|
||||
{
|
||||
await SetValueAsync(date, closePopup: true);
|
||||
}
|
||||
|
||||
private async Task ClearAsync()
|
||||
{
|
||||
await SetValueAsync(null, closePopup: true);
|
||||
}
|
||||
|
||||
private async Task SetValueAsync(DateOnly? date, bool closePopup)
|
||||
{
|
||||
lastValue = date;
|
||||
Value = date;
|
||||
displayValue = FormatValue(date);
|
||||
if (date.HasValue)
|
||||
{
|
||||
visibleMonth = ToFirstOfMonth(date.Value);
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
|
||||
if (closePopup)
|
||||
{
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
await ValueChanged.InvokeAsync(date);
|
||||
}
|
||||
|
||||
private void ShowPreviousMonth()
|
||||
{
|
||||
visibleMonth = visibleMonth.AddMonths(-1);
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
|
||||
private void ShowNextMonth()
|
||||
{
|
||||
visibleMonth = visibleMonth.AddMonths(1);
|
||||
calendarDays = BuildCalendarDays(visibleMonth);
|
||||
}
|
||||
|
||||
private void HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Escape")
|
||||
{
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetInputTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-input" : $"{TestId}-input";
|
||||
|
||||
private string GetPopoverTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-popover" : $"{TestId}-popover";
|
||||
|
||||
private string GetDayTestId(DateOnly date) => string.IsNullOrWhiteSpace(TestId)
|
||||
? $"localized-date-day-{date:yyyy-MM-dd}"
|
||||
: $"{TestId}-day-{date:yyyy-MM-dd}";
|
||||
|
||||
private static string FormatValue(DateOnly? date)
|
||||
{
|
||||
return date?.ToString("dd/MM/yyyy", ItalianCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool TryParseInput(string? value, out DateOnly date)
|
||||
{
|
||||
foreach (var format in SupportedFormats)
|
||||
{
|
||||
if (DateOnly.TryParseExact(value, format, ItalianCulture, DateTimeStyles.None, out date))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return DateOnly.TryParse(value, ItalianCulture, DateTimeStyles.None, out date);
|
||||
}
|
||||
|
||||
private static DateOnly ToFirstOfMonth(DateOnly date) => new(date.Year, date.Month, 1);
|
||||
|
||||
private static IReadOnlyList<CalendarDayCell> BuildCalendarDays(DateOnly month)
|
||||
{
|
||||
var firstDayOfMonth = ToFirstOfMonth(month);
|
||||
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
|
||||
var gridStart = firstDayOfMonth.AddDays(-(((int)firstDayOfMonth.DayOfWeek + 6) % 7));
|
||||
var gridEnd = lastDayOfMonth.AddDays((7 - (((int)lastDayOfMonth.DayOfWeek + 6) % 7) - 1 + 7) % 7);
|
||||
var days = new List<CalendarDayCell>();
|
||||
|
||||
for (var date = gridStart; date <= gridEnd; date = date.AddDays(1))
|
||||
{
|
||||
days.Add(new CalendarDayCell(date, date.Month == month.Month && date.Year == month.Year));
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
private static string[] BuildMondayFirstWeekdays()
|
||||
{
|
||||
return Enumerable.Range(0, 7)
|
||||
.Select(index => ItalianCulture.DateTimeFormat.AbbreviatedDayNames[(index + 1) % 7])
|
||||
.Select(dayName => ItalianCulture.TextInfo.ToTitleCase(dayName))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private sealed record CalendarDayCell(DateOnly Date, bool IsCurrentMonth);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue