490 lines
27 KiB
Plaintext
490 lines
27 KiB
Plaintext
@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ü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";
|
||
}
|
||
}
|
||
|