using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PowderCoating.Application.DTOs.Company; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Interfaces; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace PowderCoating.Web.Controllers; [Authorize] public class WorkOrderController : Controller { private readonly IUnitOfWork _unitOfWork; private readonly ITenantContext _tenantContext; private readonly ICompanyLogoService _logoService; public WorkOrderController(IUnitOfWork unitOfWork, ITenantContext tenantContext, ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _tenantContext = tenantContext; _logoService = logoService; } /// /// Streams a blank work order PDF. Uses the company logo from file storage, /// accent color, and terms from CompanyPreferences.WoTerms. /// [HttpGet] public async Task Blank() { var companyId = _tenantContext.GetCurrentCompanyId(); if (companyId == null) return Forbid(); var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value); var prefs = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync( p => p.CompanyId == companyId.Value && !p.IsDeleted); // Try file-system logo first, then fall back to legacy DB bytes byte[]? logoBytes = null; if (!string.IsNullOrWhiteSpace(company?.LogoFilePath)) { var (ok, content, _, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); if (ok) logoBytes = content; } if (logoBytes == null || logoBytes.Length == 0) logoBytes = company?.LogoData; var companyInfo = new CompanyInfoDto { CompanyName = company?.CompanyName ?? string.Empty, Phone = company?.Phone, Address = company?.Address, City = company?.City, State = company?.State, ZipCode = company?.ZipCode, }; var pdfBytes = GenerateBlankWorkOrderPdf(logoBytes, companyInfo, prefs?.WoAccentColor, prefs?.WoTerms); Response.Headers["Content-Disposition"] = "inline; filename=\"Blank-Work-Order.pdf\""; return File(pdfBytes, "application/pdf"); } private static byte[] GenerateBlankWorkOrderPdf( byte[]? logoData, CompanyInfoDto companyInfo, string? accentHex, string? terms) { QuestPDF.Settings.License = LicenseType.Community; var accent = ResolveColor(accentHex, Colors.Grey.Darken3); const string altRow = "#F5F5F5"; var document = Document.Create(container => { container.Page(page => { page.Size(PageSizes.Letter); page.MarginHorizontal(0.6f, Unit.Inch); page.MarginTop(0.5f, Unit.Inch); page.MarginBottom(0.4f, Unit.Inch); page.PageColor(Colors.White); page.DefaultTextStyle(x => x.FontSize(8.5f).FontFamily("Arial")); page.Content().Column(col => { // ── HEADER: logo left, company info + drop-off date right ─ col.Item().Row(row => { if (logoData != null && logoData.Length > 0) row.ConstantItem(100).PaddingRight(10).Image(logoData).FitArea(); row.RelativeItem().AlignRight().Column(c => { c.Item().Text(companyInfo.CompanyName) .FontSize(18).Bold().FontColor(accent); var parts = new[] { companyInfo.Address, string.Join(", ", new[] { companyInfo.City, companyInfo.State } .Where(s => !string.IsNullOrWhiteSpace(s))), companyInfo.ZipCode, companyInfo.Phone }.Where(s => !string.IsNullOrWhiteSpace(s)); foreach (var p in parts) c.Item().Text(p!).FontSize(9).FontColor(Colors.Grey.Darken1); }); }); // ── TITLE ROW: "WORK ORDER" left, Drop Off Date field right ─ col.Item().PaddingTop(8).Row(row => { row.RelativeItem().AlignBottom() .Text("WORK ORDER") .FontSize(24).FontColor(Colors.Grey.Lighten1).Bold(); row.ConstantItem(180).AlignBottom().Column(c => { c.Item().Text("DROP OFF DATE") .FontSize(7.5f).Bold().FontColor(accent); c.Item().PaddingTop(2) .LineHorizontal(1).LineColor(Colors.Grey.Darken1); }); }); col.Item().PaddingTop(4).LineHorizontal(1).LineColor(Colors.Grey.Lighten2); col.Item().Height(6); // ── CLIENT INFO (no Drop Off Date column) ──────────────── col.Item().Table(table => { table.ColumnsDefinition(c => c.RelativeColumn()); void HeaderCell(uint r, string label) => table.Cell().Row(r).Column(1) .Background(accent).Padding(4) .Text(label).FontSize(7.5f).Bold().FontColor(Colors.White); void DataCell(uint r) => table.Cell().Row(r).Column(1) .Border(0.5f).BorderColor(Colors.Grey.Lighten2) .MinHeight(18).Padding(3).Text(""); HeaderCell(1, "CLIENT NAME"); DataCell(2); HeaderCell(3, "CLIENT PHONE"); DataCell(4); HeaderCell(5, "DUE DATE (if applicable)"); DataCell(6); }); col.Item().Height(8); // ── ITEMS TABLE ────────────────────────────────────────── col.Item().Table(table => { table.ColumnsDefinition(c => { c.RelativeColumn(6); c.RelativeColumn(2); c.RelativeColumn(1.5f); }); // Header foreach (var (label, align) in new[] { ("PART DESCRIPTION", "left"), ("COLOR", "center"), ("QUOTE", "center") }) { var cell = table.Cell().Background(accent).Padding(5); var txt = cell.Text(label).FontSize(7.5f).Bold().FontColor(Colors.White); if (align == "center") txt.AlignCenter(); } // 12 data rows — tight height to fit page for (int i = 0; i < 12; i++) { string bg = i % 2 == 1 ? altRow : Colors.White; for (int c = 0; c < 3; c++) table.Cell().Background(bg) .Border(0.5f).BorderColor(Colors.Grey.Lighten2) .MinHeight(16).Padding(3).Text(""); } // Dark footer bar table.Cell().ColumnSpan(3).Background(accent).MinHeight(6).Text(""); }); col.Item().Height(8); // ── NOTES ──────────────────────────────────────────────── col.Item().Table(table => { table.ColumnsDefinition(c => c.RelativeColumn()); table.Cell().Background(accent).Padding(5) .Text("NOTES").FontSize(7.5f).Bold().FontColor(Colors.White); table.Cell().Border(0.5f).BorderColor(Colors.Grey.Lighten2) .MinHeight(65).Padding(3).Text(""); }); col.Item().Height(8); // ── TERMS ──────────────────────────────────────────────── if (!string.IsNullOrWhiteSpace(terms)) { col.Item().PaddingBottom(8) .Text(terms).FontSize(7.5f).Italic(); } // ── SIGNATURE LINE ─────────────────────────────────────── col.Item().Row(row => { row.RelativeItem(3).Column(c => { c.Item().Text("Customer Signature:").FontSize(8.5f); c.Item().PaddingTop(2).LineHorizontal(1).LineColor(Colors.Grey.Darken1); }); row.ConstantItem(20); row.RelativeItem(1).Column(c => { c.Item().Text("Date:").FontSize(8.5f); c.Item().PaddingTop(2).LineHorizontal(1).LineColor(Colors.Grey.Darken1); }); }); }); }); }); return document.GeneratePdf(); } private static string ResolveColor(string? hex, string fallback) { if (string.IsNullOrWhiteSpace(hex)) return fallback; try { _ = System.Drawing.ColorTranslator.FromHtml(hex); return hex; } catch { return fallback; } } }