561 lines
33 KiB
Plaintext
561 lines
33 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()
|
||
@if (ViewBag.TemplateId != null)
|
||
{
|
||
<input type="hidden" name="SourceTemplateId" value="@ViewBag.TemplateId">
|
||
}
|
||
<partial name="_ValidationSummary" />
|
||
|
||
<!-- 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="hiddenFieldsContainer"></div>
|
||
<div id="aiPhotoTempIdsContainer"></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>
|
||
|
||
<!-- 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;"></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="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,
|
||
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 == 0 ? (int?)null : (int?)Model.CustomerId),
|
||
"taxPercent": @(ViewBag.TaxPercent ?? 0),
|
||
"discountType": @Json.Serialize(Model.DiscountType),
|
||
"discountValue": @Model.DiscountValue,
|
||
"isRushJob": @Json.Serialize(Model.IsRushJob),
|
||
"ovenCostId": null,
|
||
"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)),
|
||
"itemsFieldPrefix": "JobItems",
|
||
"aiRecalcUrl": "@Url.Action("AiRecalcPrice", "Quotes")"
|
||
}
|
||
</script>
|
||
|
||
@section Styles {
|
||
<link rel="stylesheet" href="~/lib/tom-select/css/tom-select.bootstrap5.min.css">
|
||
<style>
|
||
.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-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; }
|
||
[data-bs-theme="dark"] .item-type-card { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); color: var(--bs-body-color); }
|
||
[data-bs-theme="dark"] .item-type-card:hover { border-color: #86b7fe; background: var(--bs-secondary-bg); }
|
||
[data-bs-theme="dark"] .item-type-card.selected { border-color: #0d6efd; background: #1a2a4a; }
|
||
.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; }
|
||
.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-row { border: 1px solid #dee2e6; border-radius: .5rem; padding: .75rem; margin-bottom: .5rem; }
|
||
[data-bs-theme="dark"] .quote-item-card { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); color: var(--bs-body-color); }
|
||
[data-bs-theme="dark"] .coat-row { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); }
|
||
</style>
|
||
}
|
||
|
||
@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>
|
||
}
|