Multi tenant integration #10
@@ -6,6 +6,7 @@
|
|||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<PageTitle>KW @_kw – Wochenübersicht – Timetracker</PageTitle>
|
<PageTitle>KW @_kw – Wochenübersicht – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ else
|
|||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
<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)
|
@if (!IsCurrentWeek)
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
<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 PrevWeek() { _monday = _monday.AddDays(-7); await LoadWeek(); }
|
||||||
private async Task NextWeek() { _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 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)
|
private static DateOnly GetMonday(DateOnly date)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@inject ITimetrackerService TrackerService
|
@inject ITimetrackerService TrackerService
|
||||||
@inject IHolidayService HolidayService
|
@inject IHolidayService HolidayService
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker</PageTitle>
|
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -32,6 +33,10 @@ else
|
|||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
<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)
|
@if (!IsCurrentMonth)
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
||||||
@@ -268,6 +273,11 @@ else
|
|||||||
await LoadMonth();
|
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)
|
private static string FormatTs(TimeSpan ts, bool sign = false)
|
||||||
{
|
{
|
||||||
var neg = ts < TimeSpan.Zero;
|
var neg = ts < TimeSpan.Zero;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject ITimetrackerService TrackerService
|
@inject ITimetrackerService TrackerService
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
@using System.Security.Claims
|
@using System.Security.Claims
|
||||||
@using timetracker.Shared
|
@using timetracker.Shared
|
||||||
|
|
||||||
@@ -210,6 +211,75 @@ else
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
</MudItem>
|
</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 ── *@
|
@* ── Diagnostics Panel ── *@
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
@@ -263,6 +333,12 @@ else
|
|||||||
private double _weekTargetHours;
|
private double _weekTargetHours;
|
||||||
private double _weekOvertimeHours;
|
private double _weekOvertimeHours;
|
||||||
private double _maxHoursValue = 10.0;
|
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
|
// Debug variables
|
||||||
private int _rawDbDaysCount = 0;
|
private int _rawDbDaysCount = 0;
|
||||||
@@ -373,6 +449,25 @@ else
|
|||||||
return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}";
|
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
|
private sealed class DayStat
|
||||||
{
|
{
|
||||||
public DateOnly Date { get; set; }
|
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; } = "";
|
||||||
|
}
|
||||||
@@ -459,6 +459,83 @@ trackerApi.MapGet("/month", async (ClaimsPrincipal claimsPrincipal, [FromQuery]
|
|||||||
return Results.Ok(days);
|
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) ─────────────────────────────────────────
|
// ── Holiday-API-Endpoints (Protected) ─────────────────────────────────────────
|
||||||
var holidaysApi = app.MapGroup("/api/holidays").RequireAuthorization();
|
var holidaysApi = app.MapGroup("/api/holidays").RequireAuthorization();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="*" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="*" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
|
||||||
|
|||||||
Reference in New Issue
Block a user