Initial commit
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
@model PowderCoating.Application.DTOs.Job.UpdateJobDto
|
||||
@using PowderCoating.Core.Entities
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Job";
|
||||
ViewData["PageIcon"] = "bi-pencil-square";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="d-flex justify-content-end mb-4">
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to Job
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form asp-action="Edit" method="post" id="jobEditForm">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<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 job list. Customer PO is the customer's own reference number — it appears on invoices. Special Instructions go directly to the shop floor worker on the work order.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label asp-for="CustomerId" class="form-label">Customer <span class="text-danger">*</span></label>
|
||||
<select asp-for="CustomerId" 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-4">
|
||||
<label asp-for="JobStatusId" class="form-label">Status
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Job Status"
|
||||
data-bs-content="Tracks where the job is in the workflow: Pending → Approved → Sandblasting → Cleaning → Coating → Curing → QualityCheck → Completed → ReadyForPickup → Delivered. Status changes trigger customer email notifications (if enabled). Use OnHold to pause work without losing progress.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
<select asp-for="JobStatusId" class="form-select" asp-items="@ViewBag.Statuses"></select>
|
||||
<span asp-validation-for="JobStatusId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="JobPriorityId" class="form-label">Priority
|
||||
<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>
|
||||
</label>
|
||||
<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">
|
||||
<label asp-for="ScheduledDate" class="form-label">Scheduled Date
|
||||
<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 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>
|
||||
</label>
|
||||
<input asp-for="ScheduledDate" type="date" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="DueDate" class="form-label">Due Date
|
||||
<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>
|
||||
</label>
|
||||
<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">
|
||||
<label asp-for="CustomerPO" class="form-label">Customer PO</label>
|
||||
<input asp-for="CustomerPO" class="form-control" placeholder="Enter PO number" />
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label asp-for="SpecialInstructions" class="form-label">Special Instructions</label>
|
||||
<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" 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>
|
||||
<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 through future edits.">
|
||||
<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 flex-wrap align-items-center justify-content-between gap-3">
|
||||
@{
|
||||
var sendEmailDefault = (bool)(ViewBag.EmailDefaultOnStatusChange ?? Model.SendEmailOnStatusChange);
|
||||
}
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailOnStatusChange" name="SendEmailOnStatusChange" value="true"
|
||||
checked="@(sendEmailDefault ? "checked" : null)" />
|
||||
<label class="form-check-label small" for="SendEmailOnStatusChange">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer if status changes
|
||||
</label>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" 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 Changes
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Existing items pre-fill -->
|
||||
<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": @(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
|
||||
});
|
||||
});
|
||||
|
||||
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';
|
||||
}
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user