From 4a69095f304a3f8d780c922884f7fef71bf62325 Mon Sep 17 00:00:00 2001 From: MarcWieland Date: Fri, 26 Jun 2026 22:41:23 +0200 Subject: [PATCH] PDFs erzeugen --- .../Components/MonthSelectorDialog.razor | 61 +++++ .../Components/Pages/AdminUsers.razor | 22 ++ .../Components/Pages/Month.razor | 9 + timetracker.Server/Program.cs | 46 ++++ .../Services/PdfGeneratorService.cs | 237 ++++++++++++++++ .../logs/timetracker-20260626.log | 259 ++++++++++++++++++ timetracker.Server/timetracker.Server.csproj | 1 + 7 files changed, 635 insertions(+) create mode 100644 timetracker.Client/Components/MonthSelectorDialog.razor create mode 100644 timetracker.Server/Services/PdfGeneratorService.cs diff --git a/timetracker.Client/Components/MonthSelectorDialog.razor b/timetracker.Client/Components/MonthSelectorDialog.razor new file mode 100644 index 0000000..086bcb3 --- /dev/null +++ b/timetracker.Client/Components/MonthSelectorDialog.razor @@ -0,0 +1,61 @@ +@using System.Globalization +@using MudBlazor + + + + + + @foreach (var year in _years) + { + @year + } + + + @foreach (var month in _months) + { + @month.Name + } + + + + + Abbrechen + Herunterladen + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + private int _selectedYear = DateTime.Today.Year; + private int _selectedMonth = DateTime.Today.Month; + + private List _years = Enumerable.Range(DateTime.Today.Year - 5, 6).OrderByDescending(y => y).ToList(); + + private class MonthOption + { + public int Value { get; set; } + public string Name { get; set; } = ""; + } + + private List _months = []; + + protected override void OnInitialized() + { + var deCulture = new CultureInfo("de-DE"); + _months = Enumerable.Range(1, 12).Select(m => new MonthOption + { + Value = m, + Name = deCulture.DateTimeFormat.GetMonthName(m) + }).ToList(); + } + + public class MonthSelectorResult + { + public int Year { get; set; } + public int Month { get; set; } + } + + void Submit() => MudDialog.Close(DialogResult.Ok(new MonthSelectorResult { Year = _selectedYear, Month = _selectedMonth })); + void Cancel() => MudDialog.Cancel(); +} diff --git a/timetracker.Client/Components/Pages/AdminUsers.razor b/timetracker.Client/Components/Pages/AdminUsers.razor index 01f4596..b00f3f2 100644 --- a/timetracker.Client/Components/Pages/AdminUsers.razor +++ b/timetracker.Client/Components/Pages/AdminUsers.razor @@ -9,6 +9,7 @@ @inject IUserNotificationService UserNotificationService @inject HttpClient Http @inject IDialogService DialogService +@inject NavigationManager NavigationManager @implements IDisposable Benutzerverwaltung – Timetracker @@ -131,6 +132,12 @@ else Size="Size.Small" OnClick="@(() => OpenResetPasswordDialog(context))" /> + + + @if (context.Username != "marc") { ($"Stundenzettel für {user.Username}", options); + var result = await dialog.Result; + + if (result != null && !result.Canceled && result.Data is MonthSelectorDialog.MonthSelectorResult data) + { + int year = data.Year; + int month = data.Month; + + NavigationManager.NavigateTo($"api/tracker/export/month/pdf?userId={user.Id}&year={year}&month={month}", forceLoad: true); + } + } } diff --git a/timetracker.Client/Components/Pages/Month.razor b/timetracker.Client/Components/Pages/Month.razor index ef64dd2..867777a 100644 --- a/timetracker.Client/Components/Pages/Month.razor +++ b/timetracker.Client/Components/Pages/Month.razor @@ -33,6 +33,10 @@ else + + + @@ -333,6 +337,11 @@ else NavigationManager.NavigateTo($"api/tracker/export/month?year={_year}&month={_month}", forceLoad: true); } + private void ExportMonthPdf() + { + NavigationManager.NavigateTo($"api/tracker/export/month/pdf?userId={_userId}&year={_year}&month={_month}", forceLoad: true); + } + private static string FormatTs(TimeSpan ts, bool sign = false) { var neg = ts < TimeSpan.Zero; diff --git a/timetracker.Server/Program.cs b/timetracker.Server/Program.cs index 9679de3..375366b 100644 --- a/timetracker.Server/Program.cs +++ b/timetracker.Server/Program.cs @@ -83,6 +83,7 @@ builder.Services.AddSingleton(sp => sp.GetRequiredServ // Register DB-backed services as the interfaces builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); // Add services to the container. @@ -738,6 +739,51 @@ trackerApi.MapGet("/export/month", async (ClaimsPrincipal claimsPrincipal, [From $"Monatsbericht_{monthName}_{year}.xlsx"); }); +trackerApi.MapGet("/export/month/pdf", async ( + ClaimsPrincipal claimsPrincipal, + [FromQuery] int userId, + [FromQuery] int year, + [FromQuery] int month, + ITimetrackerService trackerService, + IHolidayService holidayService, + TimetrackerDbContext db, + timetracker.Server.Services.PdfGeneratorService pdfService) => +{ + var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (!int.TryParse(idClaim, out var currentUserId)) return Results.Unauthorized(); + + if (userId != currentUserId && !claimsPrincipal.IsInRole("TenantAdmin") && !claimsPrincipal.IsInRole("SuperAdmin")) + { + return Results.Forbid(); + } + + if (month < 1 || month > 12) return Results.BadRequest("Ungültiger Monat."); + + var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId); + if (user == null) return Results.NotFound("Benutzer nicht gefunden."); + + Tenant? tenant = null; + if (user.TenantId.HasValue) + { + tenant = await db.Tenants.IgnoreQueryFilters().FirstOrDefaultAsync(t => t.Id == user.TenantId.Value); + } + + var deCulture = new System.Globalization.CultureInfo("de-DE"); + var monthName = deCulture.DateTimeFormat.GetMonthName(month); + + var workDays = await trackerService.GetMonthAsync(userId, year, month); + var vacations = await trackerService.GetVacationDaysAsync(userId, year); + var settings = await trackerService.GetSettingsAsync(userId); + var holidays = await holidayService.GetHolidaysAsync(year, settings.GermanState); + + var pdfBytes = pdfService.GenerateTimesheetPdf(user, tenant, settings, year, month, workDays, vacations, holidays); + + return Results.File( + pdfBytes, + "application/pdf", + $"Stundenzettel_{monthName}_{year}_{user.Username}.pdf"); +}); + trackerApi.MapGet("/export/year", async (ClaimsPrincipal claimsPrincipal, [FromQuery] int year, ITimetrackerService trackerService, TimetrackerDbContext db) => { var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; diff --git a/timetracker.Server/Services/PdfGeneratorService.cs b/timetracker.Server/Services/PdfGeneratorService.cs new file mode 100644 index 0000000..ce85275 --- /dev/null +++ b/timetracker.Server/Services/PdfGeneratorService.cs @@ -0,0 +1,237 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using System; +using System.Collections.Generic; +using System.Linq; +using timetracker.Shared; + +namespace timetracker.Server.Services +{ + public class PdfGeneratorService + { + public PdfGeneratorService() + { + // Configure QuestPDF community license + QuestPDF.Settings.License = LicenseType.Community; + } + + public byte[] GenerateTimesheetPdf( + User user, + Tenant? tenant, + AppSettings settings, + int year, + int month, + List workDays, + List vacationDays, + List holidays) + { + var document = Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(10).FontFamily(Fonts.Arial)); + + page.Header().Element(c => ComposeHeader(c, user, tenant, year, month)); + page.Content().Element(c => ComposeContent(c, user, settings, year, month, workDays, vacationDays, holidays)); + page.Footer().Element(ComposeFooter); + }); + }); + + return document.GeneratePdf(); + } + + private void ComposeHeader(IContainer container, User user, Tenant? tenant, int year, int month) + { + var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy", new System.Globalization.CultureInfo("de-DE")); + var companyName = tenant?.Name ?? "Hauptdomain"; + + container.Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text($"Stundenzettel - {monthName}").FontSize(20).SemiBold().FontColor(Colors.Blue.Darken2); + column.Item().Text($"Mitarbeiter: {user.Username}").FontSize(14).SemiBold(); + column.Item().PaddingTop(2).Text($"Firma: {companyName}").FontSize(11).FontColor(Colors.Grey.Darken2); + }); + }); + } + + private void ComposeContent(IContainer container, User user, AppSettings settings, int year, int month, List workDays, List vacationDays, List holidays) + { + TimeSpan totalNet = TimeSpan.Zero; + TimeSpan totalTarget = TimeSpan.Zero; + TimeSpan totalOvertime = TimeSpan.Zero; + + container.PaddingVertical(1, Unit.Centimetre).Column(column => + { + column.Item().Element(c => ComposeTable(c, settings, year, month, workDays, vacationDays, holidays, out totalNet, out totalTarget, out totalOvertime)); + + column.Item().PaddingTop(25).Element(c => ComposeSummary(c, totalNet, totalTarget, totalOvertime)); + + column.Item().PaddingTop(50).Element(ComposeSignatures); + }); + } + + private void ComposeTable(IContainer container, AppSettings settings, int year, int month, List workDays, List vacationDays, List holidays, out TimeSpan totalNetOut, out TimeSpan totalTargetOut, out TimeSpan totalOvertimeOut) + { + var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name); + var vacationSet = vacationDays.Select(v => v.Date).ToHashSet(); + var daysInMonth = DateTime.DaysInMonth(year, month); + + var totalNet = TimeSpan.Zero; + var totalTarget = TimeSpan.Zero; + var totalOvertime = TimeSpan.Zero; + + container.Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(70); // Datum + columns.ConstantColumn(50); // Start + columns.ConstantColumn(50); // Ende + columns.ConstantColumn(60); // Pause + columns.ConstantColumn(60); // Netto + columns.ConstantColumn(60); // Gleitzeit + columns.RelativeColumn(); // Info + }); + + table.Header(header => + { + header.Cell().Element(CellStyle).Text("Datum"); + header.Cell().Element(CellStyle).AlignRight().Text("Start"); + header.Cell().Element(CellStyle).AlignRight().Text("Ende"); + header.Cell().Element(CellStyle).AlignRight().Text("Pause"); + header.Cell().Element(CellStyle).AlignRight().Text("Netto"); + header.Cell().Element(CellStyle).AlignRight().Text("Gleitzeit"); + header.Cell().Element(CellStyle).Text("Info/Status"); + + static IContainer CellStyle(IContainer container) + { + return container.DefaultTextStyle(x => x.SemiBold()).PaddingVertical(5).BorderBottom(1).BorderColor(Colors.Black); + } + }); + + for (int day = 1; day <= daysInMonth; day++) + { + var date = new DateOnly(year, month, day); + var wd = workDays.FirstOrDefault(w => w.Date == date); + var isWorkDay = settings.IsWorkDay(date.DayOfWeek); + var isVacation = vacationSet.Contains(date); + var holidayName = holidayMap.GetValueOrDefault(date); + + TimeSpan? net = null; + TimeSpan breakSum = TimeSpan.Zero; + + if (wd?.StartTime != null && wd.EndTime != null) + { + var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan(); + breakSum = 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())); + var minMinutes = gross.TotalHours >= 9.0 ? 45 : 30; + var minBreak = TimeSpan.FromMinutes(Math.Max(minMinutes, settings.MinimumBreakMinutes)); + var finalBreak = breakSum > minBreak ? breakSum : minBreak; + net = gross - finalBreak; + if (net < TimeSpan.Zero) net = null; + } + + var target = isWorkDay ? TimeSpan.FromHours(settings.DailyTargetHours) : TimeSpan.Zero; + var overtime = net.HasValue ? net.Value - target : TimeSpan.Zero; + + if (net.HasValue) totalNet += net.Value; + if (isWorkDay) totalTarget += target; + if (isWorkDay && net.HasValue) totalOvertime += overtime; + + string status = ""; + if (!string.IsNullOrEmpty(holidayName)) status = holidayName; + else if (isVacation) status = "Urlaub"; + else if (!isWorkDay) status = "Wochenende"; + + var bgColor = Colors.White; + if (!isWorkDay || !string.IsNullOrEmpty(holidayName)) bgColor = Colors.Grey.Lighten4; + if (isVacation) bgColor = Colors.Orange.Lighten4; + + table.Cell().Element(c => CellStyle(c, bgColor)).Text(date.ToString("dd.MM.yyyy")); + table.Cell().Element(c => CellStyle(c, bgColor)).AlignRight().Text(wd?.StartTime?.ToString(@"HH\:mm") ?? "-"); + table.Cell().Element(c => CellStyle(c, bgColor)).AlignRight().Text(wd?.EndTime?.ToString(@"HH\:mm") ?? "-"); + table.Cell().Element(c => CellStyle(c, bgColor)).AlignRight().Text(net.HasValue ? FormatTs(breakSum) : "-"); + table.Cell().Element(c => CellStyle(c, bgColor)).AlignRight().Text(net.HasValue ? FormatTs(net.Value) : "-"); + + var overtimeColor = overtime < TimeSpan.Zero ? Colors.Red.Darken2 : Colors.Green.Darken2; + table.Cell().Element(c => CellStyle(c, bgColor)).AlignRight().Text(net.HasValue ? FormatTs(overtime, true) : "-").FontColor(net.HasValue && overtime != TimeSpan.Zero ? overtimeColor : Colors.Black); + + table.Cell().Element(c => CellStyle(c, bgColor)).Text(status).FontColor(Colors.Grey.Darken2); + + static IContainer CellStyle(IContainer container, string bgColor) + { + return container.BorderBottom(1).BorderColor(Colors.Grey.Lighten3).Background(bgColor).PaddingVertical(3).PaddingHorizontal(2); + } + } + }); + + totalNetOut = totalNet; + totalTargetOut = totalTarget; + totalOvertimeOut = totalOvertime; + } + + private void ComposeSummary(IContainer container, TimeSpan totalNet, TimeSpan totalTarget, TimeSpan totalOvertime) + { + container.Background(Colors.Grey.Lighten4).Padding(10).Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text("Zusammenfassung").SemiBold(); + column.Item().Text($"Soll-Stunden: {FormatTs(totalTarget)}"); + column.Item().Text($"Ist-Stunden: {FormatTs(totalNet)}"); + + var overtimeColor = totalOvertime < TimeSpan.Zero ? Colors.Red.Darken2 : Colors.Green.Darken2; + column.Item().Text($"Saldo: {FormatTs(totalOvertime, true)}").SemiBold().FontColor(totalOvertime != TimeSpan.Zero ? overtimeColor : Colors.Black); + }); + }); + } + + private void ComposeSignatures(IContainer container) + { + container.Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().LineHorizontal(1); + column.Item().PaddingTop(5).Text("Datum, Unterschrift Mitarbeiter").FontSize(10); + }); + row.ConstantItem(50); // space + row.RelativeItem().Column(column => + { + column.Item().LineHorizontal(1); + column.Item().PaddingTop(5).Text("Datum, Unterschrift Arbeitgeber").FontSize(10); + }); + }); + } + + private void ComposeFooter(IContainer container) + { + container.AlignCenter().Text(x => + { + x.Span("Seite "); + x.CurrentPageNumber(); + x.Span(" von "); + x.TotalPages(); + }); + } + + private static string FormatTs(TimeSpan ts, bool sign = false) + { + var neg = ts < TimeSpan.Zero; + var abs = neg ? ts.Negate() : ts; + var h = (int)abs.TotalHours; + var m = abs.Minutes; + if (!sign) return $"{h}:{m:D2} h"; + return neg ? $"-{h}:{m:D2} h" : $"+{h}:{m:D2} h"; + } + } +} diff --git a/timetracker.Server/logs/timetracker-20260626.log b/timetracker.Server/logs/timetracker-20260626.log index 00f312e..d50abe8 100644 --- a/timetracker.Server/logs/timetracker-20260626.log +++ b/timetracker.Server/logs/timetracker-20260626.log @@ -1579,3 +1579,262 @@ SELECT "a"."Id", "a"."DailyTargetHours", "a"."FlexTimeStartDate", "a"."FlexTimeS FROM "AppSettings" AS "a" WHERE "a"."TenantId" IS NULL AND "a"."UserId" = @userId LIMIT 1 +2026-06-26 22:39:52.521 +02:00 [INF] File logging active. Writing logs to /Users/marcwieland/Uni/Master/Projects/Timetracker/timetracker/timetracker.Server/logs +2026-06-26 22:39:52.930 +02:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information. +2026-06-26 22:39:52.996 +02:00 [INF] Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations-lock for more information if this takes too long. +2026-06-26 22:39:53.008 +02:00 [INF] Executed DbCommand (6ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = '__EFMigrationsLock' AND "type" = 'table'; +2026-06-26 22:39:53.015 +02:00 [INF] Executed DbCommand (4ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +INSERT OR IGNORE INTO "__EFMigrationsLock"("Id", "Timestamp") VALUES(1, '2026-06-26 20:39:53.010428+00:00'); +SELECT changes(); +2026-06-26 22:39:53.069 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY, + "ProductVersion" TEXT NOT NULL +); +2026-06-26 22:39:53.075 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = '__EFMigrationsHistory' AND "type" = 'table'; +2026-06-26 22:39:53.077 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT "MigrationId", "ProductVersion" +FROM "__EFMigrationsHistory" +ORDER BY "MigrationId"; +2026-06-26 22:39:53.081 +02:00 [INF] No migrations were applied. The database is already up to date. +2026-06-26 22:39:53.081 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +DELETE FROM "__EFMigrationsLock"; +2026-06-26 22:39:53.210 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."IsTenantAdmin", "u"."PasswordHash", "u"."PasswordSalt", "u"."TenantId", "u"."Username" +FROM "Users" AS "u" +WHERE "u"."TenantId" IS NULL AND "u"."Username" = 'marc' +LIMIT 1 +2026-06-26 22:39:53.618 +02:00 [INF] Executed DbCommand (1ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "a"."Id", "a"."DailyTargetHours", "a"."FlexTimeStartDate", "a"."FlexTimeStartingBalanceHours", "a"."GermanState", "a"."MinimumBreakMinutes", "a"."TenantId", "a"."UserId", "a"."VacationDaysPerYear", "a"."WorkFriday", "a"."WorkMonday", "a"."WorkSaturday", "a"."WorkSunday", "a"."WorkThursday", "a"."WorkTuesday", "a"."WorkWednesday" +FROM "AppSettings" AS "a" +WHERE "a"."TenantId" IS NULL AND "a"."UserId" = @userId +LIMIT 1 +2026-06-26 22:39:53.657 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "a"."Id", "a"."DailyTargetHours", "a"."FlexTimeStartDate", "a"."FlexTimeStartingBalanceHours", "a"."GermanState", "a"."MinimumBreakMinutes", "a"."TenantId", "a"."UserId", "a"."VacationDaysPerYear", "a"."WorkFriday", "a"."WorkMonday", "a"."WorkSaturday", "a"."WorkSunday", "a"."WorkThursday", "a"."WorkTuesday", "a"."WorkWednesday" +FROM "AppSettings" AS "a" +WHERE "a"."TenantId" IS NULL AND "a"."UserId" = @userId +LIMIT 1 +2026-06-26 22:39:53.668 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE CAST(strftime('%Y', "p"."Date") AS INTEGER) = @year +ORDER BY "p"."Date" +2026-06-26 22:39:53.705 +02:00 [INF] Executed DbCommand (1ms) [Parameters=[@userId='?' (DbType = Int32), @monday='?' (DbType = Date), @AddDays='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @userId AND "w"."Date" >= @monday AND "w"."Date" < @AddDays +ORDER BY "w"."Date", "w"."Id" +2026-06-26 22:39:53.709 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Id", "v"."Date", "v"."Note", "v"."TenantId", "v"."UserId" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @userId AND CAST(strftime('%Y', "v"."Date") AS INTEGER) = @year +ORDER BY "v"."Date" +2026-06-26 22:39:53.720 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Date" +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId +ORDER BY "w"."Date" +LIMIT 1 +2026-06-26 22:39:53.727 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId AND "w"."Date" >= @startDate AND "w"."Date" <= @endDate +ORDER BY "w"."Id" +2026-06-26 22:39:53.737 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE "p"."Date" >= @startDate AND "p"."Date" <= @endDate +2026-06-26 22:39:53.742 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Date" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @_8__locals1_userId AND "v"."Date" >= @startDate AND "v"."Date" <= @endDate +2026-06-26 22:39:54.526 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "a"."Id", "a"."DailyTargetHours", "a"."FlexTimeStartDate", "a"."FlexTimeStartingBalanceHours", "a"."GermanState", "a"."MinimumBreakMinutes", "a"."TenantId", "a"."UserId", "a"."VacationDaysPerYear", "a"."WorkFriday", "a"."WorkMonday", "a"."WorkSaturday", "a"."WorkSunday", "a"."WorkThursday", "a"."WorkTuesday", "a"."WorkWednesday" +FROM "AppSettings" AS "a" +WHERE "a"."TenantId" IS NULL AND "a"."UserId" = @userId +LIMIT 1 +2026-06-26 22:39:57.429 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Id", "v"."Date", "v"."Note", "v"."TenantId", "v"."UserId" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @userId AND CAST(strftime('%Y', "v"."Date") AS INTEGER) = @year +ORDER BY "v"."Date" +2026-06-26 22:39:57.430 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE CAST(strftime('%Y', "p"."Date") AS INTEGER) = @year +ORDER BY "p"."Date" +2026-06-26 22:40:02.558 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @monday='?' (DbType = Date), @AddDays='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @userId AND "w"."Date" >= @monday AND "w"."Date" < @AddDays +ORDER BY "w"."Date", "w"."Id" +2026-06-26 22:40:02.604 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Date" +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId +ORDER BY "w"."Date" +LIMIT 1 +2026-06-26 22:40:02.604 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId AND "w"."Date" >= @startDate AND "w"."Date" <= @endDate +ORDER BY "w"."Id" +2026-06-26 22:40:02.605 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE "p"."Date" >= @startDate AND "p"."Date" <= @endDate +2026-06-26 22:40:02.605 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Date" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @_8__locals1_userId AND "v"."Date" >= @startDate AND "v"."Date" <= @endDate +2026-06-26 22:40:08.071 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."IsTenantAdmin", "u"."PasswordHash", "u"."PasswordSalt", "u"."TenantId", "u"."Username" +FROM "Users" AS "u" +ORDER BY "u"."Username" +2026-06-26 22:40:08.096 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT "t"."Id", "t"."CreatedAt", "t"."IsApproved", "t"."Name", "t"."PrimaryColor", "t"."SecondaryColor", "t"."Subdomain" +FROM "Tenants" AS "t" +ORDER BY "t"."CreatedAt" DESC +2026-06-26 22:40:08.109 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.112 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" = @t_Id +2026-06-26 22:40:08.116 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."Username", "u"."IsTenantAdmin" +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.117 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.117 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" = @t_Id +2026-06-26 22:40:08.117 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."Username", "u"."IsTenantAdmin" +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.118 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.118 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" = @t_Id +2026-06-26 22:40:08.118 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."Username", "u"."IsTenantAdmin" +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.119 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:08.119 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT COUNT(*) +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" = @t_Id +2026-06-26 22:40:08.119 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@t_Id='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."Username", "u"."IsTenantAdmin" +FROM "Users" AS "u" +WHERE "u"."TenantId" = @t_Id +2026-06-26 22:40:14.487 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."IsTenantAdmin", "u"."PasswordHash", "u"."PasswordSalt", "u"."TenantId", "u"."Username" +FROM "Users" AS "u" +WHERE "u"."Id" = @userId +LIMIT 1 +2026-06-26 22:40:14.499 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @from='?' (DbType = Date), @to='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId AND "w"."Date" >= @from AND "w"."Date" < @to +ORDER BY "w"."Date", "w"."Id" +2026-06-26 22:40:14.501 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Id", "v"."Date", "v"."Note", "v"."TenantId", "v"."UserId" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @userId AND CAST(strftime('%Y', "v"."Date") AS INTEGER) = @year +ORDER BY "v"."Date" +2026-06-26 22:40:14.502 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "a"."Id", "a"."DailyTargetHours", "a"."FlexTimeStartDate", "a"."FlexTimeStartingBalanceHours", "a"."GermanState", "a"."MinimumBreakMinutes", "a"."TenantId", "a"."UserId", "a"."VacationDaysPerYear", "a"."WorkFriday", "a"."WorkMonday", "a"."WorkSaturday", "a"."WorkSunday", "a"."WorkThursday", "a"."WorkTuesday", "a"."WorkWednesday" +FROM "AppSettings" AS "a" +WHERE "a"."TenantId" IS NULL AND "a"."UserId" = @userId +LIMIT 1 +2026-06-26 22:40:14.502 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE CAST(strftime('%Y', "p"."Date") AS INTEGER) = @year +ORDER BY "p"."Date" +2026-06-26 22:40:40.569 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @monday='?' (DbType = Date), @AddDays='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @userId AND "w"."Date" >= @monday AND "w"."Date" < @AddDays +ORDER BY "w"."Date", "w"."Id" +2026-06-26 22:40:40.585 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Date" +FROM "WorkDays" AS "w" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId +ORDER BY "w"."Date" +LIMIT 1 +2026-06-26 22:40:40.586 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId AND "w"."Date" >= @startDate AND "w"."Date" <= @endDate +ORDER BY "w"."Id" +2026-06-26 22:40:40.587 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE "p"."Date" >= @startDate AND "p"."Date" <= @endDate +2026-06-26 22:40:40.587 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @startDate='?' (DbType = Date), @endDate='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Date" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @_8__locals1_userId AND "v"."Date" >= @startDate AND "v"."Date" <= @endDate +2026-06-26 22:41:04.626 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Id", "v"."Date", "v"."Note", "v"."TenantId", "v"."UserId" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @userId AND CAST(strftime('%Y', "v"."Date") AS INTEGER) = @year +ORDER BY "v"."Date" +2026-06-26 22:41:04.627 +02:00 [INF] Executed DbCommand (1ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @from='?' (DbType = Date), @to='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId AND "w"."Date" >= @from AND "w"."Date" < @to +ORDER BY "w"."Date", "w"."Id" +2026-06-26 22:41:08.848 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "u"."Id", "u"."IsTenantAdmin", "u"."PasswordHash", "u"."PasswordSalt", "u"."TenantId", "u"."Username" +FROM "Users" AS "u" +WHERE "u"."Id" = @userId +LIMIT 1 +2026-06-26 22:41:08.849 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@_8__locals1_userId='?' (DbType = Int32), @from='?' (DbType = Date), @to='?' (DbType = Date)], CommandType='"Text"', CommandTimeout='30'] +SELECT "w"."Id", "w"."Date", "w"."EndTime", "w"."StartTime", "w"."TenantId", "w"."UserId", "b"."Id", "b"."EndTime", "b"."StartTime", "b"."WorkDayId" +FROM "WorkDays" AS "w" +LEFT JOIN "BreakEntries" AS "b" ON "w"."Id" = "b"."WorkDayId" +WHERE "w"."TenantId" IS NULL AND "w"."UserId" = @_8__locals1_userId AND "w"."Date" >= @from AND "w"."Date" < @to +ORDER BY "w"."Date", "w"."Id" +2026-06-26 22:41:08.849 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32), @year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "v"."Id", "v"."Date", "v"."Note", "v"."TenantId", "v"."UserId" +FROM "VacationDays" AS "v" +WHERE "v"."TenantId" IS NULL AND "v"."UserId" = @userId AND CAST(strftime('%Y', "v"."Date") AS INTEGER) = @year +ORDER BY "v"."Date" +2026-06-26 22:41:08.849 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@userId='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "a"."Id", "a"."DailyTargetHours", "a"."FlexTimeStartDate", "a"."FlexTimeStartingBalanceHours", "a"."GermanState", "a"."MinimumBreakMinutes", "a"."TenantId", "a"."UserId", "a"."VacationDaysPerYear", "a"."WorkFriday", "a"."WorkMonday", "a"."WorkSaturday", "a"."WorkSunday", "a"."WorkThursday", "a"."WorkTuesday", "a"."WorkWednesday" +FROM "AppSettings" AS "a" +WHERE "a"."TenantId" IS NULL AND "a"."UserId" = @userId +LIMIT 1 +2026-06-26 22:41:08.850 +02:00 [INF] Executed DbCommand (0ms) [Parameters=[@year='?' (DbType = Int32)], CommandType='"Text"', CommandTimeout='30'] +SELECT "p"."Id", "p"."Counties", "p"."Date", "p"."Name" +FROM "PublicHolidays" AS "p" +WHERE CAST(strftime('%Y', "p"."Date") AS INTEGER) = @year +ORDER BY "p"."Date" diff --git a/timetracker.Server/timetracker.Server.csproj b/timetracker.Server/timetracker.Server.csproj index 9de85f2..04da38e 100644 --- a/timetracker.Server/timetracker.Server.csproj +++ b/timetracker.Server/timetracker.Server.csproj @@ -14,6 +14,7 @@ +