267 lines
12 KiB
C#
267 lines
12 KiB
C#
using MigraDoc.DocumentObjectModel;
|
|
using MigraDoc.DocumentObjectModel.Tables;
|
|
using MigraDoc.Rendering;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using timetracker.Shared;
|
|
|
|
namespace timetracker.Server.Services
|
|
{
|
|
public class PdfGeneratorService
|
|
{
|
|
public byte[] GenerateTimesheetPdf(
|
|
User user,
|
|
Tenant? tenant,
|
|
AppSettings settings,
|
|
int year,
|
|
int month,
|
|
List<WorkDay> workDays,
|
|
List<VacationDay> vacationDays,
|
|
List<PublicHoliday> holidays)
|
|
{
|
|
if (PdfSharp.Fonts.GlobalFontSettings.FontResolver == null)
|
|
{
|
|
PdfSharp.Fonts.GlobalFontSettings.FontResolver = new PdfFontResolver();
|
|
}
|
|
|
|
var document = new Document();
|
|
document.Info.Title = "Stundenzettel";
|
|
document.Info.Author = tenant?.Name ?? "Timetracker";
|
|
|
|
var section = document.AddSection();
|
|
section.PageSetup.PageFormat = PageFormat.A4;
|
|
section.PageSetup.TopMargin = "2cm";
|
|
section.PageSetup.BottomMargin = "2cm";
|
|
section.PageSetup.LeftMargin = "2cm";
|
|
section.PageSetup.RightMargin = "2cm";
|
|
|
|
var style = document.Styles["Normal"];
|
|
if (style != null && style.Font != null)
|
|
{
|
|
style.Font.Name = "OpenSans";
|
|
style.Font.Size = 10;
|
|
}
|
|
|
|
ComposeHeader(section, user, tenant, year, month);
|
|
ComposeContent(section, document, user, settings, year, month, workDays, vacationDays, holidays);
|
|
ComposeFooter(section);
|
|
|
|
var pdfRenderer = new PdfDocumentRenderer()
|
|
{
|
|
Document = document
|
|
};
|
|
pdfRenderer.RenderDocument();
|
|
|
|
using var stream = new System.IO.MemoryStream();
|
|
pdfRenderer.PdfDocument.Save(stream, false);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private void ComposeHeader(Section section, User user, Tenant? tenant, int year, int month)
|
|
{
|
|
var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy", new System.Globalization.CultureInfo("de-DE"));
|
|
var companyName = tenant?.Name ?? "Hauptdomain";
|
|
|
|
var paragraph = section.AddParagraph($"Stundenzettel - {monthName}");
|
|
paragraph.Format.Font.Size = 20;
|
|
paragraph.Format.Font.Bold = true;
|
|
paragraph.Format.Font.Color = Colors.DarkBlue;
|
|
paragraph.Format.SpaceAfter = "0.2cm";
|
|
|
|
paragraph = section.AddParagraph($"Mitarbeiter: {user.Username}");
|
|
paragraph.Format.Font.Size = 14;
|
|
paragraph.Format.Font.Bold = true;
|
|
paragraph.Format.SpaceAfter = "0.1cm";
|
|
|
|
paragraph = section.AddParagraph($"Firma: {companyName}");
|
|
paragraph.Format.Font.Size = 11;
|
|
paragraph.Format.Font.Color = Colors.DarkGray;
|
|
paragraph.Format.SpaceAfter = "1cm";
|
|
}
|
|
|
|
private void ComposeContent(Section section, Document document, User user, AppSettings settings, int year, int month, List<WorkDay> workDays, List<VacationDay> vacationDays, List<PublicHoliday> holidays)
|
|
{
|
|
ComposeTable(section, document, settings, year, month, workDays, vacationDays, holidays, out var totalNet, out var totalTarget, out var totalOvertime);
|
|
|
|
ComposeSummary(section, totalNet, totalTarget, totalOvertime);
|
|
|
|
ComposeSignatures(section);
|
|
}
|
|
|
|
private void ComposeTable(Section section, Document document, AppSettings settings, int year, int month, List<WorkDay> workDays, List<VacationDay> vacationDays, List<PublicHoliday> holidays, out TimeSpan totalNetOut, out TimeSpan totalTargetOut, out TimeSpan totalOvertimeOut)
|
|
{
|
|
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
|
|
var vacationSet = vacationDays.Select(v => v.Date).ToHashSet();
|
|
var daysInMonth = DateTime.DaysInMonth(year, month);
|
|
|
|
var totalNet = TimeSpan.Zero;
|
|
var totalTarget = TimeSpan.Zero;
|
|
var totalOvertime = TimeSpan.Zero;
|
|
|
|
var table = section.AddTable();
|
|
table.Borders.Width = 0.5;
|
|
table.Borders.Color = Colors.LightGray;
|
|
|
|
// Define columns
|
|
table.AddColumn("2.5cm"); // Datum
|
|
table.AddColumn("1.5cm"); // Start
|
|
table.AddColumn("1.5cm"); // Ende
|
|
table.AddColumn("1.8cm"); // Pause
|
|
table.AddColumn("1.8cm"); // Netto
|
|
table.AddColumn("1.8cm"); // Gleitzeit
|
|
table.AddColumn("6.1cm"); // Info
|
|
|
|
// Header Row
|
|
var row = table.AddRow();
|
|
row.HeadingFormat = true;
|
|
row.Format.Font.Bold = true;
|
|
row.Shading.Color = Colors.White;
|
|
row.Borders.Bottom.Width = 1;
|
|
row.Borders.Bottom.Color = Colors.Black;
|
|
|
|
row.Cells[0].AddParagraph("Datum");
|
|
row.Cells[1].AddParagraph("Start").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[2].AddParagraph("Ende").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[3].AddParagraph("Pause").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[4].AddParagraph("Netto").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[5].AddParagraph("Gleitzeit").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[6].AddParagraph("Info/Status");
|
|
|
|
for (int day = 1; day <= daysInMonth; day++)
|
|
{
|
|
var date = new DateOnly(year, month, day);
|
|
var wd = workDays.FirstOrDefault(w => w.Date == date);
|
|
var isWorkDay = settings.IsWorkDay(date.DayOfWeek);
|
|
var isVacation = vacationSet.Contains(date);
|
|
var holidayName = holidayMap.GetValueOrDefault(date);
|
|
|
|
TimeSpan? net = null;
|
|
TimeSpan breakSum = TimeSpan.Zero;
|
|
|
|
if (wd?.StartTime != null && wd.EndTime != null)
|
|
{
|
|
var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan();
|
|
breakSum = 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 = breakSum > minBreak ? breakSum : minBreak;
|
|
net = gross - finalBreak;
|
|
if (net < TimeSpan.Zero) net = null;
|
|
}
|
|
|
|
var target = isWorkDay ? TimeSpan.FromHours(settings.DailyTargetHours) : TimeSpan.Zero;
|
|
var overtime = net.HasValue ? net.Value - target : TimeSpan.Zero;
|
|
|
|
if (net.HasValue) totalNet += net.Value;
|
|
if (isWorkDay) totalTarget += target;
|
|
if (isWorkDay && net.HasValue) totalOvertime += overtime;
|
|
|
|
string status = "";
|
|
if (!string.IsNullOrEmpty(holidayName)) status = holidayName;
|
|
else if (isVacation) status = "Urlaub";
|
|
else if (!isWorkDay) status = "Wochenende";
|
|
|
|
var bgColor = Colors.White;
|
|
if (!isWorkDay || !string.IsNullOrEmpty(holidayName)) bgColor = new Color(245, 245, 245); // Light Gray
|
|
if (isVacation) bgColor = new Color(255, 236, 179); // Light Orange
|
|
|
|
row = table.AddRow();
|
|
row.Shading.Color = bgColor;
|
|
|
|
row.Cells[0].AddParagraph(date.ToString("dd.MM.yyyy"));
|
|
row.Cells[1].AddParagraph(wd?.StartTime?.ToString(@"HH\:mm") ?? "-").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[2].AddParagraph(wd?.EndTime?.ToString(@"HH\:mm") ?? "-").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[3].AddParagraph(net.HasValue ? FormatTs(breakSum) : "-").Format.Alignment = ParagraphAlignment.Right;
|
|
row.Cells[4].AddParagraph(net.HasValue ? FormatTs(net.Value) : "-").Format.Alignment = ParagraphAlignment.Right;
|
|
|
|
var overtimePara = row.Cells[5].AddParagraph(net.HasValue ? FormatTs(overtime, true) : "-");
|
|
overtimePara.Format.Alignment = ParagraphAlignment.Right;
|
|
if (net.HasValue && overtime != TimeSpan.Zero)
|
|
{
|
|
overtimePara.Format.Font.Color = overtime < TimeSpan.Zero ? Colors.DarkRed : Colors.DarkGreen;
|
|
}
|
|
|
|
var statusPara = row.Cells[6].AddParagraph(status);
|
|
statusPara.Format.Font.Color = Colors.DarkGray;
|
|
}
|
|
|
|
totalNetOut = totalNet;
|
|
totalTargetOut = totalTarget;
|
|
totalOvertimeOut = totalOvertime;
|
|
}
|
|
|
|
private void ComposeSummary(Section section, TimeSpan totalNet, TimeSpan totalTarget, TimeSpan totalOvertime)
|
|
{
|
|
var paragraph = section.AddParagraph();
|
|
paragraph.Format.SpaceBefore = "1cm";
|
|
|
|
var table = section.AddTable();
|
|
table.Shading.Color = new Color(245, 245, 245);
|
|
table.AddColumn("4cm");
|
|
table.AddColumn("4cm");
|
|
|
|
var row = table.AddRow();
|
|
row.Cells[0].AddParagraph("Zusammenfassung").Format.Font.Bold = true;
|
|
row.Cells[0].MergeRight = 1;
|
|
|
|
row = table.AddRow();
|
|
row.Cells[0].AddParagraph("Soll-Stunden:");
|
|
row.Cells[1].AddParagraph(FormatTs(totalTarget));
|
|
|
|
row = table.AddRow();
|
|
row.Cells[0].AddParagraph("Ist-Stunden:");
|
|
row.Cells[1].AddParagraph(FormatTs(totalNet));
|
|
|
|
row = table.AddRow();
|
|
row.Cells[0].AddParagraph("Saldo:");
|
|
var saldoPara = row.Cells[1].AddParagraph(FormatTs(totalOvertime, true));
|
|
saldoPara.Format.Font.Bold = true;
|
|
if (totalOvertime != TimeSpan.Zero)
|
|
{
|
|
saldoPara.Format.Font.Color = totalOvertime < TimeSpan.Zero ? Colors.DarkRed : Colors.DarkGreen;
|
|
}
|
|
}
|
|
|
|
private void ComposeSignatures(Section section)
|
|
{
|
|
var table = section.AddTable();
|
|
table.Format.SpaceBefore = "2cm";
|
|
table.AddColumn("7cm");
|
|
table.AddColumn("2cm");
|
|
table.AddColumn("7cm");
|
|
|
|
var row = table.AddRow();
|
|
row.Cells[0].Borders.Bottom.Width = 1;
|
|
row.Cells[2].Borders.Bottom.Width = 1;
|
|
|
|
row = table.AddRow();
|
|
row.Cells[0].AddParagraph("Datum, Unterschrift Mitarbeiter").Format.Font.Size = 8;
|
|
row.Cells[2].AddParagraph("Datum, Unterschrift Arbeitgeber").Format.Font.Size = 8;
|
|
}
|
|
|
|
private void ComposeFooter(Section section)
|
|
{
|
|
var paragraph = section.Footers.Primary.AddParagraph();
|
|
paragraph.AddText("Seite ");
|
|
paragraph.AddPageField();
|
|
paragraph.AddText(" von ");
|
|
paragraph.AddNumPagesField();
|
|
paragraph.Format.Alignment = ParagraphAlignment.Center;
|
|
}
|
|
|
|
private static string FormatTs(TimeSpan ts, bool sign = false)
|
|
{
|
|
var neg = ts < TimeSpan.Zero;
|
|
var abs = neg ? ts.Negate() : ts;
|
|
var h = (int)abs.TotalHours;
|
|
var m = abs.Minutes;
|
|
if (!sign) return $"{h}:{m:D2} h";
|
|
return neg ? $"-{h}:{m:D2} h" : $"+{h}:{m:D2} h";
|
|
}
|
|
}
|
|
}
|