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 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(); 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 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; } = ""; }