7 Commits

Author SHA1 Message Date
mrcwlnd 73599c00d1 docker-compose.yml aktualisiert 2026-05-22 07:36:19 +00:00
mrcwlnd 64c5f6aa2c docker-compose.yml aktualisiert 2026-05-22 07:29:54 +00:00
mrcwlnd 5431618d43 Dockerfile aktualisiert 2026-05-22 07:29:38 +00:00
mrcwlnd 598243ddc9 docker-compose.yml hinzugefügt 2026-05-22 07:27:32 +00:00
Wieland, Marc 9fd50f86c0 added dockerfile 2 2026-05-22 09:22:47 +02:00
Wieland, Marc 0467f45036 Added dockerfile 2026-05-22 09:21:57 +02:00
Wieland, Marc 88ac175190 first commit 2026-05-22 09:18:01 +02:00
385 changed files with 6769 additions and 2835 deletions
-11
View File
@@ -1,11 +0,0 @@
# SQLite Datenbanken und Journal-Dateien
*.db
*.db-shm
*.db-wal
# Build-Artefakte
bin/
obj/
*.user
*.suo
*.vs/
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -13,11 +13,12 @@
<ImportMap /> <ImportMap />
<link rel="icon" type="image/svg+xml" href="favicon.svg" /> <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="alternate icon" type="image/png" href="favicon.png" /> <link rel="alternate icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveWebAssembly" /> <HeadOutlet />
</head> </head>
<body> <body>
<Routes /> <Routes />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script> <script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script> <script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body> </body>
@@ -1,9 +1,4 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Nav
@inject IUserNotificationService UserNotificationService
@implements IDisposable
@using System.Security.Claims
<MudThemeProvider Theme="_theme" /> <MudThemeProvider Theme="_theme" />
<MudPopoverProvider /> <MudPopoverProvider />
@@ -34,26 +29,6 @@
@code { @code {
private bool _drawerOpen = true; private bool _drawerOpen = true;
protected override void OnInitialized()
{
UserNotificationService.OnUserDeleted += HandleUserDeleted;
}
private async Task HandleUserDeleted(int deletedUserId)
{
var state = await AuthStateProvider.GetAuthenticationStateAsync();
var idClaim = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (idClaim != null && int.TryParse(idClaim, out var myId) && myId == deletedUserId)
{
await InvokeAsync(() => Nav.NavigateTo("/auth/logout", forceLoad: true));
}
}
public void Dispose()
{
UserNotificationService.OnUserDeleted -= HandleUserDeleted;
}
private readonly MudTheme _theme = new() private readonly MudTheme _theme = new()
{ {
PaletteLight = new PaletteLight PaletteLight = new PaletteLight
+10
View File
@@ -0,0 +1,10 @@
<MudNavMenu>
<MudText Typo="Typo.h6" Class="px-4 mt-4 mb-2">Navigation</MudText>
<MudDivider Class="mb-2" />
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.CalendarMonth">Wochenübersicht</MudNavLink>
<MudNavLink Href="month" Icon="@Icons.Material.Filled.CalendarViewMonth">Monatsübersicht</MudNavLink>
<MudNavLink Href="feiertage" Icon="@Icons.Material.Filled.Celebration">Feiertage</MudNavLink>
<MudNavLink Href="urlaub-maximizer" Icon="@Icons.Material.Filled.AutoAwesome">Urlaubs-Maximizer</MudNavLink>
<MudNavLink Href="settings" Icon="@Icons.Material.Filled.Settings">Einstellungen</MudNavLink>
</MudNavMenu>
+31
View File
@@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>
+157
View File
@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}
+63
View File
@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
@@ -1,9 +1,6 @@
@page "/feiertage" @page "/feiertage"
@rendermode InteractiveWebAssembly @rendermode InteractiveServer
@attribute [Authorize] @inject HolidayService HolidayService
@inject IHolidayService HolidayService
@inject ITimetrackerService TrackerService
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>Feiertage Timetracker</PageTitle> <PageTitle>Feiertage Timetracker</PageTitle>
@@ -188,25 +185,15 @@ else
private List<PublicHoliday> _holidays = []; private List<PublicHoliday> _holidays = [];
private string _subLabel = ""; private string _subLabel = "";
private int _userId;
private AppSettings _settings = new();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var claim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (claim != null)
{
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
}
await LoadHolidays(); await LoadHolidays();
_loading = false; _loading = false;
} }
private async Task LoadHolidays() private async Task LoadHolidays()
{ {
_holidays = await HolidayService.GetHolidaysAsync(_year, _settings.GermanState); _holidays = await HolidayService.GetHolidaysAsync(_year);
var count = _holidays.Count; var count = _holidays.Count;
var past = _holidays.Count(h => h.Date < DateOnly.FromDateTime(DateTime.Today)); var past = _holidays.Count(h => h.Date < DateOnly.FromDateTime(DateTime.Today));
_subLabel = count == 0 _subLabel = count == 0
@@ -1,10 +1,8 @@
@page "/" @page "/"
@rendermode InteractiveWebAssembly @rendermode InteractiveServer
@attribute [Authorize] @inject TimetrackerService TrackerService
@inject ITimetrackerService TrackerService @inject HolidayService HolidayService
@inject IHolidayService HolidayService
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>KW @_kw Wochenübersicht Timetracker</PageTitle> <PageTitle>KW @_kw Wochenübersicht Timetracker</PageTitle>
@@ -334,7 +332,6 @@ else
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
private bool _loading = true; private bool _loading = true;
private int _userId;
private DateOnly _monday; private DateOnly _monday;
private List<DayVm> _days = []; private List<DayVm> _days = [];
private AppSettings _settings = new(); private AppSettings _settings = new();
@@ -352,49 +349,26 @@ else
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
if (claim == null) return; // Prerender-Pass Circuit noch nicht authentifiziert
_userId = int.Parse(claim.Value);
_monday = GetMonday(DateOnly.FromDateTime(DateTime.Today)); _monday = GetMonday(DateOnly.FromDateTime(DateTime.Today));
_settings = await TrackerService.GetSettingsAsync(_userId); _settings = await TrackerService.GetSettingsAsync();
await LoadWeek();
var loadWeekTask = LoadWeek(); _totalOvertime = await TrackerService.GetTotalOvertimeAsync(_settings);
var overtimeTask = TrackerService.GetTotalOvertimeAsync(_userId, _settings);
await Task.WhenAll(loadWeekTask, overtimeTask);
_totalOvertime = await overtimeTask;
_loading = false; _loading = false;
} }
private async Task LoadWeek() private async Task LoadWeek()
{ {
Task<List<PublicHoliday>> holidaysTask;
if (_monday.Year != _holidayYear) if (_monday.Year != _holidayYear)
{ {
holidaysTask = HolidayService.GetHolidaysAsync(_monday.Year, _settings.GermanState); var list = await HolidayService.GetHolidaysAsync(_monday.Year);
}
else
{
holidaysTask = Task.FromResult(new List<PublicHoliday>());
}
var dbDaysTask = TrackerService.GetWeekAsync(_userId, _monday);
await Task.WhenAll(holidaysTask, dbDaysTask);
if (_monday.Year != _holidayYear)
{
var list = await holidaysTask;
_holidays = list.ToDictionary(h => h.Date, h => h.Name); _holidays = list.ToDictionary(h => h.Date, h => h.Name);
_holidayYear = _monday.Year; _holidayYear = _monday.Year;
} }
var dbDays = await TrackerService.GetWeekAsync(_monday);
var dbDays = await dbDaysTask;
_days = Enumerable.Range(0, 7).Select(i => _days = Enumerable.Range(0, 7).Select(i =>
{ {
var date = _monday.AddDays(i); var date = _monday.AddDays(i);
return DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date, _userId); return DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date);
}).ToList(); }).ToList();
BuildWeekLabels(); BuildWeekLabels();
} }
@@ -447,7 +421,7 @@ else
private async Task SaveDay(DayVm day) private async Task SaveDay(DayVm day)
{ {
await TrackerService.UpsertWorkDayAsync(day.ToWorkDay()); await TrackerService.UpsertWorkDayAsync(day.ToWorkDay());
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings); _totalOvertime = await TrackerService.GetTotalOvertimeAsync(_settings);
BuildWeekLabels(); BuildWeekLabels();
} }
@@ -512,7 +486,6 @@ else
private sealed class DayVm private sealed class DayVm
{ {
public int Id { get; set; } public int Id { get; set; }
public int UserId { get; set; }
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
public TimeSpan? Start { get; set; } public TimeSpan? Start { get; set; }
public TimeSpan? End { get; set; } public TimeSpan? End { get; set; }
@@ -527,10 +500,9 @@ else
public TimeSpan? NetWork => GrossWork.HasValue ? GrossWork.Value - TotalBreakTime : null; public TimeSpan? NetWork => GrossWork.HasValue ? GrossWork.Value - TotalBreakTime : null;
public static DayVm From(WorkDay? wd, DateOnly date, int userId) => new() public static DayVm From(WorkDay? wd, DateOnly date) => new()
{ {
Id = wd?.Id ?? 0, Id = wd?.Id ?? 0,
UserId = wd?.UserId ?? userId,
Date = date, Date = date,
Start = wd?.StartTime?.ToTimeSpan(), Start = wd?.StartTime?.ToTimeSpan(),
End = wd?.EndTime?.ToTimeSpan(), End = wd?.EndTime?.ToTimeSpan(),
@@ -545,7 +517,6 @@ else
public WorkDay ToWorkDay() => new() public WorkDay ToWorkDay() => new()
{ {
Id = Id, Id = Id,
UserId = UserId,
Date = Date, Date = Date,
StartTime = Start.HasValue ? TimeOnly.FromTimeSpan(Start.Value) : null, StartTime = Start.HasValue ? TimeOnly.FromTimeSpan(Start.Value) : null,
EndTime = End.HasValue ? TimeOnly.FromTimeSpan(End.Value) : null, EndTime = End.HasValue ? TimeOnly.FromTimeSpan(End.Value) : null,
@@ -1,9 +1,7 @@
@page "/month" @page "/month"
@rendermode InteractiveWebAssembly @rendermode InteractiveServer
@attribute [Authorize] @inject TimetrackerService TrackerService
@inject ITimetrackerService TrackerService @inject HolidayService HolidayService
@inject IHolidayService HolidayService
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year Monatsübersicht Timetracker</PageTitle> <PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year Monatsübersicht Timetracker</PageTitle>
@@ -152,7 +150,6 @@ else
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
private bool _loading = true; private bool _loading = true;
private int _userId;
private int _year = DateTime.Today.Year; private int _year = DateTime.Today.Year;
private int _month = DateTime.Today.Month; private int _month = DateTime.Today.Month;
private List<MonthDayVm> _days = []; private List<MonthDayVm> _days = [];
@@ -170,26 +167,16 @@ else
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var authState = await AuthStateProvider.GetAuthenticationStateAsync(); _settings = await TrackerService.GetSettingsAsync();
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
if (claim == null) return;
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
await LoadMonth(); await LoadMonth();
_loading = false; _loading = false;
} }
private async Task LoadMonth() private async Task LoadMonth()
{ {
var workDaysTask = TrackerService.GetMonthAsync(_userId, _year, _month); var workDays = await TrackerService.GetMonthAsync(_year, _month);
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState); var holidays = await HolidayService.GetHolidaysAsync(_year);
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year); var vacations = await TrackerService.GetVacationDaysAsync(_year);
await Task.WhenAll(workDaysTask, holidaysTask, vacationsTask);
var workDays = await workDaysTask;
var holidays = await holidaysTask;
var vacations = await vacationsTask;
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name); var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
var vacationSet = vacations.Select(v => v.Date).ToHashSet(); var vacationSet = vacations.Select(v => v.Date).ToHashSet();
@@ -1,10 +1,8 @@
@page "/settings" @page "/settings"
@rendermode InteractiveWebAssembly @rendermode InteractiveServer
@attribute [Authorize] @inject TimetrackerService TrackerService
@inject ITimetrackerService TrackerService @inject HolidayService HolidayService
@inject IHolidayService HolidayService
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>Einstellungen Timetracker</PageTitle> <PageTitle>Einstellungen Timetracker</PageTitle>
@@ -128,65 +126,6 @@ else
</MudCard> </MudCard>
</MudItem> </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> </MudGrid>
@* ── Speichern-Button ── *@ @* ── Speichern-Button ── *@
@@ -436,33 +375,6 @@ else
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
private AppSettings? _settings; 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 int _vacYear = DateTime.Today.Year;
private List<VacationDay> _vacationDays = []; private List<VacationDay> _vacationDays = [];
private DateTime? _newVacDateFrom; private DateTime? _newVacDateFrom;
@@ -490,22 +402,14 @@ else
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var authState = await AuthStateProvider.GetAuthenticationStateAsync(); _settings = await TrackerService.GetSettingsAsync();
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); await LoadVacations();
if (claim == null) return; _holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
var loadVacationsTask = LoadVacations();
var loadHolidaysTask = HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
await Task.WhenAll(loadVacationsTask, loadHolidaysTask);
_holHolidays = await loadHolidaysTask;
} }
private async Task LoadVacations() private async Task LoadVacations()
{ {
_vacationDays = await TrackerService.GetVacationDaysAsync(_userId, _vacYear); _vacationDays = await TrackerService.GetVacationDaysAsync(_vacYear);
} }
private async Task ChangeYear(int delta) private async Task ChangeYear(int delta)
@@ -518,8 +422,6 @@ else
{ {
if (_settings == null) return; if (_settings == null) return;
await TrackerService.SaveSettingsAsync(_settings); 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); Snackbar.Add("Einstellungen gespeichert", Severity.Success);
} }
@@ -536,7 +438,7 @@ else
{ {
if (_settings!.IsWorkDay(current.DayOfWeek)) if (_settings!.IsWorkDay(current.DayOfWeek))
{ {
await TrackerService.AddVacationDayAsync(new VacationDay { UserId = _userId, Date = current, Note = note }); await TrackerService.AddVacationDayAsync(new VacationDay { Date = current, Note = note });
added++; added++;
} }
current = current.AddDays(1); current = current.AddDays(1);
@@ -550,7 +452,7 @@ else
private async Task RemoveVacation(int id) private async Task RemoveVacation(int id)
{ {
await TrackerService.RemoveVacationDayAsync(_userId, id); await TrackerService.RemoveVacationDayAsync(id);
await LoadVacations(); await LoadVacations();
Snackbar.Add("Urlaubstag entfernt", Severity.Info); Snackbar.Add("Urlaubstag entfernt", Severity.Info);
} }
@@ -559,14 +461,14 @@ else
private async Task ChangeHolYear(int delta) private async Task ChangeHolYear(int delta)
{ {
_holYear += delta; _holYear += delta;
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState); _holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
} }
private async Task FetchHolidays() private async Task FetchHolidays()
{ {
_fetchingHolidays = true; _fetchingHolidays = true;
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear); var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState); _holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
_fetchingHolidays = false; _fetchingHolidays = false;
Snackbar.Add(message, success ? Severity.Success : Severity.Error); Snackbar.Add(message, success ? Severity.Success : Severity.Error);
} }
@@ -574,7 +476,7 @@ else
private async Task DeleteHoliday(int id) private async Task DeleteHoliday(int id)
{ {
await HolidayService.DeleteAsync(id); await HolidayService.DeleteAsync(id);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState); _holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
Snackbar.Add("Feiertag entfernt", Severity.Info); Snackbar.Add("Feiertag entfernt", Severity.Info);
} }
@@ -1,10 +1,8 @@
@page "/urlaub-maximizer" @page "/urlaub-maximizer"
@rendermode InteractiveWebAssembly @rendermode InteractiveServer
@attribute [Authorize] @inject TimetrackerService TrackerService
@inject ITimetrackerService TrackerService @inject HolidayService HolidayService
@inject IHolidayService HolidayService
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>Urlaubs-Maximizer Timetracker</PageTitle> <PageTitle>Urlaubs-Maximizer Timetracker</PageTitle>
@@ -234,28 +232,18 @@ else
private int _remainingDays; private int _remainingDays;
private List<Suggestion> _suggestions = []; private List<Suggestion> _suggestions = [];
private string _subLabel = ""; private string _subLabel = "";
private int _userId;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var authState = await AuthStateProvider.GetAuthenticationStateAsync(); _settings = await TrackerService.GetSettingsAsync();
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
if (claim == null) return;
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
await LoadYear(); await LoadYear();
_loading = false; _loading = false;
} }
private async Task LoadYear() private async Task LoadYear()
{ {
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState); var holidays = await HolidayService.GetHolidaysAsync(_year);
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year); var vacations = await TrackerService.GetVacationDaysAsync(_year);
await Task.WhenAll(holidaysTask, vacationsTask);
var holidays = await holidaysTask;
var vacations = await vacationsTask;
_holidays = holidays.ToDictionary(h => h.Date, h => h.Name); _holidays = holidays.ToDictionary(h => h.Date, h => h.Name);
_vacationSet = vacations.Select(v => v.Date).ToHashSet(); _vacationSet = vacations.Select(v => v.Date).ToHashSet();
_remainingDays = Math.Max(0, _settings.VacationDaysPerYear - vacations.Count); _remainingDays = Math.Max(0, _settings.VacationDaysPerYear - vacations.Count);
@@ -274,7 +262,7 @@ else
private async Task TakeSuggestion(Suggestion s) private async Task TakeSuggestion(Suggestion s)
{ {
foreach (var d in s.VacationDaysToTake.Where(d => !_vacationSet.Contains(d))) foreach (var d in s.VacationDaysToTake.Where(d => !_vacationSet.Contains(d)))
await TrackerService.AddVacationDayAsync(new VacationDay { UserId = _userId, Date = d, Note = "Urlaubs-Maximizer" }); await TrackerService.AddVacationDayAsync(new VacationDay { Date = d, Note = "Urlaubs-Maximizer" });
await LoadYear(); await LoadYear();
var word = s.VacationDaysNeeded == 1 ? "Urlaubstag" : "Urlaubstage"; var word = s.VacationDaysNeeded == 1 ? "Urlaubstag" : "Urlaubstage";
Snackbar.Add($"{s.VacationDaysNeeded} {word} eingetragen {s.TotalFreeDays} Tage frei!", Severity.Success); Snackbar.Add($"{s.VacationDaysNeeded} {word} eingetragen {s.TotalFreeDays} Tage frei!", Severity.Success);
+8
View File
@@ -0,0 +1,8 @@
@rendermode InteractiveServer
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@@ -1,8 +1,5 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@@ -10,8 +7,7 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using timetracker @using timetracker
@using timetracker.Client.Components @using timetracker.Components
@using timetracker.Client.Components.Layout @using timetracker.Components.Layout
@using timetracker.Data @using timetracker.Data
@using timetracker.Shared
@using MudBlazor @using MudBlazor
@@ -1,15 +1,11 @@
namespace timetracker.Shared; namespace timetracker.Data;
public class AppSettings public class AppSettings
{ {
public int Id { get; set; } public int Id { get; set; }
public int UserId { get; set; }
public double DailyTargetHours { get; set; } = 7.5; public double DailyTargetHours { get; set; } = 7.5;
public int MinimumBreakMinutes { get; set; } = 30; public int MinimumBreakMinutes { get; set; } = 30;
public int VacationDaysPerYear { get; set; } = 30; public int VacationDaysPerYear { get; set; } = 30;
public string? GermanState { get; set; }
public DateOnly? FlexTimeStartDate { get; set; }
public double FlexTimeStartingBalanceHours { get; set; } = 0.0;
// Arbeitstage // Arbeitstage
public bool WorkMonday { get; set; } = true; public bool WorkMonday { get; set; } = true;
@@ -1,10 +1,9 @@
namespace timetracker.Shared; namespace timetracker.Data;
public class BreakEntry public class BreakEntry
{ {
public int Id { get; set; } public int Id { get; set; }
public int WorkDayId { get; set; } public int WorkDayId { get; set; }
[System.Text.Json.Serialization.JsonIgnore]
public WorkDay WorkDay { get; set; } = null!; public WorkDay WorkDay { get; set; } = null!;
public TimeOnly? StartTime { get; set; } public TimeOnly? StartTime { get; set; }
public TimeOnly? EndTime { get; set; } public TimeOnly? EndTime { get; set; }
@@ -1,30 +1,20 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using timetracker.Shared;
namespace timetracker.Data; namespace timetracker.Data;
public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, HttpClient http) : IHolidayService public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, HttpClient http)
{ {
private const string ApiUrl = "https://date.nager.at/api/v3/PublicHolidays/{0}/DE"; private const string ApiUrl = "https://date.nager.at/api/v3/PublicHolidays/{0}/DE";
public async Task<List<PublicHoliday>> GetHolidaysAsync(int year, string? stateCode = null) public async Task<List<PublicHoliday>> GetHolidaysAsync(int year)
{ {
await using var db = await factory.CreateDbContextAsync(); await using var db = await factory.CreateDbContextAsync();
var holidays = await db.PublicHolidays return await db.PublicHolidays
.Where(h => h.Date.Year == year) .Where(h => h.Date.Year == year)
.OrderBy(h => h.Date) .OrderBy(h => h.Date)
.ToListAsync(); .ToListAsync();
if (string.IsNullOrEmpty(stateCode))
{
// Default: return only global holidays (where Counties is null or empty)
return holidays.Where(h => string.IsNullOrEmpty(h.Counties)).ToList();
}
// Return global holidays OR holidays that match the user's state code
return holidays.Where(h => string.IsNullOrEmpty(h.Counties) || h.Counties.Split(',').Contains(stateCode)).ToList();
} }
public async Task<(bool Success, string Message)> FetchAndStoreAsync(int year) public async Task<(bool Success, string Message)> FetchAndStoreAsync(int year)
@@ -44,8 +34,7 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
.Select(h => new PublicHoliday .Select(h => new PublicHoliday
{ {
Date = DateOnly.Parse(h.Date), Date = DateOnly.Parse(h.Date),
Name = h.LocalName, Name = h.LocalName
Counties = h.Counties != null && h.Counties.Count > 0 ? string.Join(",", h.Counties) : null
})); }));
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@@ -75,9 +64,5 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
[JsonPropertyName("localName")] [JsonPropertyName("localName")]
public string LocalName { get; set; } = ""; public string LocalName { get; set; } = "";
[JsonPropertyName("counties")]
public List<string>? Counties { get; set; }
} }
} }
@@ -26,21 +26,9 @@ namespace timetracker.Data.Migrations
b.Property<double>("DailyTargetHours") b.Property<double>("DailyTargetHours")
.HasColumnType("REAL"); .HasColumnType("REAL");
b.Property<DateOnly?>("FlexTimeStartDate")
.HasColumnType("TEXT");
b.Property<double>("FlexTimeStartingBalanceHours")
.HasColumnType("REAL");
b.Property<string>("GermanState")
.HasColumnType("TEXT");
b.Property<int>("MinimumBreakMinutes") b.Property<int>("MinimumBreakMinutes")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("VacationDaysPerYear") b.Property<int>("VacationDaysPerYear")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@@ -70,6 +58,24 @@ namespace timetracker.Data.Migrations
b.ToTable("AppSettings"); b.ToTable("AppSettings");
}); });
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("PublicHolidays");
});
modelBuilder.Entity("timetracker.Data.BreakEntry", b => modelBuilder.Entity("timetracker.Data.BreakEntry", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -92,50 +98,6 @@ namespace timetracker.Data.Migrations
b.ToTable("BreakEntries"); b.ToTable("BreakEntries");
}); });
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Counties")
.HasColumnType("TEXT");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("PublicHolidays");
});
modelBuilder.Entity("timetracker.Data.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PasswordSalt")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("timetracker.Data.VacationDay", b => modelBuilder.Entity("timetracker.Data.VacationDay", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -148,9 +110,6 @@ namespace timetracker.Data.Migrations
b.Property<string>("Note") b.Property<string>("Note")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("VacationDays"); b.ToTable("VacationDays");
@@ -171,9 +130,6 @@ namespace timetracker.Data.Migrations
b.Property<TimeOnly?>("StartTime") b.Property<TimeOnly?>("StartTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("WorkDays"); b.ToTable("WorkDays");
@@ -1,9 +1,8 @@
namespace timetracker.Shared; namespace timetracker.Data;
public class PublicHoliday public class PublicHoliday
{ {
public int Id { get; set; } public int Id { get; set; }
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string? Counties { get; set; }
} }
@@ -1,11 +1,9 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using timetracker.Shared;
namespace timetracker.Data; namespace timetracker.Data;
public class TimetrackerDbContext(DbContextOptions<TimetrackerDbContext> options) : DbContext(options) public class TimetrackerDbContext(DbContextOptions<TimetrackerDbContext> options) : DbContext(options)
{ {
public DbSet<User> Users => Set<User>();
public DbSet<WorkDay> WorkDays => Set<WorkDay>(); public DbSet<WorkDay> WorkDays => Set<WorkDay>();
public DbSet<BreakEntry> BreakEntries => Set<BreakEntry>(); public DbSet<BreakEntry> BreakEntries => Set<BreakEntry>();
public DbSet<AppSettings> AppSettings => Set<AppSettings>(); public DbSet<AppSettings> AppSettings => Set<AppSettings>();
+146
View File
@@ -0,0 +1,146 @@
using Microsoft.EntityFrameworkCore;
namespace timetracker.Data;
public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
{
public async Task<List<WorkDay>> GetWeekAsync(DateOnly monday)
{
await using var db = await factory.CreateDbContextAsync();
return await db.WorkDays
.Include(w => w.Breaks)
.Where(w => w.Date >= monday && w.Date < monday.AddDays(7))
.OrderBy(w => w.Date)
.ToListAsync();
}
public async Task UpsertWorkDayAsync(WorkDay workDay)
{
await using var db = await factory.CreateDbContextAsync();
var existing = await db.WorkDays
.Include(w => w.Breaks)
.FirstOrDefaultAsync(w => w.Date == workDay.Date);
if (existing == null)
{
workDay.Id = 0;
foreach (var b in workDay.Breaks) b.Id = 0;
db.WorkDays.Add(workDay);
}
else
{
existing.StartTime = workDay.StartTime;
existing.EndTime = workDay.EndTime;
db.BreakEntries.RemoveRange(existing.Breaks);
existing.Breaks.Clear();
foreach (var b in workDay.Breaks)
existing.Breaks.Add(new BreakEntry
{
WorkDayId = existing.Id,
StartTime = b.StartTime,
EndTime = b.EndTime
});
}
await db.SaveChangesAsync();
}
public async Task<AppSettings> GetSettingsAsync()
{
await using var db = await factory.CreateDbContextAsync();
return await db.AppSettings.FindAsync(1) ?? new AppSettings { Id = 1 };
}
public async Task SaveSettingsAsync(AppSettings settings)
{
await using var db = await factory.CreateDbContextAsync();
settings.Id = 1;
var existing = await db.AppSettings.FindAsync(1);
if (existing == null)
db.AppSettings.Add(settings);
else
{
existing.DailyTargetHours = settings.DailyTargetHours;
existing.MinimumBreakMinutes = settings.MinimumBreakMinutes;
existing.VacationDaysPerYear = settings.VacationDaysPerYear;
existing.WorkMonday = settings.WorkMonday;
existing.WorkTuesday = settings.WorkTuesday;
existing.WorkWednesday = settings.WorkWednesday;
existing.WorkThursday = settings.WorkThursday;
existing.WorkFriday = settings.WorkFriday;
existing.WorkSaturday = settings.WorkSaturday;
existing.WorkSunday = settings.WorkSunday;
}
await db.SaveChangesAsync();
}
// ── Urlaub ────────────────────────────────────────────────────────────
public async Task<List<VacationDay>> GetVacationDaysAsync(int year)
{
await using var db = await factory.CreateDbContextAsync();
return await db.VacationDays
.Where(v => v.Date.Year == year)
.OrderBy(v => v.Date)
.ToListAsync();
}
public async Task AddVacationDayAsync(VacationDay vacationDay)
{
await using var db = await factory.CreateDbContextAsync();
var exists = await db.VacationDays.AnyAsync(v => v.Date == vacationDay.Date);
if (!exists)
{
vacationDay.Id = 0;
db.VacationDays.Add(vacationDay);
await db.SaveChangesAsync();
}
}
public async Task RemoveVacationDayAsync(int id)
{
await using var db = await factory.CreateDbContextAsync();
var v = await db.VacationDays.FindAsync(id);
if (v != null)
{
db.VacationDays.Remove(v);
await db.SaveChangesAsync();
}
}
// ── Gleitzeitkonto ───────────────────────────────────────────────────
public async Task<TimeSpan> GetTotalOvertimeAsync(AppSettings settings)
{
await using var db = await factory.CreateDbContextAsync();
var allDays = await db.WorkDays
.Include(w => w.Breaks)
.Where(w => w.StartTime != null && w.EndTime != null)
.ToListAsync();
var total = TimeSpan.Zero;
foreach (var wd in allDays)
{
if (!settings.IsWorkDay(wd.Date.DayOfWeek)) continue;
var gross = wd.EndTime!.Value.ToTimeSpan() - wd.StartTime!.Value.ToTimeSpan();
if (gross <= TimeSpan.Zero) continue;
var breakTotal = wd.Breaks
.Where(b => b.StartTime.HasValue && b.EndTime.HasValue && b.EndTime > b.StartTime)
.Aggregate(TimeSpan.Zero, (s, b) =>
s + (b.EndTime!.Value.ToTimeSpan() - b.StartTime!.Value.ToTimeSpan()));
total += gross - breakTotal - TimeSpan.FromHours(settings.DailyTargetHours);
}
return total;
}
// ── Monatsübersicht ───────────────────────────────────────────────────
public async Task<List<WorkDay>> GetMonthAsync(int year, int month)
{
await using var db = await factory.CreateDbContextAsync();
var from = new DateOnly(year, month, 1);
var to = from.AddMonths(1);
return await db.WorkDays
.Include(w => w.Breaks)
.Where(w => w.Date >= from && w.Date < to)
.OrderBy(w => w.Date)
.ToListAsync();
}
}
@@ -1,9 +1,8 @@
namespace timetracker.Shared; namespace timetracker.Data;
public class VacationDay public class VacationDay
{ {
public int Id { get; set; } public int Id { get; set; }
public int UserId { get; set; }
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
public string? Note { get; set; } public string? Note { get; set; }
} }
@@ -1,9 +1,8 @@
namespace timetracker.Shared; namespace timetracker.Data;
public class WorkDay public class WorkDay
{ {
public int Id { get; set; } public int Id { get; set; }
public int UserId { get; set; }
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
public TimeOnly? StartTime { get; set; } public TimeOnly? StartTime { get; set; }
public TimeOnly? EndTime { get; set; } public TimeOnly? EndTime { get; set; }
+7 -19
View File
@@ -2,35 +2,23 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src WORKDIR /src
# Copy project files for restoring dependencies # Keine Unterordner mehr beim Kopieren!
COPY timetracker.slnx ./ COPY timetracker.csproj ./
COPY timetracker.Server/timetracker.Server.csproj timetracker.Server/ RUN dotnet restore timetracker.csproj
COPY timetracker.Client/timetracker.Client.csproj timetracker.Client/
COPY timetracker.Shared/timetracker.Shared.csproj timetracker.Shared/
# Restore dependencies COPY . ./
RUN dotnet restore timetracker.slnx RUN dotnet publish -c Release -o /app/publish --no-restore
# Copy the rest of the source code
COPY timetracker.Server/ timetracker.Server/
COPY timetracker.Client/ timetracker.Client/
COPY timetracker.Shared/ timetracker.Shared/
# Publish
WORKDIR /src/timetracker.Server
RUN dotnet publish -c Release -o /app/publish
# ── Runtime Stage ───────────────────────────────────────────────────────────── # ── Runtime Stage ─────────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app WORKDIR /app
COPY --from=build /app/publish . COPY --from=build /app/publish .
# Directory for SQLite database # Verzeichnis für die SQLite-Datenbank
RUN mkdir -p /data RUN mkdir -p /data
ENV ASPNETCORE_HTTP_PORTS=8080 ENV ASPNETCORE_HTTP_PORTS=8080
ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
ENV TIMETRACKER_DB_PATH=/data/timetracker.db ENV TIMETRACKER_DB_PATH=/data/timetracker.db
ENV EnableHttpsRedirect=false ENV EnableHttpsRedirect=false
@@ -38,4 +26,4 @@ EXPOSE 8080
VOLUME ["/data"] VOLUME ["/data"]
ENTRYPOINT ["dotnet", "timetracker.Server.dll"] ENTRYPOINT ["dotnet", "timetracker.dll"]
+48
View File
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using timetracker.Components;
using timetracker.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddMudServices();
builder.Services.AddHttpClient<HolidayService>();
var dbPath = Environment.GetEnvironmentVariable("TIMETRACKER_DB_PATH")
?? Path.Combine(builder.Environment.ContentRootPath, "timetracker.db");
builder.Services.AddDbContextFactory<TimetrackerDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
builder.Services.AddScoped<TimetrackerService>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<TimetrackerDbContext>>();
await using var db = await factory.CreateDbContextAsync();
await db.Database.MigrateAsync();
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
if (app.Configuration.GetValue("EnableHttpsRedirect", !app.Environment.IsDevelopment()))
{
app.UseHttpsRedirection();
}
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.IO.Redist" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.1.0.0" newVersion="6.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.4.0" newVersion="4.0.4.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.1.0" newVersion="6.0.1.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.1.0" newVersion="4.2.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Binary file not shown.
@@ -0,0 +1,171 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/5.0.0-2.25567.12": {
"dependencies": {
"Microsoft.Build.Locator": "1.10.2",
"Newtonsoft.Json": "13.0.3",
"System.Collections.Immutable": "9.0.0",
"System.CommandLine": "2.0.0-rtm.25509.106"
},
"runtime": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll": {}
},
"resources": {
"cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "cs"
},
"de/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "de"
},
"es/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "es"
},
"fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "fr"
},
"it/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "it"
},
"ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ja"
},
"ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ko"
},
"pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "pl"
},
"pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "pt-BR"
},
"ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ru"
},
"tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "tr"
},
"zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "zh-Hans"
},
"zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.Build.Locator/1.10.2": {
"runtime": {
"lib/net8.0/Microsoft.Build.Locator.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.10.2.26959"
}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"System.Collections.Immutable/9.0.0": {
"runtime": {
"lib/net8.0/System.Collections.Immutable.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.24.52809"
}
}
},
"System.CommandLine/2.0.0-rtm.25509.106": {
"runtime": {
"lib/net8.0/System.CommandLine.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.25.51006"
}
},
"resources": {
"lib/net8.0/cs/System.CommandLine.resources.dll": {
"locale": "cs"
},
"lib/net8.0/de/System.CommandLine.resources.dll": {
"locale": "de"
},
"lib/net8.0/es/System.CommandLine.resources.dll": {
"locale": "es"
},
"lib/net8.0/fr/System.CommandLine.resources.dll": {
"locale": "fr"
},
"lib/net8.0/it/System.CommandLine.resources.dll": {
"locale": "it"
},
"lib/net8.0/ja/System.CommandLine.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ko/System.CommandLine.resources.dll": {
"locale": "ko"
},
"lib/net8.0/pl/System.CommandLine.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pt-BR/System.CommandLine.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/ru/System.CommandLine.resources.dll": {
"locale": "ru"
},
"lib/net8.0/tr/System.CommandLine.resources.dll": {
"locale": "tr"
},
"lib/net8.0/zh-Hans/System.CommandLine.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hant/System.CommandLine.resources.dll": {
"locale": "zh-Hant"
}
}
}
}
},
"libraries": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/5.0.0-2.25567.12": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.Build.Locator/1.10.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-F+nLS7IpgtslyxNvtD6Jalnf5WU08lu8yfJBNQl3cbEF3AMUphs4t7nPuRYaaU8QZyGrqtVi7i73LhAe/yHx7A==",
"path": "microsoft.build.locator/1.10.2",
"hashPath": "microsoft.build.locator.1.10.2.nupkg.sha512"
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"System.Collections.Immutable/9.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==",
"path": "system.collections.immutable/9.0.0",
"hashPath": "system.collections.immutable.9.0.0.nupkg.sha512"
},
"System.CommandLine/2.0.0-rtm.25509.106": {
"type": "package",
"serviceable": true,
"sha512": "sha512-IdCQOFNHQfK0hu3tzWOHFJLMaiEOR/4OynmOh+IfukrTIsCR4TTDm7lpuXQyMZ0eRfIyUcz06gHGJNlILAq/6A==",
"path": "system.commandline/2.0.0-rtm.25509.106",
"hashPath": "system.commandline.2.0.0-rtm.25509.106.nupkg.sha512"
}
}
}
@@ -0,0 +1,14 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"rollForward": "Major",
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More