using Lagerbestand.Models; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace Lagerbestand.Services; public interface IStorageRowPdfService { /// /// Generates a printable A4 inventory sheet for one shelf row (Area + Row). /// byte[] GenerateRowPdf(AreaType area, int row, IReadOnlyList locations); } public class StorageRowPdfService : IStorageRowPdfService { private static readonly string HeaderBackground = Colors.Grey.Lighten3; private static readonly string BorderColor = Colors.Grey.Medium; public byte[] GenerateRowPdf(AreaType area, int row, IReadOnlyList locations) { var rowLocations = locations .Where(l => l.Area == area && l.Row == row) .ToList(); var occupied = rowLocations.Count(l => l.IsOccupied); var total = StorageLocation.PositionsPerSlot * StorageLocation.MaxSlot; var printedAt = DateTime.Now; var document = Document.Create(container => { container.Page(page => { page.Size(PageSizes.A4); page.Margin(1.5f, Unit.Centimetre); page.DefaultTextStyle(x => x.FontSize(9)); page.Header() .PaddingBottom(12) .AlignCenter() .Text($"{area.GetDisplayName()} – Regal {row}") .Bold() .FontSize(20); page.Content().Row(r => { r.RelativeItem().Column(col => { col.Spacing(14); col.Item().Element(c => RenderPositionTable(c, area, row, ShelfPosition.LinksOben, rowLocations)); col.Item().Element(c => RenderPositionTable(c, area, row, ShelfPosition.LinksUnten, rowLocations)); }); r.ConstantItem(14); r.RelativeItem().Column(col => { col.Spacing(14); col.Item().Element(c => RenderPositionTable(c, area, row, ShelfPosition.RechtsOben, rowLocations)); col.Item().Element(c => RenderPositionTable(c, area, row, ShelfPosition.RechtsUnten, rowLocations)); }); }); page.Footer() .PaddingTop(10) .Row(f => { f.RelativeItem().Text($"Gedruckt am {printedAt:dd.MM.yyyy HH:mm}").FontSize(8).FontColor(Colors.Grey.Darken1); f.RelativeItem().AlignRight().Text($"{occupied} / {total} belegt").FontSize(8).FontColor(Colors.Grey.Darken1); }); }); }); return document.GeneratePdf(); } private static void RenderPositionTable(IContainer container, AreaType area, int row, ShelfPosition position, List rowLocations) { var bySlot = rowLocations .Where(l => l.Position == position) .GroupBy(l => l.SlotNumber) .ToDictionary(g => g.Key, g => g.First()); container.Column(col => { col.Spacing(4); col.Item().Text(position.GetDisplayName()).Bold().FontSize(11); col.Item().Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(28); columns.ConstantColumn(34); columns.RelativeColumn(3); columns.RelativeColumn(2); columns.ConstantColumn(44); }); table.Header(header => { header.Cell().Element(HeaderCell).Text("Platz"); header.Cell().Element(HeaderCell).Text("Code"); header.Cell().Element(HeaderCell).Text("Kunde/Inhalt"); header.Cell().Element(HeaderCell).Text("Fahrzeug"); header.Cell().Element(HeaderCell).Text("Saison"); }); for (var slot = StorageLocation.MinSlot; slot <= StorageLocation.MaxSlot; slot++) { bySlot.TryGetValue(slot, out var location); var code = location?.Code ?? $"{(int)area}{row}{(int)position}{slot}"; string content, vehicle, season; var muted = false; if (location is { IsOccupied: true, OccupiedTireStack: not null }) { var stack = location.OccupiedTireStack; content = stack.Customer?.FullName ?? stack.CatalogItem?.Name ?? "Lager"; vehicle = stack.Vehicle?.ShortName ?? "–"; season = SeasonText(stack.Season); } else if (location is { IsOccupied: true }) { content = "Belegt"; vehicle = "–"; season = "–"; } else if (location is { IsAvailable: false }) { content = "Gesperrt"; vehicle = "–"; season = "–"; muted = true; } else { content = "Frei"; vehicle = "–"; season = "–"; muted = true; } table.Cell().Element(BodyCell).Text(slot.ToString()); table.Cell().Element(BodyCell).Text(code); table.Cell().Element(BodyCell).Text(t => Styled(t.Span(content), muted)); table.Cell().Element(BodyCell).Text(t => Styled(t.Span(vehicle), muted)); table.Cell().Element(BodyCell).Text(t => Styled(t.Span(season), muted)); } }); }); } private static void Styled(TextSpanDescriptor span, bool muted) { if (muted) span.FontColor(Colors.Grey.Medium).Italic(); } private static string SeasonText(TireSeason season) => season switch { TireSeason.Sommer => "Sommer", TireSeason.Winter => "Winter", TireSeason.Allrad => "Ganzjahr", _ => season.ToString() }; private static IContainer HeaderCell(IContainer container) => container .Background(HeaderBackground) .Border(0.5f) .BorderColor(BorderColor) .PaddingVertical(3) .PaddingHorizontal(4) .DefaultTextStyle(x => x.Bold().FontSize(8)); private static IContainer BodyCell(IContainer container) => container .Border(0.5f) .BorderColor(BorderColor) .PaddingVertical(3) .PaddingHorizontal(4) .DefaultTextStyle(x => x.FontSize(8)); }