PDFs erzeugen

This commit is contained in:
MarcWieland
2026-06-26 22:41:23 +02:00
parent 42a97a36b4
commit 4a69095f30
7 changed files with 635 additions and 0 deletions
@@ -0,0 +1,61 @@
@using System.Globalization
@using MudBlazor
<MudDialog>
<DialogContent>
<MudStack Spacing="3" Class="pa-2">
<MudSelect @bind-Value="_selectedYear" Label="Jahr" Variant="Variant.Outlined">
@foreach (var year in _years)
{
<MudSelectItem Value="@year">@year</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="_selectedMonth" Label="Monat" Variant="Variant.Outlined">
@foreach (var month in _months)
{
<MudSelectItem Value="@month.Value">@month.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Abbrechen</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">Herunterladen</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
private int _selectedYear = DateTime.Today.Year;
private int _selectedMonth = DateTime.Today.Month;
private List<int> _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<MonthOption> _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();
}
@@ -9,6 +9,7 @@
@inject IUserNotificationService UserNotificationService
@inject HttpClient Http
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@implements IDisposable
<PageTitle>Benutzerverwaltung Timetracker</PageTitle>
@@ -131,6 +132,12 @@ else
Size="Size.Small"
OnClick="@(() => OpenResetPasswordDialog(context))" />
</MudTooltip>
<MudTooltip Text="Stundenzettel (PDF) herunterladen">
<MudIconButton Icon="@Icons.Material.Filled.PictureAsPdf"
Color="Color.Info"
Size="Size.Small"
OnClick="@(() => DownloadUserPdf(context))" />
</MudTooltip>
@if (context.Username != "marc")
{
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
@@ -297,4 +304,19 @@ else
}
}
}
private async Task DownloadUserPdf(User user)
{
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
var dialog = await DialogService.ShowAsync<MonthSelectorDialog>($"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);
}
}
}
@@ -33,6 +33,10 @@ else
</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
<MudTooltip Text="Stundenzettel (PDF) herunterladen">
<MudIconButton Icon="@Icons.Material.Filled.PictureAsPdf"
Style="color: var(--mud-palette-primary-text)" OnClick="ExportMonthPdf" />
</MudTooltip>
<MudTooltip Text="Monat als Excel exportieren">
<MudIconButton Icon="@Icons.Material.Filled.Download"
Style="color: var(--mud-palette-primary-text)" OnClick="ExportMonthExcel" />
@@ -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;
+46
View File
@@ -83,6 +83,7 @@ builder.Services.AddSingleton<IUserNotificationService>(sp => sp.GetRequiredServ
// Register DB-backed services as the interfaces
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ITimetrackerService, TimetrackerService>();
builder.Services.AddScoped<timetracker.Server.Services.PdfGeneratorService>();
builder.Services.AddScoped<IHolidayService, HolidayService>();
// 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;
@@ -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<WorkDay> workDays,
List<VacationDay> vacationDays,
List<PublicHoliday> 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<WorkDay> workDays, List<VacationDay> vacationDays, List<PublicHoliday> 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<WorkDay> workDays, List<VacationDay> vacationDays, List<PublicHoliday> 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";
}
}
}
@@ -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"
@@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
<PackageReference Include="MudBlazor" Version="9.4.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageReference Include="QuestPDF" Version="2026.6.1" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />