a7ad0e1de8
Custom powder/incoming powder material cost now flows into a separate auto-generated 'Custom Powder Order' line item instead of rolling into individual item prices, so users can add shipping charges before the customer sees the total. A dashed yellow preview card in the wizard shows the material cost and lets users edit the total (including shipping) before saving. After first save the price is user-owned. Also fixes a fatal CSV import crash when FinalPrice contains a non-numeric value (e.g. 'false' from a spreadsheet formula): the job CSV importer now streams rows one at a time with a lenient decimal converter, treating bad values as $0 with a per-row warning instead of aborting the entire import. Updated HelpKnowledgeBase.cs and Help articles (Jobs, Quotes) with Custom Powder Order behavior and a new Data Import / Export section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
521 lines
31 KiB
Plaintext
521 lines
31 KiB
Plaintext
@model PowderCoating.Application.DTOs.Job.CreateJobDto
|
||
@using PowderCoating.Core.Entities
|
||
|
||
@{
|
||
ViewData["Title"] = "Create Job";
|
||
ViewData["PageIcon"] = "bi-plus-circle";
|
||
}
|
||
|
||
<div class="container-fluid mt-4">
|
||
<div class="d-flex justify-content-end mb-4">
|
||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||
<i class="bi bi-arrow-left me-1"></i>Back to List
|
||
</a>
|
||
</div>
|
||
|
||
@if (ViewBag.TemplateName != null)
|
||
{
|
||
<div class="alert alert-primary alert-dismissible d-flex align-items-center gap-2 mb-4" role="alert">
|
||
<i class="bi bi-layout-text-window-reverse fs-5"></i>
|
||
<div>
|
||
Pre-filled from template <strong>@ViewBag.TemplateName</strong>.
|
||
Items and coatings have been loaded — review and adjust before saving.
|
||
</div>
|
||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||
</div>
|
||
}
|
||
|
||
<form asp-action="Create" method="post" id="jobCreateForm">
|
||
@Html.AntiForgeryToken()
|
||
<input type="hidden" name="guidedActivation" value="@ViewBag.GuidedActivation" />
|
||
@if (ViewBag.TemplateId != null)
|
||
{
|
||
<input type="hidden" name="SourceTemplateId" value="@ViewBag.TemplateId">
|
||
}
|
||
<partial name="_ValidationSummary" />
|
||
|
||
@if ((ViewBag.GuidedActivation as string) == PowderCoating.Shared.Constants.AppConstants.GuidedActivation.JobFirstPath)
|
||
{
|
||
<div class="alert alert-primary alert-permanent border-0 shadow-sm mb-4">
|
||
<div class="fw-semibold mb-1">Step 1: Create your first sample job</div>
|
||
<div>We've prefilled a quick example. You can edit anything before saving.</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Job Details Card -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Job Details</h5>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Job Details"
|
||
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and affect how work is sorted. Customer PO is the customer's own reference number for their purchase order — include it so it appears on invoices. Special Instructions go directly to the shop floor worker.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label asp-for="CustomerId" class="form-label">Customer <span class="text-danger">*</span></label>
|
||
<select asp-for="CustomerId" class="form-select" asp-items="@ViewBag.Customers" id="customerSelect">
|
||
<option value="">-- Select Customer --</option>
|
||
</select>
|
||
<span asp-validation-for="CustomerId" class="text-danger"></span>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="d-flex align-items-center gap-1">
|
||
<label asp-for="JobPriorityId" class="form-label mb-0">Priority</label>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Job Priority"
|
||
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
<select asp-for="JobPriorityId" class="form-select" asp-items="@ViewBag.Priorities"></select>
|
||
<span asp-validation-for="JobPriorityId" class="text-danger"></span>
|
||
</div>
|
||
<div class="col-12">
|
||
<label asp-for="Description" class="form-label">Description <span class="text-danger">*</span></label>
|
||
<textarea asp-for="Description" class="form-control" rows="3" placeholder="Enter job description"></textarea>
|
||
<span asp-validation-for="Description" class="text-danger"></span>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="d-flex align-items-center gap-1">
|
||
<label asp-for="ScheduledDate" class="form-label mb-0">Scheduled Date</label>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Scheduled Date"
|
||
data-bs-content="The date the shop plans to start work on this job. Used for planning and the shop floor board. Does not affect customer-facing documents.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
<input asp-for="ScheduledDate" type="date" class="form-control" />
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="d-flex align-items-center gap-1">
|
||
<label asp-for="DueDate" class="form-label mb-0">Due Date</label>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Due Date"
|
||
data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
<input asp-for="DueDate" type="date" class="form-control" />
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label asp-for="AssignedUserId" class="form-label">Assigned Worker</label>
|
||
<select asp-for="AssignedUserId" class="form-select" asp-items="@ViewBag.Workers">
|
||
<option value="">Not assigned</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="d-flex align-items-center gap-1">
|
||
<label asp-for="CustomerPO" class="form-label mb-0">Customer PO</label>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Customer PO"
|
||
data-bs-content="The customer's Purchase Order number from their own system. This prints on invoices and quotes so the customer can match it to their records. Required by many commercial customers for payment processing.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
<input asp-for="CustomerPO" class="form-control" placeholder="Enter PO number" />
|
||
</div>
|
||
<div class="col-md-7">
|
||
<div class="d-flex align-items-center gap-1">
|
||
<label asp-for="SpecialInstructions" class="form-label mb-0">Special Instructions</label>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Special Instructions"
|
||
data-bs-content="Free-text notes visible to the shop floor worker on the work order. Use this for masking requirements, handling notes, customer preferences, or anything that doesn't fit in the item-level notes — e.g., 'Keep brackets separated, customer allergic to zinc primer'.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
<textarea asp-for="SpecialInstructions" class="form-control" rows="3" placeholder="Any special instructions"></textarea>
|
||
</div>
|
||
<div class="col-md-5">
|
||
<label class="form-label fw-semibold">Tags <span class="badge rounded-pill bg-warning text-dark ms-1" style="font-size:0.65rem;">Recommended</span></label>
|
||
<p class="text-muted small mb-1">Tags are used to improve smart predictions and assistance over time. The more consistently you tag, the smarter the system gets.</p>
|
||
<input type="hidden" asp-for="Tags" id="jobTags" />
|
||
<div id="jobTagsContainer"></div>
|
||
<small class="text-muted">Press <kbd>Enter</kbd> or <kbd>,</kbd> to add a tag. Click × to remove.</small>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="form-check mt-2">
|
||
<input asp-for="RequiresCustomerApproval" class="form-check-input" type="checkbox" />
|
||
<label class="form-check-label" asp-for="RequiresCustomerApproval">
|
||
Requires Customer Approval Before Starting
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preparation Services -->
|
||
@if (ViewBag.PrepServices != null && ((List<PrepService>)ViewBag.PrepServices).Any())
|
||
{
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<h5 class="mb-0"><i class="bi bi-tools me-2"></i>Preparation Services</h5>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Preparation Services"
|
||
data-bs-content="Job-level prep services that apply to all items (e.g., full-job sandblasting, chemical strip). For prep that applies to only one item, use the per-item prep settings in the item wizard instead.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
@foreach (var service in (List<PrepService>)ViewBag.PrepServices)
|
||
{
|
||
var isChecked = Model.PrepServiceIds != null && Model.PrepServiceIds.Contains(service.Id);
|
||
<div class="col-sm-6 col-lg-4">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" name="PrepServiceIds" value="@service.Id" id="prepService_@service.Id" @(isChecked ? "checked" : "")>
|
||
<label class="form-check-label" for="prepService_@service.Id">
|
||
<strong>@service.ServiceName</strong>
|
||
@if (!string.IsNullOrWhiteSpace(service.Description))
|
||
{
|
||
<small class="text-muted d-block">@service.Description</small>
|
||
}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Items Card -->
|
||
<div class="card mb-4">
|
||
<div class="card-header d-flex align-items-center justify-content-between">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<h5 class="mb-0"><i class="bi bi-list-ul me-2"></i>Job Items</h5>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Job Items"
|
||
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
<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">
|
||
<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>
|
||
<div id="customPowderOrderPreview" class="d-none mt-2">
|
||
<div class="quote-item-card border-warning" style="border-style:dashed!important;background:var(--bs-warning-bg-subtle,#fff8e1);">
|
||
<div class="d-flex align-items-start gap-2">
|
||
<div class="flex-grow-1">
|
||
<div class="d-flex align-items-center flex-wrap gap-1 mb-1">
|
||
<span class="badge bg-warning text-dark item-badge"><i class="bi bi-truck me-1"></i>Powder Order</span>
|
||
<span class="fw-semibold" id="customPowderOrderPreviewDesc">Custom Powder Order</span>
|
||
</div>
|
||
<div class="text-muted small"><span id="customPowderOrderPreviewPrice"></span> — edit total to include shipping</div>
|
||
</div>
|
||
<div class="text-center flex-shrink-0" style="min-width:45px;">1</div>
|
||
<div class="flex-shrink-0" style="min-width:90px;">
|
||
<input type="number" id="customPowderOrderPriceInput"
|
||
class="form-control form-control-sm text-end"
|
||
min="0" step="0.01" placeholder="0.00"
|
||
title="Enter total including any shipping"
|
||
oninput="onCustomPowderPriceEdit(this.value)">
|
||
</div>
|
||
<div style="min-width:66px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="hiddenFieldsContainer"></div>
|
||
<div id="aiPhotoTempIdsContainer"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Oven & Batch Settings -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<h5 class="mb-0"><i class="bi bi-thermometer-half me-2"></i>Oven & Batch Settings</h5>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Oven & Batch Pricing"
|
||
data-bs-content="The oven cost is charged once per batch at the job level, not per item. Estimate how many oven loads the full job will fill &mdash; for example, if you have 20 small parts and your oven fits 10, that&#39;s 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven&#39;s hourly rate in Settings.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-muted small mb-3">Estimate how many oven loads the complete job will require. The oven cycle cost is added once at the job level, not per item.</p>
|
||
<div class="row g-3 align-items-end">
|
||
@if (ViewBag.OvenCosts != null && ((List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.OvenCosts).Count > 1)
|
||
{
|
||
<div class="col-md-4">
|
||
<label asp-for="OvenCostId" class="form-label fw-semibold"><i class="bi bi-thermometer-half me-1"></i>Oven</label>
|
||
<select asp-for="OvenCostId" asp-items="@((List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.OvenCosts)"
|
||
class="form-select" onchange="scheduleAutoPricing()"></select>
|
||
</div>
|
||
}
|
||
<div class="col-sm-3 col-md-2">
|
||
<label asp-for="OvenBatches" class="form-label fw-semibold">Batches</label>
|
||
<input asp-for="OvenBatches" class="form-control" type="number" min="1"
|
||
value="@(Model.OvenBatches > 0 ? Model.OvenBatches : 1)"
|
||
id="OvenBatches" onchange="scheduleAutoPricing()" />
|
||
</div>
|
||
<div class="col-sm-4 col-md-3">
|
||
<label asp-for="OvenCycleMinutes" class="form-label fw-semibold">
|
||
Cycle Time per Batch (min)
|
||
<small class="text-muted fw-normal">default: @(ViewBag.DefaultOvenCycleMinutes ?? 45)</small>
|
||
</label>
|
||
<input asp-for="OvenCycleMinutes" class="form-control" type="number" min="1"
|
||
id="OvenCycleMinutes" placeholder="@(ViewBag.DefaultOvenCycleMinutes ?? 45)"
|
||
onchange="scheduleAutoPricing()" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pricing Options (Rush / Discount) -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<h5 class="mb-0"><i class="bi bi-tag me-2"></i>Pricing Options <small class="text-muted fw-normal">(optional)</small></h5>
|
||
<a tabindex="0" class="help-icon" role="button"
|
||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||
data-bs-title="Pricing Options"
|
||
data-bs-content="Apply a rush surcharge or a one-off discount to this job. Tier discounts for the customer are applied automatically. These settings are preserved if the job is edited later.">
|
||
<i class="bi bi-question-circle"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input asp-for="IsRushJob" class="form-check-input" type="checkbox" id="IsRushJob" onchange="scheduleAutoPricing()">
|
||
<label class="form-check-label" for="IsRushJob">
|
||
<strong>Rush Job</strong> <small class="text-muted">(additional fee applies)</small>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<label asp-for="DiscountType" class="form-label fw-semibold">Discount Type</label>
|
||
<select asp-for="DiscountType" class="form-select" id="discountTypeSelect" onchange="onDiscountTypeChange(); scheduleAutoPricing()">
|
||
<option value="None">No Discount</option>
|
||
<option value="Percentage">Percentage (%)</option>
|
||
<option value="FixedAmount">Fixed Amount ($)</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4" id="discountValueSection" style="display: @(Model.DiscountType == "None" ? "none" : "block")">
|
||
<label asp-for="DiscountValue" class="form-label fw-semibold">Discount Value</label>
|
||
<input asp-for="DiscountValue" type="number" class="form-control" id="discountValueInput"
|
||
min="0" step="0.01" onchange="scheduleAutoPricing()" />
|
||
</div>
|
||
<div class="col-md-4" id="discountReasonSection" style="display: @(Model.DiscountType == "None" ? "none" : "block")">
|
||
<label asp-for="DiscountReason" class="form-label fw-semibold">Reason</label>
|
||
<input asp-for="DiscountReason" class="form-control" />
|
||
</div>
|
||
</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 text-success d-none" id="quoteDiscountRow">
|
||
Discount (<span id="quoteDiscountPercentDisplay">0</span>%):
|
||
<strong id="quoteDiscountDisplay">-$0.00</strong>
|
||
</p>
|
||
<p class="mb-1 text-warning d-none" id="rushFeeRow">
|
||
<i class="bi bi-lightning-fill me-1"></i>Rush Fee: <strong id="rushFeeDisplay">$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="Index" 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>Create Job
|
||
</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="merchandiseItemsData" type="application/json">
|
||
@Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.MerchandiseItems ?? new List<object>()))
|
||
</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>
|
||
|
||
<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,
|
||
isSalesItem = item.IsSalesItem,
|
||
isAiItem = item.IsAiItem,
|
||
isCustomFormulaItem = item.IsCustomFormulaItem,
|
||
customItemTemplateId = item.CustomItemTemplateId,
|
||
formulaFieldValuesJson = item.FormulaFieldValuesJson,
|
||
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,
|
||
supplierId = 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 == 0 ? (int?)null : (int?)Model.CustomerId),
|
||
"taxPercent": @(ViewBag.TaxPercent ?? 0),
|
||
"discountType": @Json.Serialize(Model.DiscountType),
|
||
"discountValue": @Model.DiscountValue,
|
||
"isRushJob": @Json.Serialize(Model.IsRushJob),
|
||
"ovenCostId": @Json.Serialize(Model.OvenCostId),
|
||
"areaUnit": @Json.Serialize((string?)ViewBag.AreaUnit),
|
||
"useMetric": @Json.Serialize((bool)(ViewBag.UseMetric ?? false)),
|
||
"pricingUrl": "@Url.Action("CalculatePricing", "Jobs")",
|
||
"aiUploadUrl": "@Url.Action("UploadAiPhoto", "Quotes")",
|
||
"aiAnalyzeUrl": "@Url.Action("AiAnalyzeItem", "Quotes")",
|
||
"aiPhotoQuotesEnabled": @Json.Serialize((bool)(ViewBag.AiPhotoQuotesEnabled ?? true)),
|
||
"customFormulaTemplates": @Json.Serialize(ViewBag.CustomFormulaTemplates ?? new List<object>()),
|
||
"formulaEvalUrl": "@Url.Action("EvaluateFormula", "CompanySettings")",
|
||
"itemsFieldPrefix": "JobItems",
|
||
"aiRecalcUrl": "@Url.Action("AiRecalcPrice", "Quotes")"
|
||
}
|
||
</script>
|
||
|
||
@section Styles {
|
||
<link rel="stylesheet" href="~/lib/tom-select/css/tom-select.bootstrap5.min.css">
|
||
<link rel="stylesheet" href="~/css/item-wizard.css">
|
||
}
|
||
|
||
@section Scripts {
|
||
<script src="~/lib/tom-select/js/tom-select.complete.min.js"></script>
|
||
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
initTagInput('jobTags', 'jobTagsContainer');
|
||
new TomSelect('#customerSelect', {
|
||
placeholder: '-- Select Customer --',
|
||
openOnFocus: true,
|
||
maxOptions: false
|
||
});
|
||
});
|
||
|
||
// Update customerId in pageMeta when customer dropdown changes
|
||
document.getElementById('customerSelect')?.addEventListener('change', function () {
|
||
if (typeof pageMeta !== 'undefined') {
|
||
pageMeta.customerId = this.value ? parseInt(this.value) : null;
|
||
}
|
||
});
|
||
|
||
function onDiscountTypeChange() {
|
||
const type = document.getElementById('discountTypeSelect').value;
|
||
const show = type !== 'None';
|
||
document.getElementById('discountValueSection').style.display = show ? 'block' : 'none';
|
||
document.getElementById('discountReasonSection').style.display = show ? 'block' : 'none';
|
||
}
|
||
|
||
@if (ViewBag.TemplateJson != null)
|
||
{
|
||
<text>
|
||
// Pre-populate items from template
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const template = @Html.Raw(ViewBag.TemplateJson);
|
||
if (template && template.items && typeof loadItemsFromTemplate === 'function') {
|
||
loadItemsFromTemplate(template.items);
|
||
}
|
||
});
|
||
</text>
|
||
}
|
||
</script>
|
||
}
|