Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Jobs/EditItems.cshtml
T
spouliot 2bf8871892 Fix NoExtraLayerCharge persistence, appointment reminders, coat notes display, scroll restoration, and invoice Send dead-button
- Appointment reminders: add AppointmentReminderBackgroundService (60s poll), ReminderSentAt
  dedup stamp, NotifyAppointmentReminderAsync sends both customer email and creator staff email;
  AppointmentReminderStaff notification type + default template added; DateTime.Now used instead
  of UtcNow to match locally-stored ScheduledStartTime; ToLocalTime() double-conversion removed

- NoExtraLayerCharge not persisted: flag existed on CreateQuoteItemCoatDto and was used by
  pricing engine but never written to JobItemCoat/QuoteItemCoat entities — every edit reset it
  to false and re-applied the extra layer charge; added column to both entities (migration
  AddNoExtraLayerChargeToCoats), both read DTOs, all 3 JobItemAssemblyService overloads,
  JobItemCoatSeed inner class, and existingItemsData JSON in all 5 wizard views; fixed JS
  template path that hard-coded noExtraLayerCharge: false

- Coat notes not visible: notes were rendered in desktop job details but missing from the wizard
  item card summary and the mobile card view; both fixed

- Scroll position lost on item save: sessionStorage save/restore added to item-wizard.js owner
  form submit handler; path-keyed so cross-page navigation does not restore stale position;
  requestAnimationFrame used for reliable mobile scroll restoration

- Invoice Send dead button: #sendChannelModal was gated inside @if (isDraft) but the button
  targeting it fires for Sent/Overdue invoices too when customer has both email and SMS; modal
  moved outside the Draft guard

- InitialCreate migration added for fresh database installs; Baseline migration guarded with
  IF OBJECT_ID check so it no-ops on fresh DBs; Razor scoping bug fixed in Customers/Index.cshtml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:48:16 -04:00

190 lines
8.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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" />
<input type="hidden" name="OvenCostId" value="@Model.OvenCostId" />
<input type="hidden" name="OvenBatches" value="@Model.OvenBatches" />
<input type="hidden" name="OvenCycleMinutes" value="@Model.OvenCycleMinutes" />
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger alert-permanent 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>
@await Html.PartialAsync("_SqFtCalculatorModal")
@await Html.PartialAsync("_ItemWizardModal")
<!-- 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,
isAiItem = item.IsAiItem,
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,
noExtraLayerCharge = c.NoExtraLayerCharge
}),
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": @Json.Serialize(Model.OvenCostId),
"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 {
<link rel="stylesheet" href="~/css/item-wizard.css">
}
@section Scripts {
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
}