240 lines
10 KiB
C#
240 lines
10 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Streams a blank work order PDF. Uses the company logo from file storage,
|
|
/// accent color, and terms from CompanyPreferences.WoTerms.
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> 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; }
|
|
}
|
|
}
|