4ec55e7290
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).
Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
5.4 KiB
Plaintext
118 lines
5.4 KiB
Plaintext
@using PowderCoating.Application.DTOs.Wizard
|
|
@model WizardPricingTiersStepDto
|
|
@{
|
|
ViewData["Title"] = "Setup Wizard — Pricing Tiers";
|
|
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
|
|
int step = ViewBag.Step as int? ?? 8;
|
|
}
|
|
@section Styles { @await Html.PartialAsync("_WizardStyles") }
|
|
|
|
<div class="wizard-layout">
|
|
@await Html.PartialAsync("_WizardProgress", progress)
|
|
|
|
<div class="wizard-content">
|
|
<div class="wizard-step-header">
|
|
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
|
|
<h2><i class="bi bi-percent me-2"></i>Pricing Tiers</h2>
|
|
<p class="text-secondary">Create discount tiers for your commercial customers. Once set up, you can assign a tier to any customer so their quotes automatically apply the discount.</p>
|
|
</div>
|
|
|
|
<form asp-action="PostStep8" method="post">
|
|
@Html.AntiForgeryToken()
|
|
<input type="hidden" name="TiersJson" id="tiersJson" value="@(Model.TiersJson ?? "[]")" />
|
|
|
|
<div class="wizard-card">
|
|
<h5 class="wizard-card-title">Pricing Tiers</h5>
|
|
<p class="text-secondary small mb-3">
|
|
Common examples: <em>Gold (10% off)</em>, <em>Silver (5% off)</em>, <em>Wholesale (15% off)</em>.
|
|
Customers without a tier are billed at standard rates.
|
|
</p>
|
|
|
|
<div id="tiersList"></div>
|
|
|
|
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addTier()">
|
|
<i class="bi bi-plus-circle me-1"></i>Add Pricing Tier
|
|
</button>
|
|
|
|
<div class="alert alert-info alert-permanent d-flex gap-2 mt-3 mb-0" role="alert">
|
|
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
|
|
<div class="small">
|
|
You can skip this step and configure pricing tiers later from <strong>Settings → Pricing Tiers</strong>.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@await Html.PartialAsync("_WizardFooter", step)
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
var tiers = JSON.parse(document.getElementById('tiersJson').value || '[]');
|
|
|
|
function renderTiers() {
|
|
var container = document.getElementById('tiersList');
|
|
if (tiers.length === 0) {
|
|
container.innerHTML = '<p class="text-secondary small py-2">No tiers added yet. You can skip this step and set up pricing tiers later from Settings → Pricing Tiers.</p>';
|
|
} else {
|
|
container.innerHTML = tiers.map(function (t, idx) {
|
|
return `<div class="wz-item-row">
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold mb-1">Tier Name <span class="text-danger">*</span></label>
|
|
<input class="form-control form-control-sm" value="${escHtml(t.tierName)}" onchange="updateTier(${idx},'tierName',this.value)" placeholder="e.g. Gold, Wholesale" />
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold mb-1">Description</label>
|
|
<input class="form-control form-control-sm" value="${escHtml(t.description)}" onchange="updateTier(${idx},'description',this.value)" placeholder="e.g. High-volume commercial accounts" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-semibold mb-1">Discount (%)</label>
|
|
<div class="input-group input-group-sm">
|
|
<input class="form-control form-control-sm" type="number" min="0" max="100" step="0.1" value="${t.discountPercent || 0}" onchange="updateTierNum(${idx},'discountPercent',this.value)" />
|
|
<span class="input-group-text">%</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2 text-end">
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeTier(${idx})" title="Remove">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
document.getElementById('tiersJson').value = JSON.stringify(tiers);
|
|
}
|
|
|
|
function addTier() {
|
|
tiers.push({ tierName: '', description: '', discountPercent: 0 });
|
|
renderTiers();
|
|
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
|
|
if (inputs.length) inputs[0].focus();
|
|
}
|
|
|
|
function updateTier(idx, field, value) {
|
|
tiers[idx][field] = value;
|
|
document.getElementById('tiersJson').value = JSON.stringify(tiers);
|
|
}
|
|
|
|
function updateTierNum(idx, field, value) {
|
|
tiers[idx][field] = value === '' ? 0 : parseFloat(value);
|
|
document.getElementById('tiersJson').value = JSON.stringify(tiers);
|
|
}
|
|
|
|
function removeTier(idx) {
|
|
tiers.splice(idx, 1);
|
|
renderTiers();
|
|
}
|
|
|
|
function escHtml(str) {
|
|
return (str || '').toString().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
renderTiers();
|
|
</script>
|
|
}
|