first commit

This commit is contained in:
Wieland, Marc
2026-05-22 09:18:01 +02:00
commit 88ac175190
346 changed files with 69358 additions and 0 deletions
+489
View File
@@ -0,0 +1,489 @@
@page "/settings"
@rendermode InteractiveServer
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
@inject ISnackbar Snackbar
<PageTitle>Einstellungen Timetracker</PageTitle>
@if (_settings == null)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
</MudStack>
}
else
{
<MudStack Spacing="4">
@* ── Header ── *@
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color:white;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.Settings" Style="color:white; font-size:2rem" />
<MudStack Spacing="0">
<MudText Typo="Typo.h5" Style="color:white; font-weight:700">Einstellungen</MudText>
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,0.72)">
Arbeitszeit, Arbeitstage und Urlaub konfigurieren
</MudText>
</MudStack>
</MudStack>
</MudPaper>
<MudGrid Spacing="4">
@* ── Arbeitszeit ── *@
<MudItem xs="12" md="6">
<MudCard Elevation="3" Class="rounded-xl h-100">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Schedule" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Arbeitszeit</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="4">
<MudNumericField @bind-Value="_settings.DailyTargetHours"
Label="Sollstunden pro Tag (h)"
Variant="Variant.Outlined"
Min="0.5" Max="24.0" Step="0.25"
Format="0.##"
HelperText="Vertraglich vereinbarte Nettoarbeitszeit" />
<MudNumericField @bind-Value="_settings.MinimumBreakMinutes"
Label="Gesetzliche Mindestpause (min)"
Variant="Variant.Outlined"
Min="0" Max="120" Step="5"
HelperText="Pflichtpause laut Arbeitszeitgesetz" />
<MudPaper Elevation="0" Class="pa-3 rounded-lg"
Style="background: var(--mud-palette-background-grey);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary" Class="mb-2">
Tagesberechnung
</MudText>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Secondary">Netto (Soll)</MudText>
<MudText Typo="Typo.body2"><b>@FormatHours(_settings.DailyTargetHours)</b></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Secondary">+ Mindestpause</MudText>
<MudText Typo="Typo.body2"><b>@_settings.MinimumBreakMinutes min</b></MudText>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Secondary">= Brutto-Anwesenheit</MudText>
<MudText Typo="Typo.body2" Color="Color.Primary">
<b>@FormatHours(_settings.DailyTargetHours + _settings.MinimumBreakMinutes / 60.0)</b>
</MudText>
</MudStack>
</MudStack>
</MudPaper>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
@* ── Arbeitstage ── *@
<MudItem xs="12" md="6">
<MudCard Elevation="3" Class="rounded-xl h-100">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Arbeitstage</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-3">
Wähle die Wochentage, an denen du arbeitest.
</MudText>
<MudStack Spacing="2">
@foreach (var (label, getter, setter) in WorkDayToggles)
{
var isChecked = getter(_settings);
<MudPaper Elevation="0" Class="pa-2 rounded-lg"
Style="@($"background: {(isChecked ? "rgba(63,81,181,0.08)" : "var(--mud-palette-background-grey)")};")">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Circle"
Style="@($"font-size:10px; color:{(isChecked ? "#3F51B5" : "#CFD8DC")}")" />
<MudText Typo="Typo.body1" Style="@(isChecked ? "font-weight:600" : "")">
@label
</MudText>
</MudStack>
<MudSwitch Value="@isChecked"
ValueChanged="@((bool v) => setter(_settings, v))"
Color="Color.Primary" />
</MudStack>
</MudPaper>
}
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@* ── Speichern-Button ── *@
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="Save" StartIcon="@Icons.Material.Filled.Save"
Size="Size.Large" Style="max-width:300px">
Einstellungen speichern
</MudButton>
<MudDivider />
@* ── Urlaubsverwaltung ── *@
<MudCard Elevation="3" Class="rounded-xl">
<MudCardHeader Style="background: linear-gradient(90deg, rgba(0,150,136,0.1) 0%, transparent 100%);">
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.BeachAccess" Color="Color.Secondary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Urlaubsverwaltung</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
Size="Size.Small" OnClick="@(() => ChangeYear(-1))" />
<MudText Typo="Typo.h6" Style="font-weight:700; min-width:50px; text-align:center">
@_vacYear
</MudText>
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
Size="Size.Small" OnClick="@(() => ChangeYear(1))" />
</MudStack>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudGrid Spacing="4">
@* ── Urlaubskontingent ── *@
<MudItem xs="12" md="5">
<MudStack Spacing="3">
<MudNumericField @bind-Value="_settings.VacationDaysPerYear"
Label="Urlaubstage pro Jahr"
Variant="Variant.Outlined"
Min="1" Max="365" Step="1"
HelperText="Dein jährliches Urlaubskontingent" />
@* ── Statistik-Chips ── *@
<MudGrid Spacing="2">
<MudItem xs="4">
<MudPaper Elevation="0" Class="pa-3 rounded-lg text-center"
Style="background: rgba(63,81,181,0.08);">
<MudText Typo="Typo.h5" Color="Color.Primary" Style="font-weight:700">
@_settings.VacationDaysPerYear
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Gesamt</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Elevation="0" Class="pa-3 rounded-lg text-center"
Style="background: rgba(244,67,54,0.08);">
<MudText Typo="Typo.h5" Color="Color.Error" Style="font-weight:700">
@_vacationDays.Count
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Genommen</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Elevation="0" Class="pa-3 rounded-lg text-center"
Style="@($"background: rgba({(_vacRemaining >= 0 ? "76,175,80" : "255,152,0")},0.08);")">
<MudText Typo="Typo.h5"
Color="@(_vacRemaining >= 0 ? Color.Success : Color.Warning)"
Style="font-weight:700">
@_vacRemaining
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Verbleibend</MudText>
</MudPaper>
</MudItem>
</MudGrid>
@* ── Fortschrittsbalken ── *@
<MudTooltip Text="@($"{_vacationDays.Count} von {_settings.VacationDaysPerYear} Tagen genommen")">
<MudProgressLinear Value="@Math.Min(VacationUsedPercent, 100)"
Color="@(VacationUsedPercent > 100 ? Color.Error : VacationUsedPercent > 80 ? Color.Warning : Color.Success)"
Rounded="true" Style="height:10px" />
</MudTooltip>
<MudText Typo="Typo.caption" Color="Color.Secondary" Align="Align.Center">
@VacationUsedPercent % des Jahresurlaubs @_vacYear verbraucht
</MudText>
</MudStack>
</MudItem>
@* ── Urlaub hinzufügen ── *@
<MudItem xs="12" md="7">
<MudStack Spacing="3">
<MudText Typo="Typo.subtitle2" Style="font-weight:600">Urlaub eintragen</MudText>
<MudStack Row="true" AlignItems="AlignItems.End" Spacing="2" Wrap="Wrap.Wrap">
<MudDatePicker @bind-Date="_newVacDateFrom"
Label="Von"
Variant="Variant.Outlined"
DateFormat="dd.MM.yyyy"
Style="width:300px"
PickerVariant="PickerVariant.Inline" />
<MudDatePicker @bind-Date="_newVacDateTo"
Label="Bis"
Variant="Variant.Outlined"
DateFormat="dd.MM.yyyy"
Style="width:300px"
MinDate="@_newVacDateFrom"
PickerVariant="PickerVariant.Inline" />
<MudTextField @bind-Value="_newVacNote"
Label="Notiz (optional)"
Variant="Variant.Outlined"
Style="width:300px" />
<MudButton Variant="Variant.Filled" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddVacation" Disabled="@(_newVacDateFrom == null)">
Hinzufügen
</MudButton>
</MudStack>
@* ── Liste der Urlaubstage ── *@
@if (_vacationDays.Count == 0)
{
<MudPaper Elevation="0" Class="pa-4 rounded-lg text-center"
Style="background: var(--mud-palette-background-grey);">
<MudIcon Icon="@Icons.Material.Filled.BeachAccess"
Style="font-size:2.5rem; color:#CFD8DC" />
<MudText Color="Color.Secondary" Class="mt-1">
Noch keine Urlaubstage für @_vacYear eingetragen.
</MudText>
</MudPaper>
}
else
{
<MudList T="VacationDay" Dense="true">
@foreach (var v in _vacationDays)
{
<MudListItem>
<MudStack Row="true" AlignItems="AlignItems.Center"
Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.BeachAccess"
Size="Size.Small" Color="Color.Secondary" />
<MudStack Spacing="0">
<MudText Typo="Typo.body2" Style="font-weight:600">
@v.Date.ToString("dddd, dd. MMMM yyyy", _deCulture)
</MudText>
@if (!string.IsNullOrWhiteSpace(v.Note))
{
<MudText Typo="Typo.caption" Color="Color.Secondary">
@v.Note
</MudText>
}
</MudStack>
</MudStack>
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
Size="Size.Small" Color="Color.Error"
OnClick="@(async () => await RemoveVacation(v.Id))" />
</MudStack>
</MudListItem>
<MudDivider />
}
</MudList>
}
</MudStack>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
@* ── Feiertagsverwaltung ── *@
<MudCard Elevation="3" Class="rounded-xl">
<MudCardHeader Style="background: linear-gradient(90deg, rgba(0,188,212,0.1) 0%, transparent 100%);">
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Celebration" Color="Color.Tertiary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Feiertage (Deutschland)</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
Size="Size.Small" OnClick="@(() => ChangeHolYear(-1))" />
<MudText Typo="Typo.h6" Style="font-weight:700; min-width:50px; text-align:center">
@_holYear
</MudText>
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
Size="Size.Small" OnClick="@(() => ChangeHolYear(1))" />
<MudButton Variant="Variant.Filled" Color="Color.Tertiary"
StartIcon="@Icons.Material.Filled.CloudDownload"
OnClick="FetchHolidays"
Disabled="@_fetchingHolidays"
Size="Size.Small">
@if (_fetchingHolidays)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
Von API laden
</MudButton>
</MudStack>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
@if (_holHolidays.Count == 0)
{
<MudPaper Elevation="0" Class="pa-4 rounded-lg text-center"
Style="background: var(--mud-palette-background-grey);">
<MudIcon Icon="@Icons.Material.Filled.Celebration"
Style="font-size:2.5rem; color:#CFD8DC" />
<MudText Color="Color.Secondary" Class="mt-1">
Keine Feiertage f&#252;r @_holYear gespeichert. Klicke "Von API laden".
</MudText>
</MudPaper>
}
else
{
<MudList T="PublicHoliday" Dense="true">
@foreach (var h in _holHolidays)
{
<MudListItem>
<MudStack Row="true" AlignItems="AlignItems.Center"
Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Celebration"
Size="Size.Small" Color="Color.Tertiary" />
<MudStack Spacing="0">
<MudText Typo="Typo.body2" Style="font-weight:600">
@h.Date.ToString("dddd, dd. MMMM yyyy", _deCulture)
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@h.Name</MudText>
</MudStack>
</MudStack>
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
Size="Size.Small" Color="Color.Error"
OnClick="@(async () => await DeleteHoliday(h.Id))" />
</MudStack>
</MudListItem>
<MudDivider />
}
</MudList>
}
</MudCardContent>
</MudCard>
</MudStack>
}
@code {
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
private AppSettings? _settings;
private int _vacYear = DateTime.Today.Year;
private List<VacationDay> _vacationDays = [];
private DateTime? _newVacDateFrom;
private DateTime? _newVacDateTo;
private string _newVacNote = "";
private int _holYear = DateTime.Today.Year;
private List<PublicHoliday> _holHolidays = [];
private bool _fetchingHolidays;
private int _vacRemaining => (_settings?.VacationDaysPerYear ?? 0) - _vacationDays.Count;
private int VacationUsedPercent => _settings?.VacationDaysPerYear > 0
? (int)Math.Round(_vacationDays.Count * 100.0 / _settings.VacationDaysPerYear)
: 0;
// Arbeitstage-Konfiguration als Liste von (Label, Getter, Setter)
private static readonly (string Label, Func<AppSettings, bool> Get, Action<AppSettings, bool> Set)[] WorkDayToggles =
[
("Montag", s => s.WorkMonday, (s, v) => s.WorkMonday = v),
("Dienstag", s => s.WorkTuesday, (s, v) => s.WorkTuesday = v),
("Mittwoch", s => s.WorkWednesday, (s, v) => s.WorkWednesday = v),
("Donnerstag", s => s.WorkThursday, (s, v) => s.WorkThursday = v),
("Freitag", s => s.WorkFriday, (s, v) => s.WorkFriday = v),
("Samstag", s => s.WorkSaturday, (s, v) => s.WorkSaturday = v),
("Sonntag", s => s.WorkSunday, (s, v) => s.WorkSunday = v),
];
protected override async Task OnInitializedAsync()
{
_settings = await TrackerService.GetSettingsAsync();
await LoadVacations();
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
}
private async Task LoadVacations()
{
_vacationDays = await TrackerService.GetVacationDaysAsync(_vacYear);
}
private async Task ChangeYear(int delta)
{
_vacYear += delta;
await LoadVacations();
}
private async Task Save()
{
if (_settings == null) return;
await TrackerService.SaveSettingsAsync(_settings);
Snackbar.Add("Einstellungen gespeichert", Severity.Success);
}
private async Task AddVacation()
{
if (_newVacDateFrom == null) return;
var from = DateOnly.FromDateTime(_newVacDateFrom.Value);
var to = _newVacDateTo.HasValue ? DateOnly.FromDateTime(_newVacDateTo.Value) : from;
if (to < from) to = from;
var note = string.IsNullOrWhiteSpace(_newVacNote) ? null : _newVacNote.Trim();
var current = from;
var added = 0;
while (current <= to)
{
if (_settings!.IsWorkDay(current.DayOfWeek))
{
await TrackerService.AddVacationDayAsync(new VacationDay { Date = current, Note = note });
added++;
}
current = current.AddDays(1);
}
_newVacDateFrom = null;
_newVacDateTo = null;
_newVacNote = "";
await LoadVacations();
Snackbar.Add(added == 1 ? "Urlaubstag eingetragen" : $"{added} Urlaubstage eingetragen", Severity.Success);
}
private async Task RemoveVacation(int id)
{
await TrackerService.RemoveVacationDayAsync(id);
await LoadVacations();
Snackbar.Add("Urlaubstag entfernt", Severity.Info);
}
// ── Feiertage ────────────────────────────────────────────────
private async Task ChangeHolYear(int delta)
{
_holYear += delta;
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
}
private async Task FetchHolidays()
{
_fetchingHolidays = true;
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
_fetchingHolidays = false;
Snackbar.Add(message, success ? Severity.Success : Severity.Error);
}
private async Task DeleteHoliday(int id)
{
await HolidayService.DeleteAsync(id);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
Snackbar.Add("Feiertag entfernt", Severity.Info);
}
private static string FormatHours(double hours)
{
var ts = TimeSpan.FromHours(hours);
return $"{(int)ts.TotalHours}:{ts.Minutes:D2} h";
}
}