279 lines
11 KiB
C#
279 lines
11 KiB
C#
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; } = "";
|
|
}
|