feat: Implement sidebar toggle functionality and enhance Monthly Timesheet summary view
This commit is contained in:
parent
cab549ab3a
commit
a7f8dfba01
13 changed files with 686 additions and 58 deletions
|
|
@ -212,6 +212,55 @@ public sealed class CouchbaseLiteWorkDayService : IWorkDayService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<MonthlyTimesheetModel> GetMonthlyTimesheetAsync(int year, int month, bool includePreview, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var from = new DateOnly(year, month, 1);
|
||||
var to = from.AddMonths(1).AddDays(-1);
|
||||
var days = await GetRangeAsync(from, to, cancellationToken);
|
||||
var dayLookup = days.ToDictionary(day => day.Date);
|
||||
var settings = await appSettingsService.GetAsync(cancellationToken);
|
||||
|
||||
var daySummaries = new List<MonthlyTimesheetDaySummary>();
|
||||
for (var date = from; date <= to; date = date.AddDays(1))
|
||||
{
|
||||
dayLookup.TryGetValue(date, out var day);
|
||||
daySummaries.Add(CreateTimesheetDaySummary(day, date, includePreview, settings.StandardWorkHoursPerDay));
|
||||
}
|
||||
|
||||
return new MonthlyTimesheetModel
|
||||
{
|
||||
Year = year,
|
||||
Month = month,
|
||||
Days = daySummaries.Select(summary => new MonthlyTimesheetDayModel
|
||||
{
|
||||
Date = summary.Date,
|
||||
IsWeekend = summary.Date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
|
||||
IsHoliday = summary.HolidayDays > 0m || dayLookup.GetValueOrDefault(summary.Date)?.IsItalianFestivity == true,
|
||||
IsClosure = summary.VacationDays > 0m && HasEventType(dayLookup.GetValueOrDefault(summary.Date), CalendarEventType.Closure),
|
||||
WorkUnitSummaries = dayLookup.GetValueOrDefault(summary.Date)?.WorkUnits
|
||||
.Where(unit => includePreview || !unit.IsPreview)
|
||||
.Select(FormatTimesheetWorkUnitSummary)
|
||||
.ToList() ?? [],
|
||||
EventSummaries = dayLookup.GetValueOrDefault(summary.Date)?.CalendarEvents
|
||||
.Select(FormatTimesheetEventSummary)
|
||||
.ToList() ?? []
|
||||
}).ToList(),
|
||||
Rows =
|
||||
[
|
||||
CreateTimesheetRow("office", "Ore lavorative in presenza", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.OfficeHours)),
|
||||
CreateTimesheetRow("home", "Ore lavorative in smart working", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.HomeHours)),
|
||||
CreateTimesheetRow("overtime", "Straordinari", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.OvertimeHours)),
|
||||
CreateTimesheetRow("weekend", "Weekend", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.WeekendHours)),
|
||||
CreateTimesheetRow("night", "Notturni (22-06)", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.NightHours)),
|
||||
CreateTimesheetRow("vacation", "Giorni di ferie", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.VacationDays)),
|
||||
CreateTimesheetRow("permit", "Ore di permesso", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.PermitHours)),
|
||||
CreateTimesheetRow("compensatory-rest", "Riposo compensativo", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.CompensatoryRestDays), includeZeroTotal: false),
|
||||
CreateTimesheetRow("sick", "Giorni di malattia", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.SickDays)),
|
||||
CreateTimesheetRow("holiday", "Festività", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.HolidayDays))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> GenerateMonthlyPreviewWorkUnitsAsync(int year, int month, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
|
@ -431,6 +480,64 @@ public sealed class CouchbaseLiteWorkDayService : IWorkDayService
|
|||
return days.Count(day => day.CalendarEvents.Any(calendarEvent => calendarEvent.EventType == eventType));
|
||||
}
|
||||
|
||||
private static MonthlyTimesheetDaySummary CreateTimesheetDaySummary(WorkDayDocument? day, DateOnly date, bool includePreview, decimal defaultStandardHours)
|
||||
{
|
||||
var includedUnits = day?.WorkUnits.Where(unit => includePreview || !unit.IsPreview).ToList() ?? [];
|
||||
var totalHours = includedUnits.Sum(unit => unit.ManualWorkedHours);
|
||||
var explicitHoliday = HasEventType(day, CalendarEventType.Holiday);
|
||||
var illness = HasEventType(day, CalendarEventType.Illness);
|
||||
var dayOff = HasEventType(day, CalendarEventType.DayOff);
|
||||
var closure = HasEventType(day, CalendarEventType.Closure);
|
||||
var isWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
|
||||
var isAutomaticHoliday = day?.IsItalianFestivity ?? false;
|
||||
var standardHours = includedUnits.FirstOrDefault()?.CoeffSnapshot.StandardWorkHoursPerDay ?? defaultStandardHours;
|
||||
var nightHours = includedUnits.Sum(GetNightHours);
|
||||
var weekdayDaytimeHours = isWeekend ? 0m : Math.Max(0m, totalHours - nightHours);
|
||||
var suppressVacation = isWeekend || explicitHoliday || isAutomaticHoliday || illness;
|
||||
var hasNonWorkingEvent = explicitHoliday || illness || dayOff || closure;
|
||||
var permitHours = !isWeekend && !isAutomaticHoliday && !hasNonWorkingEvent && totalHours < standardHours
|
||||
? standardHours - totalHours
|
||||
: 0m;
|
||||
|
||||
return new MonthlyTimesheetDaySummary
|
||||
{
|
||||
Date = date,
|
||||
OfficeHours = includedUnits.Where(unit => unit.Location == WorkUnitLocation.Office).Sum(unit => unit.ManualWorkedHours),
|
||||
HomeHours = includedUnits.Where(unit => unit.Location == WorkUnitLocation.Home).Sum(unit => unit.ManualWorkedHours),
|
||||
OvertimeHours = Math.Max(0m, weekdayDaytimeHours - standardHours),
|
||||
WeekendHours = isWeekend ? totalHours : 0m,
|
||||
NightHours = nightHours,
|
||||
VacationDays = (dayOff || closure) && !suppressVacation ? 1m : 0m,
|
||||
PermitHours = Math.Max(0m, permitHours),
|
||||
CompensatoryRestDays = 0m,
|
||||
SickDays = illness ? 1m : 0m,
|
||||
HolidayDays = explicitHoliday && !isWeekend ? 1m : 0m
|
||||
};
|
||||
}
|
||||
|
||||
private static MonthlyTimesheetRowModel CreateTimesheetRow(
|
||||
string key,
|
||||
string label,
|
||||
MonthlyTimesheetValueFormat valueFormat,
|
||||
IEnumerable<decimal> values,
|
||||
bool includeZeroTotal = true)
|
||||
{
|
||||
var dailyValues = values
|
||||
.Select(value => value > 0m ? value : (decimal?)null)
|
||||
.ToList();
|
||||
|
||||
var total = dailyValues.Where(value => value.HasValue).Sum(value => value ?? 0m);
|
||||
|
||||
return new MonthlyTimesheetRowModel
|
||||
{
|
||||
Key = key,
|
||||
Label = label,
|
||||
ValueFormat = valueFormat,
|
||||
DailyValues = dailyValues,
|
||||
Total = includeZeroTotal || total > 0m ? total : null
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal GetHoursOff(WorkDayDocument day, bool includePreview)
|
||||
{
|
||||
var includedUnits = day.WorkUnits.Where(unit => includePreview || !unit.IsPreview).ToList();
|
||||
|
|
@ -449,6 +556,63 @@ public sealed class CouchbaseLiteWorkDayService : IWorkDayService
|
|||
return eventType is CalendarEventType.DayOff or CalendarEventType.Closure or CalendarEventType.Holiday or CalendarEventType.Illness;
|
||||
}
|
||||
|
||||
private static bool HasEventType(WorkDayDocument? day, CalendarEventType eventType)
|
||||
{
|
||||
return day?.CalendarEvents.Any(calendarEvent => calendarEvent.EventType == eventType) == true;
|
||||
}
|
||||
|
||||
private static string FormatTimesheetWorkUnitSummary(WorkUnitDocument unit)
|
||||
{
|
||||
var prefix = unit.Location == WorkUnitLocation.Home ? "SW" : "Pres";
|
||||
var hours = FormatCompactHours(unit.ManualWorkedHours);
|
||||
if (unit.StartTime.HasValue && unit.EndTime.HasValue)
|
||||
{
|
||||
return $"{prefix}: {unit.Label} ({unit.StartTime:HH:mm}-{unit.EndTime:HH:mm}, {hours}h{(unit.IsPreview ? ", preview" : string.Empty)})";
|
||||
}
|
||||
|
||||
return $"{prefix}: {unit.Label} ({hours}h{(unit.IsPreview ? ", preview" : string.Empty)})";
|
||||
}
|
||||
|
||||
private static string FormatTimesheetEventSummary(CalendarEventDocument calendarEvent)
|
||||
{
|
||||
if (calendarEvent.StartTime.HasValue)
|
||||
{
|
||||
return $"{calendarEvent.EventType}: {calendarEvent.Description} ({calendarEvent.StartTime:HH:mm})";
|
||||
}
|
||||
|
||||
return $"{calendarEvent.EventType}: {calendarEvent.Description}";
|
||||
}
|
||||
|
||||
private static string FormatCompactHours(decimal value)
|
||||
{
|
||||
return value == decimal.Truncate(value)
|
||||
? value.ToString("0")
|
||||
: value.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static decimal GetNightHours(WorkUnitDocument unit)
|
||||
{
|
||||
if (!unit.StartTime.HasValue || !unit.EndTime.HasValue || unit.EndTime <= unit.StartTime)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
|
||||
return GetOverlapHours(unit.StartTime.Value, unit.EndTime.Value, new TimeOnly(0, 0), new TimeOnly(6, 0))
|
||||
+ GetOverlapHours(unit.StartTime.Value, unit.EndTime.Value, new TimeOnly(22, 0), new TimeOnly(23, 59, 59));
|
||||
}
|
||||
|
||||
private static decimal GetOverlapHours(TimeOnly rangeStart, TimeOnly rangeEnd, TimeOnly windowStart, TimeOnly windowEnd)
|
||||
{
|
||||
var overlapStart = rangeStart > windowStart ? rangeStart : windowStart;
|
||||
var overlapEnd = rangeEnd < windowEnd ? rangeEnd : windowEnd;
|
||||
if (overlapEnd <= overlapStart)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
|
||||
return Math.Round((decimal)(overlapEnd - overlapStart).TotalHours, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static WorkUnitDocument CreatePreviewWorkUnit(string label, TimeOnly startTime, TimeOnly endTime, AppSettingsDocument settings)
|
||||
{
|
||||
var workUnit = new WorkUnitDocument
|
||||
|
|
|
|||
|
|
@ -22,5 +22,7 @@ public interface IWorkDayService
|
|||
|
||||
Task<MonthlySummaryModel> GetMonthlySummaryAsync(int year, int month, bool includePreview, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<MonthlyTimesheetModel> GetMonthlyTimesheetAsync(int year, int month, bool includePreview, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<int> GenerateMonthlyPreviewWorkUnitsAsync(int year, int month, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue