296 lines
9.7 KiB
Text
296 lines
9.7 KiB
Text
|
|
@page "/workday"
|
||
|
|
@page "/workday/{DateStr}"
|
||
|
|
@attribute [Authorize]
|
||
|
|
@rendermode InteractiveServer
|
||
|
|
|
||
|
|
@inject IWorkDayService WorkDayService
|
||
|
|
@inject IAppSettingsService AppSettingsService
|
||
|
|
@inject IItalianFestivitySource FestivitySource
|
||
|
|
@inject NavigationManager Navigation
|
||
|
|
|
||
|
|
<PageTitle>Work Day</PageTitle>
|
||
|
|
|
||
|
|
<h1>Work Day Entry</h1>
|
||
|
|
|
||
|
|
@if (!loaded)
|
||
|
|
{
|
||
|
|
<p><em>Loading...</em></p>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<div class="row g-3">
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Date</label>
|
||
|
|
<input type="date" class="form-control" value="@selectedDate.ToString("yyyy-MM-dd")" @onchange="OnDateChanged" />
|
||
|
|
@if (isWeekend || isFestivity)
|
||
|
|
{
|
||
|
|
<div class="mt-1">
|
||
|
|
@if (isWeekend) { <span class="badge bg-danger me-1">Weekend</span> }
|
||
|
|
@if (isFestivity) { <span class="badge bg-warning text-dark">Festivity</span> }
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Day Type</label>
|
||
|
|
<select class="form-select" value="@selectedDayType" @onchange="OnDayTypeChanged">
|
||
|
|
@foreach (var dt in Enum.GetValues<DayType>())
|
||
|
|
{
|
||
|
|
<option value="@dt">@dt</option>
|
||
|
|
}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Start Time</label>
|
||
|
|
<input type="time" class="form-control" value="@startTimeStr" @onchange="OnStartTimeChanged" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Actual Exit Time</label>
|
||
|
|
<input type="time" class="form-control" value="@actualExitTimeStr" @onchange="OnActualExitChanged" />
|
||
|
|
<div class="form-text">Informational only, not used in calculations.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Extra hours delta</label>
|
||
|
|
<input type="number" class="form-control" step="0.25" value="@extraHoursDelta" @onchange="OnExtraDeltaChanged" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12">
|
||
|
|
<label class="form-label">Notes</label>
|
||
|
|
<textarea class="form-control" rows="2" @bind="notes"></textarea>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<hr />
|
||
|
|
|
||
|
|
<h2 class="h5">Computed values</h2>
|
||
|
|
<div class="row g-3">
|
||
|
|
<div class="col-6 col-md-4 col-lg-3">
|
||
|
|
<label class="form-label text-muted">Projected Exit</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">@(projectedExitTime?.ToString("HH:mm") ?? "—")</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-6 col-md-4 col-lg-3">
|
||
|
|
<label class="form-label text-muted">Worked (base)</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">@workedHoursBase.ToString("N2")h</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-6 col-md-4 col-lg-3">
|
||
|
|
<label class="form-label text-muted">Worked (final)</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">@workedHoursFinal.ToString("N2")h</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-6 col-md-4 col-lg-3">
|
||
|
|
<label class="form-label text-muted">Hours Off</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">@hoursOff.ToString("N2")h</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-6 col-md-4 col-lg-3">
|
||
|
|
<label class="form-label text-muted">Gross Income</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">€@grossIncome.ToString("N2")</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-6 col-md-4 col-lg-3">
|
||
|
|
<label class="form-label text-muted">Net Income</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">€@netIncome.ToString("N2")</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="d-flex align-items-center gap-2 mt-4">
|
||
|
|
<button class="btn btn-primary" @onclick="SaveAsync">Save</button>
|
||
|
|
@if (!string.IsNullOrWhiteSpace(statusMessage))
|
||
|
|
{
|
||
|
|
<span class="text-success">@statusMessage</span>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
@code {
|
||
|
|
[Parameter] public string? DateStr { get; set; }
|
||
|
|
|
||
|
|
private bool loaded;
|
||
|
|
private DateOnly selectedDate = DateOnly.FromDateTime(DateTime.Today);
|
||
|
|
private DayType selectedDayType = DayType.None;
|
||
|
|
private string? startTimeStr;
|
||
|
|
private string? actualExitTimeStr;
|
||
|
|
private decimal extraHoursDelta;
|
||
|
|
private string? notes;
|
||
|
|
private string? statusMessage;
|
||
|
|
|
||
|
|
// Computed preview
|
||
|
|
private TimeOnly? projectedExitTime;
|
||
|
|
private decimal workedHoursBase;
|
||
|
|
private decimal workedHoursFinal;
|
||
|
|
private decimal hoursOff;
|
||
|
|
private decimal grossIncome;
|
||
|
|
private decimal netIncome;
|
||
|
|
private bool isWeekend;
|
||
|
|
private bool isFestivity;
|
||
|
|
|
||
|
|
// Loaded from settings
|
||
|
|
private AppSettingsDocument settings = new();
|
||
|
|
private IReadOnlyCollection<DateOnly> festivities = [];
|
||
|
|
|
||
|
|
protected override async Task OnInitializedAsync()
|
||
|
|
{
|
||
|
|
if (!string.IsNullOrEmpty(DateStr) && DateOnly.TryParseExact(DateStr, "yyyy-MM-dd", out var parsed))
|
||
|
|
{
|
||
|
|
selectedDate = parsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
settings = await AppSettingsService.GetAsync();
|
||
|
|
festivities = FestivitySource.GetFestivities(selectedDate.Year);
|
||
|
|
|
||
|
|
await LoadExistingEntry();
|
||
|
|
RecomputeFlags();
|
||
|
|
RecomputePreview();
|
||
|
|
loaded = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task LoadExistingEntry()
|
||
|
|
{
|
||
|
|
var existing = await WorkDayService.GetAsync(selectedDate);
|
||
|
|
if (existing is not null)
|
||
|
|
{
|
||
|
|
selectedDayType = existing.DayType;
|
||
|
|
startTimeStr = existing.StartTime?.ToString("HH:mm");
|
||
|
|
actualExitTimeStr = existing.ActualExitTime?.ToString("HH:mm");
|
||
|
|
extraHoursDelta = existing.ExtraHoursDelta;
|
||
|
|
notes = existing.Notes;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
selectedDayType = DayType.None;
|
||
|
|
startTimeStr = null;
|
||
|
|
actualExitTimeStr = null;
|
||
|
|
extraHoursDelta = 0;
|
||
|
|
notes = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task OnDateChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
if (DateOnly.TryParse(e.Value?.ToString(), out var d))
|
||
|
|
{
|
||
|
|
selectedDate = d;
|
||
|
|
festivities = FestivitySource.GetFestivities(selectedDate.Year);
|
||
|
|
await LoadExistingEntry();
|
||
|
|
RecomputeFlags();
|
||
|
|
RecomputePreview();
|
||
|
|
statusMessage = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnDayTypeChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
if (Enum.TryParse<DayType>(e.Value?.ToString(), out var dt))
|
||
|
|
{
|
||
|
|
selectedDayType = dt;
|
||
|
|
RecomputePreview();
|
||
|
|
statusMessage = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnStartTimeChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
startTimeStr = e.Value?.ToString();
|
||
|
|
RecomputePreview();
|
||
|
|
statusMessage = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnActualExitChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
actualExitTimeStr = e.Value?.ToString();
|
||
|
|
statusMessage = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnExtraDeltaChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
if (decimal.TryParse(e.Value?.ToString(), out var val))
|
||
|
|
{
|
||
|
|
extraHoursDelta = val;
|
||
|
|
}
|
||
|
|
RecomputePreview();
|
||
|
|
statusMessage = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RecomputeFlags()
|
||
|
|
{
|
||
|
|
isWeekend = selectedDate.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
|
||
|
|
isFestivity = festivities.Contains(selectedDate);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RecomputePreview()
|
||
|
|
{
|
||
|
|
TimeOnly? start = null;
|
||
|
|
if (!string.IsNullOrEmpty(startTimeStr) && TimeOnly.TryParse(startTimeStr, out var s))
|
||
|
|
{
|
||
|
|
start = s;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (selectedDayType is DayType.Work or DayType.Home)
|
||
|
|
{
|
||
|
|
workedHoursBase = settings.StandardWorkHoursPerDay;
|
||
|
|
if (start.HasValue)
|
||
|
|
{
|
||
|
|
var totalHours = settings.StandardWorkHoursPerDay + settings.LunchBreakHours;
|
||
|
|
projectedExitTime = start.Value.Add(TimeSpan.FromHours((double)totalHours));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
projectedExitTime = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
workedHoursBase = 0;
|
||
|
|
projectedExitTime = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
workedHoursFinal = workedHoursBase + extraHoursDelta;
|
||
|
|
|
||
|
|
hoursOff = selectedDayType is DayType.Work or DayType.Home
|
||
|
|
? Math.Max(0, settings.StandardWorkHoursPerDay - workedHoursFinal)
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
grossIncome = workedHoursFinal * settings.HourlyGrossRate;
|
||
|
|
var taxableBase = grossIncome * settings.ProfitabilityCoefficient;
|
||
|
|
netIncome = grossIncome - (taxableBase * settings.InpsRate) - (taxableBase * settings.SubstituteTaxRate);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task SaveAsync()
|
||
|
|
{
|
||
|
|
TimeOnly? start = null;
|
||
|
|
TimeOnly? exit = null;
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(startTimeStr) && TimeOnly.TryParse(startTimeStr, out var s))
|
||
|
|
{
|
||
|
|
start = s;
|
||
|
|
}
|
||
|
|
if (!string.IsNullOrEmpty(actualExitTimeStr) && TimeOnly.TryParse(actualExitTimeStr, out var e2))
|
||
|
|
{
|
||
|
|
exit = e2;
|
||
|
|
}
|
||
|
|
|
||
|
|
var workDay = new WorkDayDocument
|
||
|
|
{
|
||
|
|
Date = selectedDate,
|
||
|
|
DayType = selectedDayType,
|
||
|
|
StartTime = start,
|
||
|
|
ActualExitTime = exit,
|
||
|
|
ExtraHoursDelta = extraHoursDelta,
|
||
|
|
Notes = notes
|
||
|
|
};
|
||
|
|
|
||
|
|
var saved = await WorkDayService.SaveAsync(workDay);
|
||
|
|
|
||
|
|
// Update preview with saved computed values
|
||
|
|
projectedExitTime = saved.ProjectedExitTime;
|
||
|
|
workedHoursBase = saved.WorkedHoursBase;
|
||
|
|
workedHoursFinal = saved.WorkedHoursFinal;
|
||
|
|
hoursOff = saved.HoursOff;
|
||
|
|
grossIncome = saved.GrossIncome;
|
||
|
|
netIncome = saved.NetIncome;
|
||
|
|
isWeekend = saved.IsWeekend;
|
||
|
|
isFestivity = saved.IsItalianFestivity;
|
||
|
|
|
||
|
|
statusMessage = $"Saved at {DateTime.Now:t}";
|
||
|
|
}
|
||
|
|
}
|