Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Jobs/Create.cshtml
T
spouliot df504674e9 Add oven/batch settings to job create and edit forms
CreateJobDto and UpdateJobDto now carry OvenCostId, OvenBatches, and
OvenCycleMinutes. The Create POST sets these on the new Job entity and
passes them to the pricing engine; the Edit GET populates them from the
existing job so the form reflects saved values, and the Edit POST writes
them back before repricing.

Both Jobs/Create.cshtml and Jobs/Edit.cshtml now include an Oven & Batch
Settings card (matching the quote form) with oven selector, batch count,
and cycle time inputs. The wizard init block now passes the selected
OvenCostId instead of null so live auto-pricing reflects the oven cost.

ViewBag.DefaultOvenCycleMinutes added to PopulateCreateEditWizardViewBagsAsync
so the placeholder in both views shows the company default.

Also fixed: NoExtraLayerCharge was missing from the Edit GET coat DTO
mapping (would have caused the flag to reset to false on next edit).

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

492 lines
29 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.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 &times; 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>
<!-- 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 &amp; 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 &amp; 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 &amp;mdash; for example, if you have 20 small parts and your oven fits 10, that&amp;#39;s 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven&amp;#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,
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 == 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)),
"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>
}