Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,309 @@
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@using PowderCoating.Core.Entities
@{
ViewData["Title"] = $"Edit Items — {Model.JobNumber}";
ViewData["PageIcon"] = "bi-list-check";
}
<div class="container-fluid mt-4">
<div class="d-flex justify-content-end mb-4">
<a asp-action="Details" asp-route-id="@Model.JobId" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Job
</a>
</div>
<form asp-action="UpdateItems" asp-controller="Jobs" method="post" id="jobItemsForm">
@Html.AntiForgeryToken()
<input type="hidden" name="JobId" value="@Model.JobId" />
<input type="hidden" name="JobNumber" value="@Model.JobNumber" />
<input type="hidden" name="CustomerId" value="@Model.CustomerId" />
<input type="hidden" name="TaxPercent" value="@Model.TaxPercent" />
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger mb-4" role="alert">
<ul class="mb-0">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<li>@error.ErrorMessage</li>
}
</ul>
</div>
}
<!-- Items Card -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="mb-0"><i class="bi bi-list-ul me-2"></i>Job Items</h5>
<button type="button" class="btn btn-primary" onclick="openWizard()">
<i class="bi bi-plus-circle me-1"></i>Add Item
</button>
</div>
<div class="card-body">
<div id="itemsEmptyMessage" class="text-center py-5 text-muted" style="display: none;">
<i class="bi bi-box-seam display-4 d-block mb-2 opacity-25"></i>
<p class="mb-0">No items added yet.</p>
<p class="small">Click <strong>Add Item</strong> to get started.</p>
</div>
<div id="itemCardsContainer"></div>
<!-- Hidden fields written by wizard JS -->
<div id="hiddenFieldsContainer"></div>
</div>
</div>
<!-- Pricing Summary -->
<div class="card mb-4">
<div class="card-header bg-primary text-white d-flex align-items-center justify-content-between">
<h5 class="mb-0"><i class="bi bi-calculator me-2"></i>Pricing Summary</h5>
<span id="pricingSpinner" class="spinner-border spinner-border-sm text-white d-none" role="status"></span>
</div>
<div class="card-body">
<div class="text-end" id="pricingSummary">
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
Tier Discount (<span id="pricingTierDiscountPercentDisplay">0</span>%):
<strong id="pricingTierDiscountDisplay">-$0.00</strong>
</p>
<p class="mb-1 d-none" id="shopSuppliesRow">Shop Supplies (<span id="shopSuppliesPercentDisplay">0</span>%): <strong id="shopSuppliesDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="subtotalRow">Subtotal: <strong id="subtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="taxRow">Tax (<span id="taxPercentDisplay">0</span>%): <strong id="taxDisplay">$0.00</strong></p>
<hr class="my-2 d-none" id="pricingDivider" />
<h5 class="mb-0 d-none" id="totalRow">Total: <strong id="totalDisplay" class="text-primary">$0.00</strong></h5>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="card mb-4">
<div class="card-body d-flex align-items-center justify-content-end gap-3">
<a asp-action="Details" asp-route-id="@Model.JobId" class="btn btn-outline-secondary btn-lg">
<i class="bi bi-x-circle me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn">
<i class="bi bi-check-circle me-1"></i>Save Items
</button>
</div>
</div>
</form>
</div>
<!-- Surface Area Calculator Modal -->
<div class="modal fade" id="sqFtCalculatorModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-calculator me-2"></i>Surface Area Calculator <small class="text-muted">(per item)</small></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Shape</label>
<select id="calcShape" class="form-select" onchange="toggleShapeInputs()">
<option value="rectangle">Rectangle / Square</option>
<option value="cylinder">Cylinder (Tube)</option>
<option value="circle">Circle (Flat)</option>
</select>
</div>
<div id="rectangleInputs">
<div class="row g-2">
<div class="col-6"><label class="form-label">Length (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectLength" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div>
<div id="cylinderInputs" style="display:none">
<div class="row g-2">
<div class="col-6"><label class="form-label">Diameter (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="cylDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
<div class="col-6"><label class="form-label">Height (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="cylHeight" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
</div>
<div id="circleInputs" style="display:none">
<label class="form-label">Diameter (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</button>
</div>
</div>
</div>
</div>
<!-- ========================= ITEM WIZARD MODAL ========================= -->
<div class="modal fade" id="itemWizardModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div class="d-flex flex-column">
<h5 class="modal-title mb-0" id="wizardTitle">Add Item</h5>
<div class="text-muted small mb-1" id="wizardStepTitle">Choose Item Type</div>
<div class="d-flex align-items-center gap-2" id="wizardStepIndicator">
<span class="wizard-step-dot active" data-step="1" title="Item Type"></span>
<div class="wizard-step-line"></div>
<span class="wizard-step-dot" data-step="2" title="Item Details"></span>
<div class="wizard-step-line" id="step2Line"></div>
<span class="wizard-step-dot" data-step="3" title="Coating Layers" id="step3Dot"></span>
<div class="wizard-step-line" id="step3Line"></div>
<span class="wizard-step-dot" data-step="4" title="Prep Services" id="step4Dot"></span>
<span class="text-muted small ms-2" id="wizardStepLabel">Step 1 of 4</span>
</div>
</div>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="wizardBody" style="min-height: 300px;">
<!-- Content injected by JS -->
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary d-none" id="btnWizardBack" onclick="wizardBack()">
<i class="bi bi-arrow-left me-1"></i>Back
</button>
<button type="button" class="btn btn-primary" id="btnWizardNext" onclick="wizardNext()">
Next <i class="bi bi-arrow-right ms-1"></i>
</button>
<button type="button" class="btn btn-success d-none" id="btnWizardSave" onclick="wizardSave()">
<i class="bi bi-check-lg me-1"></i>Add Item
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Embedded data for JS -->
@if (ViewBag.InventoryCoatings != null)
{
<script id="inventoryPowdersData" type="application/json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.InventoryCoatings))
</script>
}
@if (ViewBag.CatalogItems != null)
{
<script id="catalogItemsData" type="application/json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CatalogItems))
</script>
}
<script id="vendorsData" type="application/json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.Vendors ?? new List<object>()))
</script>
<script id="prepServicesData" type="application/json">
@Html.Raw(Json.Serialize(((List<PrepService>)(ViewBag.PrepServices ?? new List<PrepService>())).Select(p => new { id = p.Id, name = p.ServiceName, description = p.Description, requiresBlastSetup = p.RequiresBlastSetup })))
</script>
<script id="blastSetupsData" type="application/json">
@Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>()))
</script>
<!-- Existing items -->
<script id="existingItemsData" type="application/json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize((Model.JobItems ?? new List<PowderCoating.Application.DTOs.Quote.CreateQuoteItemDto>()).Select(item => new {
description = item.Description,
quantity = item.Quantity,
surfaceAreaSqFt = item.SurfaceAreaSqFt,
estimatedMinutes = item.EstimatedMinutes,
catalogItemId = item.CatalogItemId,
manualUnitPrice = item.ManualUnitPrice,
powderCostOverride = item.PowderCostOverride,
complexity = item.Complexity,
isGenericItem = item.IsGenericItem,
isLaborItem = item.IsLaborItem,
requiresSandblasting = item.RequiresSandblasting,
requiresMasking = item.RequiresMasking,
notes = item.Notes,
includePrepCost = item.IncludePrepCost,
coats = item.Coats.Select(c => new {
coatName = c.CoatName,
sequence = c.Sequence,
inventoryItemId = c.InventoryItemId,
colorName = c.ColorName,
vendorId = c.VendorId,
colorCode = c.ColorCode,
finish = c.Finish,
coverageSqFtPerLb = c.CoverageSqFtPerLb,
transferEfficiency = c.TransferEfficiency,
powderCostPerLb = c.PowderCostPerLb,
powderToOrder = c.PowderToOrder,
notes = c.Notes
}),
prepServices = item.PrepServices.Select(ps => new {
prepServiceId = ps.PrepServiceId,
estimatedMinutes = ps.EstimatedMinutes,
blastSetupId = ps.BlastSetupId
})
})))
</script>
<script id="quoteMetaData" type="application/json">
{
"customerId": @Json.Serialize(Model.CustomerId),
"taxPercent": @Model.TaxPercent,
"discountType": "None",
"discountValue": 0,
"isRushJob": false,
"ovenCostId": null,
"areaUnit": @Json.Serialize((string?)ViewBag.AreaUnit),
"useMetric": @Json.Serialize((bool)(ViewBag.UseMetric ?? false)),
"pricingUrl": "@Url.Action("CalculatePricing", "Jobs")",
"itemsFieldPrefix": "JobItems",
"aiRecalcUrl": "@Url.Action("AiRecalcPrice", "Quotes")"
}
</script>
@section Styles {
<style>
/* Wizard step indicator */
.wizard-step-dot {
width: 22px; height: 22px; border-radius: 50%;
background: #dee2e6; display: inline-block; cursor: default;
border: 2px solid #dee2e6; transition: all .2s;
flex-shrink: 0;
}
.wizard-step-dot.active { background: #0d6efd; border-color: #0d6efd; }
.wizard-step-dot.done { background: #198754; border-color: #198754; }
.wizard-step-dot.skip { background: #adb5bd; border-color: #adb5bd; }
.wizard-step-line { flex: 1; height: 2px; background: #dee2e6; min-width: 30px; }
/* Item type picker cards */
.item-type-card {
border: 2px solid #dee2e6; border-radius: .75rem; padding: 1.25rem 1rem;
cursor: pointer; transition: all .15s; text-align: center;
background: #fff; user-select: none;
}
.item-type-card:hover { border-color: #86b7fe; background: #f0f6ff; }
.item-type-card.selected { border-color: #0d6efd; background: #eef3ff; }
.item-type-card .item-type-icon { font-size: 2rem; margin-bottom: .5rem; }
.catalog-list-item { cursor: pointer; border-bottom: 1px solid var(--bs-border-color); font-size: .9rem; transition: background .1s; }
.catalog-list-item:last-child { border-bottom: none; }
.catalog-list-item:hover { background: var(--bs-tertiary-bg); }
.catalog-list-item.selected { background: #eef3ff; color: #0d6efd; font-weight: 600; }
[data-bs-theme="dark"] .catalog-list-item.selected { background: #1a2a4a; color: #86b7fe; }
/* Summary cards */
.quote-item-card {
border: 1px solid #dee2e6; border-radius: .5rem;
padding: .75rem 1rem; margin-bottom: .5rem;
background: #fafafa;
}
.quote-item-card .item-badge { font-size: .7rem; }
/* Coat rows in wizard */
.coat-row { border: 1px solid #dee2e6; border-radius: .5rem; padding: .75rem; margin-bottom: .5rem; }
</style>
}
@section Scripts {
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
}