feat: Add Grid View and Monthly Summary pages with workday management
Some checks failed
Publish Container / publish (push) Failing after 1m2s
Some checks failed
Publish Container / publish (push) Failing after 1m2s
- Implement GridView.razor for displaying a tabular view of workdays in the current month. - Create MonthlySummary.razor to show a summary of worked hours, income, and day types for the selected month. - Introduce WorkDayEditor.razor for adding and editing workday entries with detailed calculations. - Update Home.razor to include links to the new Grid View and Monthly Summary pages. - Add IWorkDayService interface and CouchbaseLiteWorkDayService implementation for managing workday data. - Define domain models: WorkDayDocument, MonthlySummaryModel, and CoeffSnapshotDocument for data structure. - Enhance CouchbaseLiteDatabaseProvider to include a collection for workdays. - Update app settings and services to support new features. - Add CSS styles for calendar view and table formatting.
This commit is contained in:
parent
6e3371514e
commit
3ccce7e8a6
17 changed files with 1257 additions and 18 deletions
190
Components/Pages/CalendarView.razor
Normal file
190
Components/Pages/CalendarView.razor
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
@page "/calendar"
|
||||
@page "/calendar/{YearMonth}"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@inject IWorkDayService WorkDayService
|
||||
@inject IItalianFestivitySource FestivitySource
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>Calendar</PageTitle>
|
||||
|
||||
<h1>Calendar</h1>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="PreviousMonth">« Prev</button>
|
||||
<h2 class="h5 mb-0">@firstOfMonth.ToString("MMMM yyyy")</h2>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="NextMonth">Next »</button>
|
||||
</div>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered calendar-table">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Mon</th>
|
||||
<th>Tue</th>
|
||||
<th>Wed</th>
|
||||
<th>Thu</th>
|
||||
<th>Fri</th>
|
||||
<th>Sat</th>
|
||||
<th>Sun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var week in weeks)
|
||||
{
|
||||
<tr>
|
||||
@foreach (var cell in week)
|
||||
{
|
||||
@if (cell is null)
|
||||
{
|
||||
<td class="calendar-cell bg-light"></td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="calendar-cell @GetCellClass(cell)" @onclick="() => NavigateToDay(cell.Date)" role="button">
|
||||
<div class="calendar-day-number">@cell.Date.Day</div>
|
||||
@if (cell.Entry is not null)
|
||||
{
|
||||
<span class="badge @GetBadgeClass(cell.Entry.DayType)">@cell.Entry.DayType</span>
|
||||
<div class="calendar-hours">@cell.Entry.WorkedHoursFinal.ToString("N1")h</div>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3 class="h6">Legend</h3>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge bg-primary">Work</span>
|
||||
<span class="badge bg-success">Home</span>
|
||||
<span class="badge bg-warning text-dark">Closure</span>
|
||||
<span class="badge bg-info text-dark">Illness</span>
|
||||
<span class="badge bg-secondary">DayOff</span>
|
||||
<span class="badge bg-danger">Holiday</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string? YearMonth { get; set; }
|
||||
|
||||
private DateOnly firstOfMonth;
|
||||
private bool loading = true;
|
||||
private List<CalendarCell?[]> weeks = [];
|
||||
private IReadOnlyCollection<DateOnly> festivities = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(YearMonth) && DateTime.TryParseExact(YearMonth, "yyyy-MM", null, System.Globalization.DateTimeStyles.None, out var parsed))
|
||||
{
|
||||
firstOfMonth = new DateOnly(parsed.Year, parsed.Month, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
firstOfMonth = new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1);
|
||||
}
|
||||
|
||||
await LoadMonth();
|
||||
}
|
||||
|
||||
private async Task LoadMonth()
|
||||
{
|
||||
loading = true;
|
||||
festivities = FestivitySource.GetFestivities(firstOfMonth.Year);
|
||||
|
||||
var lastDay = firstOfMonth.AddMonths(1).AddDays(-1);
|
||||
var entries = await WorkDayService.GetRangeAsync(firstOfMonth, lastDay);
|
||||
var lookup = entries.ToDictionary(e => e.Date);
|
||||
|
||||
// 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))
|
||||
{
|
||||
currentWeek[dayOfWeek] = new CalendarCell
|
||||
{
|
||||
Date = d,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (dayOfWeek > 0)
|
||||
{
|
||||
weeks.Add(currentWeek);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private async Task PreviousMonth()
|
||||
{
|
||||
firstOfMonth = firstOfMonth.AddMonths(-1);
|
||||
await LoadMonth();
|
||||
}
|
||||
|
||||
private async Task NextMonth()
|
||||
{
|
||||
firstOfMonth = firstOfMonth.AddMonths(1);
|
||||
await LoadMonth();
|
||||
}
|
||||
|
||||
private void NavigateToDay(DateOnly date) =>
|
||||
Navigation.NavigateTo($"/workday/{date:yyyy-MM-dd}");
|
||||
|
||||
private string GetCellClass(CalendarCell cell)
|
||||
{
|
||||
if (cell.IsWeekend || cell.IsFestivity) return "calendar-weekend";
|
||||
if (cell.Entry is null) return "";
|
||||
return cell.Entry.DayType switch
|
||||
{
|
||||
DayType.Closure => "calendar-closure",
|
||||
DayType.Illness => "calendar-illness",
|
||||
DayType.DayOff => "calendar-dayoff",
|
||||
DayType.Holiday => "calendar-holiday",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetBadgeClass(DayType type) => type switch
|
||||
{
|
||||
DayType.Work => "bg-primary",
|
||||
DayType.Home => "bg-success",
|
||||
DayType.Closure => "bg-warning text-dark",
|
||||
DayType.Illness => "bg-info text-dark",
|
||||
DayType.DayOff => "bg-secondary",
|
||||
DayType.Holiday => "bg-danger",
|
||||
_ => "bg-light text-dark"
|
||||
};
|
||||
|
||||
private sealed class CalendarCell
|
||||
{
|
||||
public DateOnly Date { 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