WorkTracker/Services/Exports/MonthlyTimesheetExcelExporter.cs
2026-04-24 10:45:44 +02:00

230 lines
No EOL
7.2 KiB
C#

using ClosedXML.Excel;
using WorkTracker.Domain;
namespace WorkTracker.Services.Exports;
public sealed class MonthlyTimesheetExcelExporter : IMonthlyTimesheetExcelExporter
{
private const int DayHeaderRow = 1;
private const int DayStartColumn = 3;
private const int TemplateDayCount = 30;
private static readonly IReadOnlyDictionary<string, int> TimesheetRowMap = new Dictionary<string, int>(StringComparer.Ordinal)
{
["office"] = 2,
["home"] = 4,
["overtime"] = 5,
["weekend"] = 6,
["night"] = 7,
["vacation"] = 8,
["permit"] = 9,
["compensatory-rest"] = 10,
["sick"] = 11,
["holiday"] = 12
};
public byte[] Export(MonthlyTimesheetModel timesheet, Stream templateStream)
{
ArgumentNullException.ThrowIfNull(timesheet);
ArgumentNullException.ThrowIfNull(templateStream);
if (templateStream.CanSeek)
{
templateStream.Position = 0;
}
using var workbook = new XLWorkbook(templateStream);
workbook.DefinedNames.DeleteAll();
var worksheet = workbook.Worksheet(1);
var dayCount = timesheet.Days.Count;
AdjustDayColumns(worksheet, dayCount);
ApplyDayHeaders(worksheet, timesheet.Days);
ApplyRowValues(worksheet, timesheet.Rows, dayCount);
ApplyTotalFormulas(worksheet, dayCount);
using var output = new MemoryStream();
workbook.SaveAs(output);
return output.ToArray();
}
private static void AdjustDayColumns(IXLWorksheet worksheet, int dayCount)
{
var totalColumn = DayStartColumn + TemplateDayCount;
var delta = dayCount - TemplateDayCount;
if (delta > 0)
{
worksheet.Column(totalColumn).InsertColumnsBefore(delta);
}
else if (delta < 0)
{
worksheet.Columns(DayStartColumn + dayCount, DayStartColumn + TemplateDayCount - 1).Delete();
}
}
private static void ApplyDayHeaders(IXLWorksheet worksheet, IReadOnlyList<MonthlyTimesheetDayModel> days)
{
var lastDayColumn = DayStartColumn + days.Count - 1;
for (var index = 0; index < days.Count; index++)
{
var day = days[index];
var column = DayStartColumn + index;
var headerCell = worksheet.Cell(DayHeaderRow, column);
headerCell.Value = day.Date.Day;
ApplyDayStyle(worksheet, headerCell, day, column, lastDayColumn, 1);
foreach (var rowNumber in TimesheetRowMap.Values)
{
ApplyDayStyle(worksheet, worksheet.Cell(rowNumber, column), day, column, lastDayColumn, rowNumber);
}
}
}
private static void ApplyRowValues(IXLWorksheet worksheet, IReadOnlyList<MonthlyTimesheetRowModel> rows, int dayCount)
{
foreach (var row in rows)
{
if (!TimesheetRowMap.TryGetValue(row.Key, out var worksheetRow))
{
continue;
}
for (var dayIndex = 0; dayIndex < dayCount; dayIndex++)
{
var cell = worksheet.Cell(worksheetRow, DayStartColumn + dayIndex);
var value = dayIndex < row.DailyValues.Count ? row.DailyValues[dayIndex] : null;
if (value.HasValue && value.Value > 0m)
{
cell.Value = value.Value;
}
else
{
cell.Clear(XLClearOptions.Contents);
}
}
}
}
private static void ApplyTotalFormulas(IXLWorksheet worksheet, int dayCount)
{
var lastDayColumn = DayStartColumn + dayCount - 1;
var totalColumn = lastDayColumn + 1;
var firstDayColumnLetter = XLHelper.GetColumnLetterFromNumber(DayStartColumn);
var lastDayColumnLetter = XLHelper.GetColumnLetterFromNumber(lastDayColumn);
foreach (var rowNumber in TimesheetRowMap.Values)
{
worksheet.Cell(rowNumber, totalColumn).FormulaA1 = $"SUM({firstDayColumnLetter}{rowNumber}:{lastDayColumnLetter}{rowNumber})";
}
}
private static void ApplyDayStyle(IXLWorksheet worksheet, IXLCell targetCell, MonthlyTimesheetDayModel day, int column, int lastDayColumn, int rowNumber)
{
var baseCell = worksheet.Cell(GetBaseStyleRowAddress(rowNumber), GetBaseStyleColumn(column, lastDayColumn));
var baseStyle = baseCell.Style;
if (!ShouldHighlight(day))
{
targetCell.Style = baseStyle;
return;
}
var sampleColumn = GetHighlightSampleColumn(day, rowNumber);
var sampleCell = worksheet.Cell(GetSampleStyleRow(rowNumber), sampleColumn);
var sampleStyle = sampleCell.Style;
targetCell.Style = sampleStyle;
targetCell.Style.Border.LeftBorder = baseStyle.Border.LeftBorder;
targetCell.Style.Border.LeftBorderColor = baseStyle.Border.LeftBorderColor;
targetCell.Style.Border.RightBorder = baseStyle.Border.RightBorder;
targetCell.Style.Border.RightBorderColor = baseStyle.Border.RightBorderColor;
targetCell.Style.Border.TopBorder = baseStyle.Border.TopBorder;
targetCell.Style.Border.TopBorderColor = baseStyle.Border.TopBorderColor;
targetCell.Style.Border.BottomBorder = baseStyle.Border.BottomBorder;
targetCell.Style.Border.BottomBorderColor = baseStyle.Border.BottomBorderColor;
}
private static bool ShouldHighlight(MonthlyTimesheetDayModel day)
{
return day.IsWeekend || day.IsHoliday;
}
private static int GetBaseStyleRowAddress(int rowNumber)
{
return rowNumber switch
{
1 => 1,
2 => 2,
4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 => 4,
12 => 12,
_ => rowNumber
};
}
private static int GetSampleStyleRow(int rowNumber)
{
return rowNumber switch
{
1 => 1,
2 => 2,
4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 => 4,
12 => 12,
_ => rowNumber
};
}
private static int GetBaseStyleColumn(int column, int lastDayColumn)
{
if (column == DayStartColumn)
{
return DayStartColumn;
}
if (column == lastDayColumn)
{
return lastDayColumn == DayStartColumn + TemplateDayCount - 1
? DayStartColumn + TemplateDayCount - 1
: DayStartColumn + 1;
}
return DayStartColumn + 1;
}
private static int GetHighlightSampleColumn(MonthlyTimesheetDayModel day, int rowNumber)
{
if (rowNumber == 1)
{
if (day.IsHoliday || day.Date.DayOfWeek == DayOfWeek.Sunday)
{
return 7;
}
if (day.Date.DayOfWeek == DayOfWeek.Saturday)
{
return 6;
}
return 7;
}
if (rowNumber == 2)
{
if (day.IsHoliday || day.Date.DayOfWeek == DayOfWeek.Sunday)
{
return 7;
}
if (day.Date.DayOfWeek == DayOfWeek.Saturday)
{
return 6;
}
return 7;
}
return 6;
}
}