Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Jobs/WorkOrder.cshtml
T
spouliot 94e536178c Add optional Project Name field to quotes, jobs, and printed documents
- Add ProjectName (nvarchar 100, nullable) to Quote and Job entities;
  migration AddProjectNameToQuotesAndJobs applied
- Add ProjectName to all relevant DTOs: QuoteDto/Create/Update,
  JobDto/List/Create/Update, InvoiceDto (mapped from Job.ProjectName
  via AutoMapper so the invoice PDF picks it up without a separate column)
- Form field added after Customer PO in Quote Create/Edit and Job Create/Edit
- CreateJobFromQuote copies ProjectName from quote to job automatically
- Details views (Quote and Job) display Project when set
- Printable quote PDF: Project row in the quote details block
- Work order: Project row in customer/job info section
- Invoice PDF: Project shown in the Job Reference block alongside Job # and PO #

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 14:48:28 -04:00

674 lines
28 KiB
Plaintext

@model PowderCoating.Application.DTOs.Job.JobDto
@using PowderCoating.Web.Controllers
@{
ViewData["Title"] = $"Work Order - {Model.JobNumber}";
Layout = null; // No layout for print
var company = ViewBag.Company as PowderCoating.Core.Entities.Company;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/lib/bootstrap-icons/font/bootstrap-icons.css">
<script>
// Auto-trigger print dialog when page loads
window.onload = function() {
// Small delay to ensure page is fully rendered
setTimeout(function() {
window.print();
}, 500);
};
</script>
<style>
@@media print {
.no-print {
display: none !important;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.page-break {
page-break-after: always;
}
@@page {
margin: 0.4in 0.5in;
size: letter;
}
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 9pt;
line-height: 1.3;
}
.work-order-header {
border-bottom: 2px solid #0d6efd;
padding-bottom: 10px;
margin-bottom: 15px;
}
.company-logo {
max-height: 60px;
max-width: 150px;
margin-bottom: 5px;
}
.company-name {
font-size: 16pt;
font-weight: bold;
color: #0d6efd;
margin-bottom: 3px;
}
.company-info {
font-size: 8pt;
line-height: 1.4;
}
.work-order-title {
font-size: 20pt;
font-weight: bold;
text-align: center;
color: #333;
}
.section-title {
background-color: #f8f9fa;
border-left: 3px solid #0d6efd;
padding: 4px 8px;
margin-top: 12px;
margin-bottom: 8px;
font-weight: bold;
font-size: 11pt;
}
.info-row {
padding: 3px 0;
border-bottom: 1px solid #e9ecef;
}
.info-label {
font-weight: 600;
color: #6c757d;
margin-bottom: 2px;
font-size: 8pt;
}
.info-value {
color: #212529;
font-size: 9pt;
}
.items-table {
width: 100%;
margin-top: 8px;
border-collapse: collapse;
font-size: 8pt;
}
.items-table th {
background-color: #0d6efd;
color: white;
padding: 4px 6px;
text-align: left;
font-weight: 600;
font-size: 8pt;
}
.items-table td {
padding: 4px 6px;
border-bottom: 1px solid #dee2e6;
}
.items-table tbody tr:nth-child(even) {
background-color: #f8f9fa;
}
.badge {
padding: 2px 8px;
border-radius: 3px;
font-weight: 600;
font-size: 8pt;
}
.prep-service-badge {
display: inline-block;
background-color: #d1e7dd;
color: #0f5132;
border: 1px solid #0f5132;
padding: 3px 8px;
margin: 2px;
border-radius: 3px;
font-weight: 600;
font-size: 8pt;
}
.signature-section {
margin-top: 15px;
page-break-inside: avoid;
}
.signature-line {
border-top: 1px solid #333;
margin-top: 25px;
padding-top: 3px;
font-size: 8pt;
}
.special-instructions {
background-color: #fff3cd;
border: 1px solid #ffc107;
padding: 8px;
border-radius: 3px;
margin-top: 8px;
font-size: 8pt;
}
.footer-note {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid #dee2e6;
font-size: 7pt;
color: #6c757d;
text-align: center;
}
.compact-section {
margin-bottom: 8px;
}
.item-card {
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 10px;
page-break-inside: avoid;
}
.item-card-header {
background-color: #0d6efd;
color: white;
padding: 5px 10px;
border-radius: 3px 3px 0 0;
display: flex;
align-items: center;
gap: 10px;
}
.item-card-body {
padding: 8px 10px;
}
.coat-table {
width: 100%;
border-collapse: collapse;
font-size: 8pt;
margin-top: 4px;
}
.coat-table th {
background-color: #e9ecef;
padding: 3px 6px;
font-weight: 600;
font-size: 7.5pt;
color: #495057;
border: 1px solid #dee2e6;
}
.coat-table td {
padding: 3px 6px;
border: 1px solid #dee2e6;
vertical-align: middle;
}
.order-badge {
display: inline-block;
background-color: #ffc107;
color: #212529;
border: 1px solid #e0a800;
padding: 1px 5px;
border-radius: 3px;
font-weight: 700;
font-size: 7pt;
}
.sub-label {
font-size: 7.5pt;
font-weight: 600;
color: #6c757d;
margin-bottom: 2px;
}
</style>
</head>
<body>
<div class="container-fluid p-3">
<!-- Print Button (hidden when printing) -->
<div class="no-print mb-2">
<button onclick="window.print()" class="btn btn-primary btn-sm">
<i class="bi bi-printer me-2"></i>Print Work Order
</button>
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary btn-sm ms-2">
<i class="bi bi-arrow-left me-2"></i>Back to Job Details
</a>
</div>
<!-- Header -->
<div class="work-order-header">
<div class="row align-items-center">
<div class="col-6">
@if (company != null)
{
@* Company Logo *@
@if (!string.IsNullOrWhiteSpace(company.LogoFilePath) || company.LogoData != null)
{
<img src="@Url.Action("Logo", "CompanySettings")" alt="@company.CompanyName" class="company-logo" />
}
<div class="company-name">@company.CompanyName</div>
<div class="company-info">
@if (!string.IsNullOrWhiteSpace(company.Address))
{
<div>@company.Address</div>
}
@if (!string.IsNullOrWhiteSpace(company.Phone))
{
<span>Phone: @company.Phone</span>
}
@if (!string.IsNullOrWhiteSpace(company.PrimaryContactEmail))
{
<span class="ms-2">Email: @company.PrimaryContactEmail</span>
}
</div>
}
else
{
<div class="company-name">Powder Coating Shop</div>
}
</div>
<div class="col-6">
<div class="work-order-title">WORK ORDER</div>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 8px;">
<div style="font-size: 8pt; line-height: 1.6; flex: 1; text-align: center;">
<div class="mb-2">
<span class="text-muted">Job #:</span>
<span style="font-size: 14pt;" class="fw-bold ms-1">@Model.JobNumber</span>
</div>
<div class="d-flex justify-content-center align-items-center gap-3 mb-1">
<div>
<span class="text-muted">Priority:</span>
<span class="badge bg-@Model.PriorityColorClass ms-1">@Model.PriorityDisplayName</span>
</div>
<div>
<span class="text-muted">Status:</span>
<span class="badge bg-@Model.StatusColorClass ms-1">@Model.StatusDisplayName</span>
</div>
</div>
<div class="text-center text-muted" style="font-size: 7pt;">Created: @Model.CreatedAt.ToString("MM/dd/yyyy")</div>
</div>
@if (ViewBag.ViewQrCodeBase64 != null)
{
<div style="text-align: center; flex-shrink: 0;">
<img src="data:image/png;base64,@ViewBag.ViewQrCodeBase64"
alt="View Job"
style="width: 64px; height: 64px; image-rendering: pixelated; display: block;" />
<div style="font-size: 6.5pt; color: #6c757d; margin-top: 2px;">View Job</div>
</div>
}
</div>
</div>
</div>
</div>
<!-- Customer & Job Information Combined -->
<div class="row compact-section">
<div class="col-6">
<div class="section-title">
<i class="bi bi-person-circle me-1"></i>Customer Information
</div>
<div class="info-row">
<div class="info-label">Company/Name</div>
<div class="info-value">
@if (!string.IsNullOrWhiteSpace(Model.CustomerCompanyName))
{
<strong>@Model.CustomerCompanyName</strong>
}
else
{
@Model.CustomerName
}
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Model.CustomerContactName))
{
<div class="info-row">
<div class="info-label">Contact Person</div>
<div class="info-value">@Model.CustomerContactName</div>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
{
<div class="info-row">
<div class="info-label">Customer PO</div>
<div class="info-value">@Model.CustomerPO</div>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.ProjectName))
{
<div class="info-row">
<div class="info-label">Project</div>
<div class="info-value">@Model.ProjectName</div>
</div>
}
</div>
<div class="col-6">
<div class="section-title">
<i class="bi bi-briefcase me-1"></i>Job Information
</div>
<div class="row">
@if (!string.IsNullOrWhiteSpace(Model.AssignedWorkerName))
{
<div class="col-6">
<div class="info-row">
<div class="info-label">Assigned Worker</div>
<div class="info-value">@Model.AssignedWorkerName</div>
</div>
</div>
}
@if (Model.DueDate.HasValue)
{
<div class="col-6">
<div class="info-row">
<div class="info-label">Due Date</div>
<div class="info-value">
<strong>@Model.DueDate.Value.ToString("MM/dd/yyyy")</strong>
</div>
</div>
</div>
}
@if (Model.ScheduledDate.HasValue)
{
<div class="col-6">
<div class="info-row">
<div class="info-label">Scheduled Date</div>
<div class="info-value">@Model.ScheduledDate.Value.ToString("MM/dd/yyyy")</div>
</div>
</div>
}
</div>
</div>
</div>
<!-- Description -->
@if (!string.IsNullOrWhiteSpace(Model.Description))
{
<div class="compact-section">
<div class="info-row">
<div class="info-label">Job Description</div>
<div class="info-value">@Model.Description</div>
</div>
</div>
}
<!-- Preparation Services -->
@if (Model.PrepServices != null && Model.PrepServices.Any())
{
<div class="compact-section">
<div class="info-label">
<i class="bi bi-tools me-1"></i>Preparation Services Required
</div>
<div class="mt-1">
@foreach (var service in Model.PrepServices)
{
<span class="prep-service-badge">
<i class="bi bi-check-circle-fill me-1"></i>@service.ServiceName
</span>
}
</div>
</div>
}
<!-- Job Items -->
@if (Model.Items != null && Model.Items.Any())
{
<div class="section-title">
<i class="bi bi-list-ul me-1"></i>Items to be Coated
</div>
@for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
<div class="item-card">
<!-- Item Header -->
<div class="item-card-header">
<span style="font-size: 10pt; font-weight: 700; min-width: 24px;">#@(i + 1)</span>
<span style="font-size: 10pt; font-weight: 700; flex: 1;">@item.Description</span>
<span style="font-size: 8pt; white-space: nowrap;">
Qty: <strong>@item.Quantity</strong>
</span>
@if (item.SurfaceAreaSqFt > 0)
{
<span style="font-size: 8pt; white-space: nowrap;">
Area: <strong>@item.SurfaceAreaSqFt.ToString("N2") sq ft</strong>
</span>
}
else if (item.SurfaceArea.HasValue)
{
<span style="font-size: 8pt; white-space: nowrap;">
Area: <strong>@item.SurfaceArea.Value.ToString("N2") sq ft</strong>
</span>
}
</div>
<div class="item-card-body">
<div class="row">
<!-- Coating Layers -->
<div class="@((item.PrepServices != null && item.PrepServices.Any()) ? "col-8" : "col-12")">
@if (item.Coats != null && item.Coats.Any())
{
<div class="sub-label"><i class="bi bi-layers me-1"></i>Coating Layers</div>
<table class="coat-table">
<thead>
<tr>
<th style="width: 12%;">Layer</th>
<th style="width: 20%;">Coat Name</th>
<th style="width: 18%;">Color</th>
<th style="width: 10%;">Code</th>
<th style="width: 15%;">Finish</th>
<th style="width: 15%;">Vendor</th>
<th style="width: 10%;">Powder</th>
</tr>
</thead>
<tbody>
@foreach (var coat in item.Coats.OrderBy(c => c.Sequence))
{
var isCustom = !coat.InventoryItemId.HasValue && (coat.PowderCostPerLb.HasValue || coat.PowderToOrder.HasValue);
<tr>
<td style="text-align: center;">@coat.Sequence</td>
<td><strong>@coat.CoatName</strong></td>
<td>@Html.Raw(coat.ColorName ?? "&mdash;")</td>
<td>@Html.Raw(coat.ColorCode ?? "&mdash;")</td>
<td>@Html.Raw(coat.Finish ?? "&mdash;")</td>
<td>@Html.Raw(coat.VendorName ?? "&mdash;")</td>
<td>
@if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0)
{
if (isCustom)
{
<span class="order-badge">ORDER @coat.PowderToOrder.Value.ToString("N2") lbs</span>
}
else
{
<span>@coat.PowderToOrder.Value.ToString("N2") lbs</span>
}
}
else
{
<span class="text-muted">&mdash;</span>
}
</td>
</tr>
@if (!string.IsNullOrWhiteSpace(coat.Notes))
{
<tr>
<td></td>
<td colspan="6" style="font-size: 7.5pt; color: #6c757d; font-style: italic;">
<i class="bi bi-chat-left-text me-1"></i>@coat.Notes
</td>
</tr>
}
}
</tbody>
</table>
}
else
{
<span class="text-muted" style="font-size: 8pt;">No coat details specified.</span>
}
</div>
<!-- Prep Services -->
@if (item.PrepServices != null && item.PrepServices.Any())
{
<div class="col-4">
<div class="sub-label"><i class="bi bi-tools me-1"></i>Prep Services</div>
@foreach (var svc in item.PrepServices)
{
<div style="margin-bottom: 3px;">
<span class="prep-service-badge">
<i class="bi bi-check-circle-fill me-1"></i>@svc.PrepServiceName
</span>
@if (svc.EstimatedMinutes > 0)
{
<span style="font-size: 7.5pt; color: #6c757d; margin-left: 4px;">@svc.EstimatedMinutes min</span>
}
</div>
}
</div>
}
</div>
<!-- Item Notes -->
@if (!string.IsNullOrWhiteSpace(item.Notes))
{
<div style="margin-top: 6px; padding: 4px 8px; background: #fff3cd; border-left: 3px solid #ffc107; border-radius: 2px; font-size: 8pt;">
<strong>Notes:</strong> @item.Notes
</div>
}
</div>
</div>
}
}
<!-- Special Instructions -->
@if (!string.IsNullOrWhiteSpace(Model.SpecialInstructions))
{
<div class="compact-section">
<div class="section-title">
<i class="bi bi-exclamation-triangle me-1"></i>Special Instructions
</div>
<div class="special-instructions">
<pre style="white-space: pre-wrap; font-family: inherit; margin: 0;">@Model.SpecialInstructions</pre>
</div>
</div>
}
<!-- Signature Section -->
<div class="signature-section">
<div class="row">
<div class="col-4">
<div style="font-size: 8pt;">Worker Signature:</div>
<div class="signature-line"></div>
<div class="text-muted" style="font-size: 7pt;">Print Name: ___________________ Date: _______</div>
</div>
<div class="col-4">
<div style="font-size: 8pt;">Quality Control:</div>
<div class="signature-line"></div>
<div class="text-muted" style="font-size: 7pt;">Inspector: ___________________ Date: _______</div>
</div>
<div class="col-4">
<div style="font-size: 8pt;">Customer Approval:</div>
<div class="signature-line"></div>
<div class="text-muted" style="font-size: 7pt;">Signature: ___________________ Date: _______</div>
</div>
</div>
</div>
<!-- QR Codes Row: Status Bump + Powder Usage -->
@{
var powderQrCodes = ViewBag.PowderQrCodes as List<PowderQrCodeInfo>;
bool hasPowderQrs = powderQrCodes != null && powderQrCodes.Count > 0;
}
@if (ViewBag.QrCodeBase64 != null || hasPowderQrs)
{
<div style="margin-top: 14px; padding: 10px; border: 1px dashed #dee2e6; border-radius: 4px;">
<div style="font-size: 8pt; font-weight: 700; color: #6c757d; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 10px;">
<i class="bi bi-qr-code me-1"></i>Scan Codes
</div>
<div style="display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start;">
@* Status bump QR *@
@if (ViewBag.QrCodeBase64 != null)
{
<div style="display: flex; align-items: center; gap: 10px; min-width: 200px;">
<img src="data:image/png;base64,@ViewBag.QrCodeBase64"
alt="Status Update QR"
style="width: 80px; height: 80px; image-rendering: pixelated; flex-shrink: 0;" />
<div>
<div style="font-size: 8.5pt; font-weight: 700; margin-bottom: 3px;">
<i class="bi bi-arrow-right-circle me-1"></i>Update Status
</div>
<div style="font-size: 7.5pt; color: #6c757d; line-height: 1.5;">
Advance job to<br />next status.
</div>
</div>
</div>
}
@* Powder usage QRs &mdash; one per unique inventory item *@
@if (hasPowderQrs)
{
@foreach (var pqr in powderQrCodes!)
{
<div style="display: flex; align-items: center; gap: 10px; min-width: 200px;">
<img src="data:image/png;base64,@pqr.Base64"
alt="Powder Usage QR - @pqr.Name"
style="width: 80px; height: 80px; image-rendering: pixelated; flex-shrink: 0;" />
<div>
<div style="font-size: 8.5pt; font-weight: 700; margin-bottom: 2px;">
<i class="bi bi-box-seam me-1"></i>Log Usage
</div>
<div style="font-size: 8pt; font-weight: 600; margin-bottom: 1px;">@pqr.Name</div>
@if (!string.IsNullOrWhiteSpace(pqr.ColorCode))
{
<div style="font-size: 7.5pt; color: #6c757d;">Code: @pqr.ColorCode</div>
}
@if (!string.IsNullOrWhiteSpace(pqr.Manufacturer))
{
<div style="font-size: 7.5pt; color: #6c757d;">@pqr.Manufacturer</div>
}
@if (pqr.TotalLbs > 0)
{
<div style="font-size: 7.5pt; color: #0d6efd; font-weight: 600;">Est. @pqr.TotalLbs.ToString("N2") lbs</div>
}
</div>
</div>
}
}
</div>
</div>
}
<!-- Footer -->
<div class="footer-note">
Generated: @DateTime.Now.ToString("MM/dd/yyyy hh:mm tt")
</div>
</div>
</body>
</html>