Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a89779a00c | |||
| febc875834 | |||
| 2305e4b728 | |||
| b6dc66744f | |||
| a01f80bb14 | |||
| 9980d9217a | |||
| 03b025c09c | |||
| 0719d41fe1 | |||
| b12e94128c |
@@ -28,9 +28,14 @@ services:
|
||||
- DB_PROVIDER=PostgreSQL
|
||||
- ConnectionStrings__DefaultConnection=Host=db;Database=timetracker;Username=timetracker_user;Password=SecretPassword123;
|
||||
- EnableHttpsRedirect=false
|
||||
- TenantSettings__BaseDomain=timetracker.marc-wieland.de
|
||||
volumes:
|
||||
- dpkeys:/home/app/.aspnet/DataProtection-Keys
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
name: timetracker_pgdata
|
||||
dpkeys:
|
||||
name: timetracker_dpkeys
|
||||
@@ -65,6 +65,9 @@
|
||||
new("Neu", "Userprofil angelegt"),
|
||||
new("Upgrade", "Früheste Gehenszeit wird angezeigt"),
|
||||
new("Upgrade", "Pausen werden dynamisch berechnet"),
|
||||
new("Neu", "Excel-Export"),
|
||||
new("Neu", "Multi-Tenant Integration"),
|
||||
new("Upgrade", "Verbesserte Single-User Ansicht")
|
||||
], Highlighted: true),
|
||||
new("1.4", "08.06.2026", true,
|
||||
[
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@inject ISnackbar Snackbar
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageTitle>KW @_kw – Wochenübersicht – Timetracker</PageTitle>
|
||||
|
||||
@@ -34,6 +35,10 @@ else
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
||||
<MudTooltip Text="Woche als Excel exportieren">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Download"
|
||||
Style="color: var(--mud-palette-primary-text)" OnClick="ExportWeekExcel" />
|
||||
</MudTooltip>
|
||||
@if (!IsCurrentWeek)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
||||
@@ -491,6 +496,10 @@ else
|
||||
private async Task PrevWeek() { _monday = _monday.AddDays(-7); await LoadWeek(); }
|
||||
private async Task NextWeek() { _monday = _monday.AddDays(7); await LoadWeek(); }
|
||||
private async Task GoToCurrentWeek() { _monday = GetMonday(DateOnly.FromDateTime(DateTime.Today)); await LoadWeek(); }
|
||||
private void ExportWeekExcel()
|
||||
{
|
||||
NavigationManager.NavigateTo($"api/tracker/export/week?monday={_monday:yyyy-MM-dd}", forceLoad: true);
|
||||
}
|
||||
|
||||
private static DateOnly GetMonday(DateOnly date)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject IHolidayService HolidayService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker</PageTitle>
|
||||
|
||||
@@ -32,6 +33,10 @@ else
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
||||
<MudTooltip Text="Monat als Excel exportieren">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Download"
|
||||
Style="color: var(--mud-palette-primary-text)" OnClick="ExportMonthExcel" />
|
||||
</MudTooltip>
|
||||
@if (!IsCurrentMonth)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
||||
@@ -268,6 +273,11 @@ else
|
||||
await LoadMonth();
|
||||
}
|
||||
|
||||
private void ExportMonthExcel()
|
||||
{
|
||||
NavigationManager.NavigateTo($"api/tracker/export/month?year={_year}&month={_month}", forceLoad: true);
|
||||
}
|
||||
|
||||
private static string FormatTs(TimeSpan ts, bool sign = false)
|
||||
{
|
||||
var neg = ts < TimeSpan.Zero;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@attribute [Authorize]
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@using System.Security.Claims
|
||||
@using timetracker.Shared
|
||||
|
||||
@@ -211,6 +212,75 @@ else
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
@* ── Excel Export ── *@
|
||||
<MudItem xs="12">
|
||||
<MudCard Elevation="3" Class="rounded-xl">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6" Style="font-weight:700; color: var(--mud-palette-text-primary)">Excel-Datenexport</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Lade deine erfassten Arbeitszeiten in verschiedenen Zeiträumen als formatierte Excel-Datei herunter.</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid Spacing="3">
|
||||
@* Wochenexport *@
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.subtitle2" Style="font-weight:700">Wochenexport</MudText>
|
||||
<MudDatePicker Label="Woche wählen" @bind-Date="_exportWeekDate" PickerVariant="PickerVariant.Dialog" Variant="Variant.Outlined" Color="Color.Primary" />
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Download" Variant="Variant.Filled" Color="Color.Primary" OnClick="ExportWeek" FullWidth="true">
|
||||
Woche exportieren
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
@* Monatsexport *@
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.subtitle2" Style="font-weight:700">Monatsexport</MudText>
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudSelect T="int" Label="Monat" @bind-Value="_exportMonth" Variant="Variant.Outlined">
|
||||
@for (int m = 1; m <= 12; m++)
|
||||
{
|
||||
var monthVal = m;
|
||||
<MudSelectItem Value="@monthVal">@_deCulture.DateTimeFormat.GetMonthName(monthVal)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect T="int" Label="Jahr" @bind-Value="_exportMonthYear" Variant="Variant.Outlined">
|
||||
@for (int y = DateTime.Today.Year - 5; y <= DateTime.Today.Year + 1; y++)
|
||||
{
|
||||
var yearVal = y;
|
||||
<MudSelectItem Value="@yearVal">@yearVal</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Download" Variant="Variant.Filled" Color="Color.Primary" OnClick="ExportMonth" FullWidth="true">
|
||||
Monat exportieren
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
@* Jahresexport *@
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.subtitle2" Style="font-weight:700">Jahresexport</MudText>
|
||||
<MudSelect T="int" Label="Jahr" @bind-Value="_exportYear" Variant="Variant.Outlined">
|
||||
@for (int y = DateTime.Today.Year - 5; y <= DateTime.Today.Year + 1; y++)
|
||||
{
|
||||
var yearVal = y;
|
||||
<MudSelectItem Value="@yearVal">@yearVal</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Download" Variant="Variant.Filled" Color="Color.Primary" OnClick="ExportYear" FullWidth="true">
|
||||
Jahr exportieren
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
@* ── Diagnostics Panel ── *@
|
||||
<MudItem xs="12">
|
||||
<MudExpansionPanels>
|
||||
@@ -264,6 +334,12 @@ else
|
||||
private double _weekOvertimeHours;
|
||||
private double _maxHoursValue = 10.0;
|
||||
|
||||
// Excel Export variables
|
||||
private DateTime? _exportWeekDate = DateTime.Today;
|
||||
private int _exportMonth = DateTime.Today.Month;
|
||||
private int _exportMonthYear = DateTime.Today.Year;
|
||||
private int _exportYear = DateTime.Today.Year;
|
||||
|
||||
// Debug variables
|
||||
private int _rawDbDaysCount = 0;
|
||||
private Dictionary<DateOnly, string> _dbMatchStatus = new();
|
||||
@@ -373,6 +449,25 @@ else
|
||||
return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}";
|
||||
}
|
||||
|
||||
private void ExportWeek()
|
||||
{
|
||||
if (_exportWeekDate.HasValue)
|
||||
{
|
||||
var date = DateOnly.FromDateTime(_exportWeekDate.Value);
|
||||
NavigationManager.NavigateTo($"api/tracker/export/week?monday={date:yyyy-MM-dd}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExportMonth()
|
||||
{
|
||||
NavigationManager.NavigateTo($"api/tracker/export/month?year={_exportMonthYear}&month={_exportMonth}", forceLoad: true);
|
||||
}
|
||||
|
||||
private void ExportYear()
|
||||
{
|
||||
NavigationManager.NavigateTo($"api/tracker/export/year?year={_exportYear}", forceLoad: true);
|
||||
}
|
||||
|
||||
private sealed class DayStat
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
using ClosedXML.Excel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using timetracker.Shared;
|
||||
|
||||
namespace timetracker.Data;
|
||||
|
||||
public static class ExcelExporter
|
||||
{
|
||||
public static async Task<byte[]> GenerateExcelExportAsync(
|
||||
int userId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
string title,
|
||||
ITimetrackerService trackerService,
|
||||
TimetrackerDbContext db)
|
||||
{
|
||||
var settings = await trackerService.GetSettingsAsync(userId);
|
||||
|
||||
var workDays = await db.WorkDays
|
||||
.Include(w => w.Breaks)
|
||||
.Where(w => w.UserId == userId && w.Date >= startDate && w.Date <= endDate)
|
||||
.ToDictionaryAsync(w => w.Date);
|
||||
|
||||
var holidaysList = await db.PublicHolidays
|
||||
.Where(h => h.Date >= startDate && h.Date <= endDate)
|
||||
.ToListAsync();
|
||||
|
||||
var state = settings.GermanState;
|
||||
var holidayMap = holidaysList
|
||||
.Where(h => string.IsNullOrEmpty(h.Counties) || (!string.IsNullOrEmpty(state) && h.Counties.Split(',').Contains(state)))
|
||||
.ToDictionary(h => h.Date, h => h.Name);
|
||||
|
||||
var vacationMap = await db.VacationDays
|
||||
.Where(v => v.UserId == userId && v.Date >= startDate && v.Date <= endDate)
|
||||
.ToDictionaryAsync(v => v.Date, v => v.Note ?? "Urlaub");
|
||||
|
||||
var user = await db.Users.FindAsync(userId);
|
||||
var userName = user != null ? user.Username : "Mitarbeiter";
|
||||
|
||||
var rows = new List<ExportRow>();
|
||||
|
||||
for (var date = startDate; date <= endDate; date = date.AddDays(1))
|
||||
{
|
||||
bool isWorkDay = settings.IsWorkDay(date.DayOfWeek);
|
||||
bool isHoliday = holidayMap.ContainsKey(date);
|
||||
bool isVacation = vacationMap.ContainsKey(date);
|
||||
|
||||
double targetHours = (isWorkDay && !isHoliday && !isVacation) ? settings.DailyTargetHours : 0.0;
|
||||
|
||||
string status = "Wochenende";
|
||||
if (isHoliday) status = "Feiertag";
|
||||
else if (isVacation) status = "Urlaub";
|
||||
else if (isWorkDay) status = "Arbeitstag";
|
||||
|
||||
TimeSpan? startTime = null;
|
||||
TimeSpan? endTime = null;
|
||||
TimeSpan breakTime = TimeSpan.Zero;
|
||||
double actualHours = 0.0;
|
||||
|
||||
string note = "";
|
||||
if (isHoliday) note = holidayMap[date];
|
||||
else if (isVacation) note = vacationMap[date];
|
||||
|
||||
if (workDays.TryGetValue(date, out var wd))
|
||||
{
|
||||
if (wd.StartTime.HasValue) startTime = wd.StartTime.Value.ToTimeSpan();
|
||||
if (wd.EndTime.HasValue) endTime = wd.EndTime.Value.ToTimeSpan();
|
||||
|
||||
if (startTime.HasValue && endTime.HasValue)
|
||||
{
|
||||
var gross = endTime.Value - startTime.Value;
|
||||
if (gross > TimeSpan.Zero)
|
||||
{
|
||||
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()));
|
||||
|
||||
var minMinutes = gross.TotalHours >= 9.0 ? 45 : 30;
|
||||
var minBreak = TimeSpan.FromMinutes(Math.Max(minMinutes, settings.MinimumBreakMinutes));
|
||||
var finalBreak = breakTotal > minBreak ? breakTotal : minBreak;
|
||||
|
||||
breakTime = finalBreak;
|
||||
actualHours = (gross - finalBreak).TotalHours;
|
||||
if (actualHours < 0) actualHours = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.Add(new ExportRow
|
||||
{
|
||||
Date = date,
|
||||
Status = status,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
BreakTime = breakTime,
|
||||
ActualHours = actualHours,
|
||||
TargetHours = targetHours,
|
||||
DiffHours = actualHours - targetHours,
|
||||
Note = note
|
||||
});
|
||||
}
|
||||
|
||||
return ExportToExcel(title, userName, startDate, endDate, rows);
|
||||
}
|
||||
|
||||
public static byte[] ExportToExcel(
|
||||
string title,
|
||||
string userName,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
List<ExportRow> rows)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Arbeitszeiten");
|
||||
|
||||
// Gitterlinien explizit einblenden
|
||||
worksheet.ShowGridLines = true;
|
||||
|
||||
// Title Block
|
||||
worksheet.Cell("A1").Value = title;
|
||||
worksheet.Cell("A1").Style.Font.Bold = true;
|
||||
worksheet.Cell("A1").Style.Font.FontSize = 16;
|
||||
worksheet.Cell("A1").Style.Font.FontColor = XLColor.FromHtml("#0F172A"); // Slate 900
|
||||
|
||||
// Info Block
|
||||
worksheet.Cell("A2").Value = $"Mitarbeiter: {userName}";
|
||||
worksheet.Cell("A2").Style.Font.FontSize = 11;
|
||||
worksheet.Cell("A2").Style.Font.Italic = true;
|
||||
|
||||
worksheet.Cell("A3").Value = $"Zeitraum: {startDate:dd.MM.yyyy} bis {endDate:dd.MM.yyyy}";
|
||||
worksheet.Cell("A3").Style.Font.FontSize = 11;
|
||||
worksheet.Cell("A3").Style.Font.Italic = true;
|
||||
|
||||
// Tabellenüberschriften (Zeile 5)
|
||||
string[] headers = {
|
||||
"Datum", "Wochentag", "Status", "Beginn", "Ende",
|
||||
"Pause", "Netto (Ist)", "Soll-Stunden", "Differenz (Gleitzeit)", "Notiz / Feiertag / Urlaub"
|
||||
};
|
||||
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
var cell = worksheet.Cell(5, i + 1);
|
||||
cell.Value = headers[i];
|
||||
cell.Style.Font.Bold = true;
|
||||
cell.Style.Font.FontColor = XLColor.White;
|
||||
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#0EA5E9"); // Primary color (Sky 500)
|
||||
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||
}
|
||||
|
||||
// Datenzeilen
|
||||
int currentRow = 6;
|
||||
var deCulture = new CultureInfo("de-DE");
|
||||
foreach (var r in rows)
|
||||
{
|
||||
worksheet.Cell(currentRow, 1).Value = r.Date.ToString("dd.MM.yyyy");
|
||||
worksheet.Cell(currentRow, 2).Value = r.Date.ToString("dddd", deCulture);
|
||||
worksheet.Cell(currentRow, 3).Value = r.Status;
|
||||
|
||||
if (r.StartTime.HasValue)
|
||||
{
|
||||
worksheet.Cell(currentRow, 4).Value = r.StartTime.Value.ToString(@"hh\:mm");
|
||||
}
|
||||
else
|
||||
{
|
||||
worksheet.Cell(currentRow, 4).Value = "—";
|
||||
}
|
||||
|
||||
if (r.EndTime.HasValue)
|
||||
{
|
||||
worksheet.Cell(currentRow, 5).Value = r.EndTime.Value.ToString(@"hh\:mm");
|
||||
}
|
||||
else
|
||||
{
|
||||
worksheet.Cell(currentRow, 5).Value = "—";
|
||||
}
|
||||
|
||||
worksheet.Cell(currentRow, 6).Value = r.BreakTime.TotalMinutes > 0 ? r.BreakTime.ToString(@"hh\:mm") : "—";
|
||||
|
||||
worksheet.Cell(currentRow, 7).Value = r.ActualHours;
|
||||
worksheet.Cell(currentRow, 7).Style.NumberFormat.Format = "0.00";
|
||||
|
||||
worksheet.Cell(currentRow, 8).Value = r.TargetHours;
|
||||
worksheet.Cell(currentRow, 8).Style.NumberFormat.Format = "0.00";
|
||||
|
||||
worksheet.Cell(currentRow, 9).Value = r.DiffHours;
|
||||
worksheet.Cell(currentRow, 9).Style.NumberFormat.Format = "+0.00;-0.00;0.00";
|
||||
|
||||
if (r.DiffHours > 0)
|
||||
{
|
||||
worksheet.Cell(currentRow, 9).Style.Font.FontColor = XLColor.FromHtml("#16A34A"); // Green 600
|
||||
worksheet.Cell(currentRow, 9).Style.Font.Bold = true;
|
||||
}
|
||||
else if (r.DiffHours < 0)
|
||||
{
|
||||
worksheet.Cell(currentRow, 9).Style.Font.FontColor = XLColor.FromHtml("#DC2626"); // Red 600
|
||||
worksheet.Cell(currentRow, 9).Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
worksheet.Cell(currentRow, 10).Value = r.Note;
|
||||
|
||||
// Zebra-Streifen
|
||||
if (currentRow % 2 == 0)
|
||||
{
|
||||
for (int col = 1; col <= headers.Length; col++)
|
||||
{
|
||||
worksheet.Cell(currentRow, col).Style.Fill.BackgroundColor = XLColor.FromHtml("#F8FAFC"); // Slate 50
|
||||
}
|
||||
}
|
||||
|
||||
// Rahmenlinien
|
||||
for (int col = 1; col <= headers.Length; col++)
|
||||
{
|
||||
worksheet.Cell(currentRow, col).Style.Border.BottomBorder = XLBorderStyleValues.Thin;
|
||||
worksheet.Cell(currentRow, col).Style.Border.BottomBorderColor = XLColor.FromHtml("#E2E8F0");
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
// Summenzeile
|
||||
currentRow++;
|
||||
|
||||
worksheet.Cell(currentRow, 6).Value = "Gesamt:";
|
||||
worksheet.Cell(currentRow, 6).Style.Font.Bold = true;
|
||||
worksheet.Cell(currentRow, 6).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right;
|
||||
|
||||
var actualSumRange = $"G6:G{currentRow - 2}";
|
||||
worksheet.Cell(currentRow, 7).FormulaA1 = $"SUM({actualSumRange})";
|
||||
worksheet.Cell(currentRow, 7).Style.Font.Bold = true;
|
||||
worksheet.Cell(currentRow, 7).Style.NumberFormat.Format = "0.00";
|
||||
|
||||
var targetSumRange = $"H6:H{currentRow - 2}";
|
||||
worksheet.Cell(currentRow, 8).FormulaA1 = $"SUM({targetSumRange})";
|
||||
worksheet.Cell(currentRow, 8).Style.Font.Bold = true;
|
||||
worksheet.Cell(currentRow, 8).Style.NumberFormat.Format = "0.00";
|
||||
|
||||
var diffSumRange = $"I6:I{currentRow - 2}";
|
||||
worksheet.Cell(currentRow, 9).FormulaA1 = $"SUM({diffSumRange})";
|
||||
worksheet.Cell(currentRow, 9).Style.Font.Bold = true;
|
||||
worksheet.Cell(currentRow, 9).Style.NumberFormat.Format = "+0.00;-0.00;0.00";
|
||||
|
||||
// Summenrahmen
|
||||
for (int col = 6; col <= 9; col++)
|
||||
{
|
||||
worksheet.Cell(currentRow, col).Style.Border.TopBorder = XLBorderStyleValues.Thin;
|
||||
worksheet.Cell(currentRow, col).Style.Border.TopBorderColor = XLColor.Black;
|
||||
worksheet.Cell(currentRow, col).Style.Border.BottomBorder = XLBorderStyleValues.Double;
|
||||
worksheet.Cell(currentRow, col).Style.Border.BottomBorderColor = XLColor.Black;
|
||||
}
|
||||
|
||||
// Spaltenbreiten automatisch anpassen
|
||||
worksheet.Columns().AdjustToContents();
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
workbook.SaveAs(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportRow
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public TimeSpan? StartTime { get; set; }
|
||||
public TimeSpan? EndTime { get; set; }
|
||||
public TimeSpan BreakTime { get; set; }
|
||||
public double ActualHours { get; set; }
|
||||
public double TargetHours { get; set; }
|
||||
public double DiffHours { get; set; }
|
||||
public string Note { get; set; } = "";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace timetracker.Data.Migrations
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsTenantAdmin",
|
||||
table: "Users",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using timetracker.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace timetracker.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TimetrackerDbContext))]
|
||||
[Migration("20260625184756_PendingChanges")]
|
||||
partial class PendingChanges
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.AppSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("DailyTargetHours")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateOnly?>("FlexTimeStartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("FlexTimeStartingBalanceHours")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("GermanState")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MinimumBreakMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("TenantId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VacationDaysPerYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkFriday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkMonday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkSaturday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkSunday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkThursday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkTuesday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkWednesday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.BreakEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("WorkDayId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkDayId");
|
||||
|
||||
b.ToTable("BreakEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.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.Shared.Tenant", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsApproved")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Subdomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Subdomain")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tenants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsTenantAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("TenantId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.VacationDay", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("TenantId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("VacationDays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.WorkDay", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("TenantId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkDays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.BreakEntry", b =>
|
||||
{
|
||||
b.HasOne("timetracker.Shared.WorkDay", "WorkDay")
|
||||
.WithMany("Breaks")
|
||||
.HasForeignKey("WorkDayId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("WorkDay");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.User", b =>
|
||||
{
|
||||
b.HasOne("timetracker.Shared.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.WorkDay", b =>
|
||||
{
|
||||
b.Navigation("Breaks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace timetracker.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PendingChanges : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace timetracker.Data.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.8");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
|
||||
|
||||
modelBuilder.Entity("timetracker.Shared.AppSettings", b =>
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public class TenantProvider : ITenantProvider
|
||||
}
|
||||
|
||||
// Resolve and cache in HttpContext.Items
|
||||
ResolveTenantAsync().GetAwaiter().GetResult();
|
||||
ResolveTenant();
|
||||
|
||||
if (httpContext.Items.TryGetValue("TenantId", out cachedId))
|
||||
{
|
||||
@@ -48,27 +48,27 @@ public class TenantProvider : ITenantProvider
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tenant?> GetCurrentTenantAsync()
|
||||
public Task<Tenant?> GetCurrentTenantAsync()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext == null) return null;
|
||||
if (httpContext == null) return Task.FromResult<Tenant?>(null);
|
||||
|
||||
if (httpContext.Items.TryGetValue("CurrentTenant", out var cachedTenant))
|
||||
{
|
||||
return (Tenant?)cachedTenant;
|
||||
return Task.FromResult((Tenant?)cachedTenant);
|
||||
}
|
||||
|
||||
await ResolveTenantAsync();
|
||||
ResolveTenant();
|
||||
|
||||
if (httpContext.Items.TryGetValue("CurrentTenant", out cachedTenant))
|
||||
{
|
||||
return (Tenant?)cachedTenant;
|
||||
return Task.FromResult((Tenant?)cachedTenant);
|
||||
}
|
||||
|
||||
return null;
|
||||
return Task.FromResult<Tenant?>(null);
|
||||
}
|
||||
|
||||
private async Task ResolveTenantAsync()
|
||||
private void ResolveTenant()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext == null) return;
|
||||
@@ -82,9 +82,9 @@ public class TenantProvider : ITenantProvider
|
||||
|
||||
if (!string.IsNullOrEmpty(subdomain))
|
||||
{
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync();
|
||||
var tenant = await db.Tenants
|
||||
.FirstOrDefaultAsync(t => t.Subdomain.ToLower() == subdomain.ToLower());
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
var tenant = db.Tenants
|
||||
.FirstOrDefault(t => t.Subdomain.ToLower() == subdomain.ToLower());
|
||||
|
||||
if (tenant != null)
|
||||
{
|
||||
|
||||
@@ -114,14 +114,16 @@ if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase))
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? builder.Configuration["ConnectionStrings:DefaultConnection"];
|
||||
builder.Services.AddDbContextFactory<TimetrackerDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
options.UseNpgsql(connectionString)
|
||||
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbPath = Environment.GetEnvironmentVariable("TIMETRACKER_DB_PATH")
|
||||
?? Path.Combine(builder.Environment.ContentRootPath, "timetracker.db");
|
||||
builder.Services.AddDbContextFactory<TimetrackerDbContext>(options =>
|
||||
options.UseSqlite($"Data Source={dbPath}"));
|
||||
options.UseSqlite($"Data Source={dbPath}")
|
||||
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -131,13 +133,149 @@ using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<TimetrackerDbContext>>();
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
|
||||
if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
try
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
{
|
||||
await conn.OpenAsync();
|
||||
}
|
||||
|
||||
// Check if AppSettings table already exists
|
||||
using (var cmdCheck = conn.CreateCommand())
|
||||
{
|
||||
cmdCheck.CommandText = "SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'AppSettings');";
|
||||
var appSettingsExists = (bool)(await cmdCheck.ExecuteScalarAsync() ?? false);
|
||||
|
||||
if (appSettingsExists)
|
||||
{
|
||||
// Ensure __EFMigrationsHistory table exists
|
||||
using (var cmdCreateHistory = conn.CreateCommand())
|
||||
{
|
||||
cmdCreateHistory.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS ""__EFMigrationsHistory"" (
|
||||
""MigrationId"" character varying(150) NOT NULL,
|
||||
""ProductVersion"" character varying(32) NOT NULL,
|
||||
CONSTRAINT ""PK___EFMigrationsHistory"" PRIMARY KEY (""MigrationId"")
|
||||
);";
|
||||
await cmdCreateHistory.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// Check if the initial migration is already recorded
|
||||
using (var cmdCheckHistory = conn.CreateCommand())
|
||||
{
|
||||
cmdCheckHistory.CommandText = @"SELECT COUNT(*) FROM ""__EFMigrationsHistory"" WHERE ""MigrationId"" = '20260520133634_Initial';";
|
||||
var historyCount = Convert.ToInt32(await cmdCheckHistory.ExecuteScalarAsync() ?? 0);
|
||||
|
||||
if (historyCount == 0)
|
||||
{
|
||||
// Seed the history table with the migrations that were already created by EnsureCreated in older versions
|
||||
using (var cmdInsertHistory = conn.CreateCommand())
|
||||
{
|
||||
cmdInsertHistory.CommandText = @"
|
||||
INSERT INTO ""__EFMigrationsHistory"" (""MigrationId"", ""ProductVersion"") VALUES
|
||||
('20260520133634_Initial', '10.0.9'),
|
||||
('20260520200000_AddPublicHolidays', '10.0.9'),
|
||||
('20260522081459_AddMultiUser', '10.0.9'),
|
||||
('20260607213215_AddFlexTimeAndHolidayState', '10.0.9');";
|
||||
await cmdInsertHistory.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fehler beim Vorbereiten der PostgreSQL Migrationshistorie: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await db.Database.MigrateAsync();
|
||||
try
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
{
|
||||
await conn.OpenAsync();
|
||||
}
|
||||
|
||||
// Check if column IsTenantAdmin in Users table is of type integer
|
||||
using (var cmdCheckType = conn.CreateCommand())
|
||||
{
|
||||
cmdCheckType.CommandText = @"
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Users' AND column_name = 'IsTenantAdmin';";
|
||||
var dataType = await cmdCheckType.ExecuteScalarAsync() as string;
|
||||
if (dataType != null && dataType.Equals("integer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Alter the column type to boolean
|
||||
using (var cmdAlterType = conn.CreateCommand())
|
||||
{
|
||||
cmdAlterType.CommandText = @"
|
||||
ALTER TABLE ""Users""
|
||||
ALTER COLUMN ""IsTenantAdmin"" TYPE boolean
|
||||
USING (""IsTenantAdmin"" <> 0);";
|
||||
await cmdAlterType.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if column IsApproved in Tenants table is of type integer
|
||||
using (var cmdCheckApproved = conn.CreateCommand())
|
||||
{
|
||||
cmdCheckApproved.CommandText = @"
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Tenants' AND column_name = 'IsApproved';";
|
||||
var dataType = await cmdCheckApproved.ExecuteScalarAsync() as string;
|
||||
if (dataType != null && dataType.Equals("integer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Alter the column type to boolean
|
||||
using (var cmdAlterApproved = conn.CreateCommand())
|
||||
{
|
||||
cmdAlterApproved.CommandText = @"
|
||||
ALTER TABLE ""Tenants""
|
||||
ALTER COLUMN ""IsApproved"" TYPE boolean
|
||||
USING (""IsApproved"" <> 0);";
|
||||
await cmdAlterApproved.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if column CreatedAt in Tenants table is of type text or character varying
|
||||
using (var cmdCheckCreatedAt = conn.CreateCommand())
|
||||
{
|
||||
cmdCheckCreatedAt.CommandText = @"
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Tenants' AND column_name = 'CreatedAt';";
|
||||
var dataType = await cmdCheckCreatedAt.ExecuteScalarAsync() as string;
|
||||
if (dataType != null && (dataType.Equals("text", StringComparison.OrdinalIgnoreCase) || dataType.Equals("character varying", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// Alter the column type to timestamp without time zone
|
||||
using (var cmdAlterCreatedAt = conn.CreateCommand())
|
||||
{
|
||||
cmdAlterCreatedAt.CommandText = @"
|
||||
ALTER TABLE ""Tenants""
|
||||
ALTER COLUMN ""CreatedAt"" TYPE timestamp without time zone
|
||||
USING (""CreatedAt""::timestamp without time zone);";
|
||||
await cmdAlterCreatedAt.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fehler beim Konvertieren der PostgreSQL-Spaltentypen (IsTenantAdmin/IsApproved/CreatedAt): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,6 +597,83 @@ trackerApi.MapGet("/month", async (ClaimsPrincipal claimsPrincipal, [FromQuery]
|
||||
return Results.Ok(days);
|
||||
});
|
||||
|
||||
trackerApi.MapGet("/export/week", async (ClaimsPrincipal claimsPrincipal, [FromQuery] string monday, ITimetrackerService trackerService, TimetrackerDbContext db) =>
|
||||
{
|
||||
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
|
||||
|
||||
if (DateOnly.TryParse(monday, out var date))
|
||||
{
|
||||
var mondayDate = date;
|
||||
int diff = ((int)mondayDate.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7;
|
||||
mondayDate = mondayDate.AddDays(-diff);
|
||||
var sundayDate = mondayDate.AddDays(6);
|
||||
|
||||
var fileBytes = await ExcelExporter.GenerateExcelExportAsync(
|
||||
userId,
|
||||
mondayDate,
|
||||
sundayDate,
|
||||
$"Wochenbericht KW {System.Globalization.ISOWeek.GetWeekOfYear(new DateTime(mondayDate.Year, mondayDate.Month, mondayDate.Day))}",
|
||||
trackerService,
|
||||
db);
|
||||
|
||||
return Results.File(
|
||||
fileBytes,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
$"Wochenbericht_KW_{System.Globalization.ISOWeek.GetWeekOfYear(new DateTime(mondayDate.Year, mondayDate.Month, mondayDate.Day))}_{mondayDate:yyyyMMdd}.xlsx");
|
||||
}
|
||||
return Results.BadRequest("Ungültiges Datum.");
|
||||
});
|
||||
|
||||
trackerApi.MapGet("/export/month", async (ClaimsPrincipal claimsPrincipal, [FromQuery] int year, [FromQuery] int month, ITimetrackerService trackerService, TimetrackerDbContext db) =>
|
||||
{
|
||||
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
|
||||
|
||||
if (month < 1 || month > 12) return Results.BadRequest("Ungültiger Monat.");
|
||||
|
||||
var startDate = new DateOnly(year, month, 1);
|
||||
var endDate = startDate.AddMonths(1).AddDays(-1);
|
||||
|
||||
var deCulture = new System.Globalization.CultureInfo("de-DE");
|
||||
var monthName = deCulture.DateTimeFormat.GetMonthName(month);
|
||||
|
||||
var fileBytes = await ExcelExporter.GenerateExcelExportAsync(
|
||||
userId,
|
||||
startDate,
|
||||
endDate,
|
||||
$"Monatsbericht {monthName} {year}",
|
||||
trackerService,
|
||||
db);
|
||||
|
||||
return Results.File(
|
||||
fileBytes,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
$"Monatsbericht_{monthName}_{year}.xlsx");
|
||||
});
|
||||
|
||||
trackerApi.MapGet("/export/year", async (ClaimsPrincipal claimsPrincipal, [FromQuery] int year, ITimetrackerService trackerService, TimetrackerDbContext db) =>
|
||||
{
|
||||
var idClaim = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (!int.TryParse(idClaim, out var userId)) return Results.Unauthorized();
|
||||
|
||||
var startDate = new DateOnly(year, 1, 1);
|
||||
var endDate = new DateOnly(year, 12, 31);
|
||||
|
||||
var fileBytes = await ExcelExporter.GenerateExcelExportAsync(
|
||||
userId,
|
||||
startDate,
|
||||
endDate,
|
||||
$"Jahresbericht {year}",
|
||||
trackerService,
|
||||
db);
|
||||
|
||||
return Results.File(
|
||||
fileBytes,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
$"Jahresbericht_{year}.xlsx");
|
||||
});
|
||||
|
||||
// ── Holiday-API-Endpoints (Protected) ─────────────────────────────────────────
|
||||
var holidaysApi = app.MapGroup("/api/holidays").RequireAuthorization();
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="*" />
|
||||
<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="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user