230 lines
7.2 KiB
C#
230 lines
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;
|
||
|
|
}
|
||
|
|
}
|