Neuste Version

This commit is contained in:
2026-06-07 23:36:45 +02:00
commit f636392e52
363 changed files with 70842 additions and 0 deletions
+583
View File
@@ -0,0 +1,583 @@
@page "/settings"
@rendermode InteractiveServer
@attribute [Authorize]
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
@inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
<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>
@* ── Region & Feiertage ── *@
<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.Public" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Region & Feiertage</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="4">
<MudText Typo="Typo.body2" Color="Color.Secondary">
Wähle dein Bundesland aus, um Bundesland-spezifische Feiertage zu berücksichtigen.
</MudText>
<MudSelect T="string" Label="Bundesland" Variant="Variant.Outlined" @bind-Value="_settings.GermanState" Clearable="true" Placeholder="Nur bundesweite Feiertage">
@foreach (var state in GermanStates)
{
<MudSelectItem Value="@state.Key">@state.Value</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
@* ── Gleitzeitkonto ── *@
<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.AccountBalance" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Gleitzeitkonto-Start</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="4">
<MudDatePicker Label="Berechnungsstart"
@bind-Date="_flexStartDate"
HelperText="Gleitzeitberechnung läuft ab diesem Datum. Wenn leer, ab dem ersten Arbeitseintrag."
Variant="Variant.Outlined"
Clearable="true"
DateFormat="dd.MM.yyyy"
PickerVariant="PickerVariant.Inline" />
<MudNumericField @bind-Value="_settings.FlexTimeStartingBalanceHours"
Label="Anfangsüberstunden (h)"
Variant="Variant.Outlined"
Step="0.5"
Format="0.##"
HelperText="Stufensaldo (Guthaben/Schulden) zum Berechnungsstart" />
</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 _userId;
private DateTime? _flexStartDate
{
get => _settings?.FlexTimeStartDate?.ToDateTime(TimeOnly.MinValue);
set => _settings!.FlexTimeStartDate = value.HasValue ? DateOnly.FromDateTime(value.Value) : null;
}
private static readonly Dictionary<string, string> GermanStates = new()
{
{ "DE-BW", "Baden-Württemberg" },
{ "DE-BY", "Bayern" },
{ "DE-BE", "Berlin" },
{ "DE-BB", "Brandenburg" },
{ "DE-HB", "Bremen" },
{ "DE-HH", "Hamburg" },
{ "DE-HE", "Hessen" },
{ "DE-MV", "Mecklenburg-Vorpommern" },
{ "DE-NI", "Niedersachsen" },
{ "DE-NW", "Nordrhein-Westfalen" },
{ "DE-RP", "Rheinland-Pfalz" },
{ "DE-SL", "Saarland" },
{ "DE-SN", "Sachsen" },
{ "DE-ST", "Sachsen-Anhalt" },
{ "DE-SH", "Schleswig-Holstein" },
{ "DE-TH", "Thüringen" }
};
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()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
if (claim == null) return;
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
await LoadVacations();
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
}
private async Task LoadVacations()
{
_vacationDays = await TrackerService.GetVacationDaysAsync(_userId, _vacYear);
}
private async Task ChangeYear(int delta)
{
_vacYear += delta;
await LoadVacations();
}
private async Task Save()
{
if (_settings == null) return;
await TrackerService.SaveSettingsAsync(_settings);
// Nach dem Speichern Feiertage neu laden, falls sich das Bundesland geändert hat
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
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 { UserId = _userId, 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(_userId, id);
await LoadVacations();
Snackbar.Add("Urlaubstag entfernt", Severity.Info);
}
// ── Feiertage ────────────────────────────────────────────────
private async Task ChangeHolYear(int delta)
{
_holYear += delta;
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
}
private async Task FetchHolidays()
{
_fetchingHolidays = true;
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
_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, _settings?.GermanState);
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";
}
}