Restore all zeroed views + add bulk gift certificate creation

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>
This commit is contained in:
2026-05-14 20:09:22 -04:00
parent 3eda91f170
commit 4ec55e7290
240 changed files with 73116 additions and 0 deletions
@@ -0,0 +1,183 @@
@{
ViewData["Title"] = "Delete Account";
ViewData["PageIcon"] = "bi-trash3-fill";
var companyName = ViewBag.CompanyName as string ?? "your company";
var userCount = (int)(ViewBag.UserCount ?? 0);
var jobCount = (int)(ViewBag.JobCount ?? 0);
var quoteCount = (int)(ViewBag.QuoteCount ?? 0);
var customerCount = (int)(ViewBag.CustomerCount ?? 0);
var invoiceCount = (int)(ViewBag.InvoiceCount ?? 0);
}
<div class="container" style="max-width:720px;">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a asp-controller="CompanySettings" asp-action="Index">Company Settings</a>
</li>
<li class="breadcrumb-item active">Delete Account</li>
</ol>
</nav>
<!-- Error alert -->
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-permanent" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>@TempData["Error"]
</div>
}
<!-- Header -->
<div class="d-flex align-items-center gap-3 mb-4">
<div class="bg-danger bg-opacity-10 rounded-circle p-3">
<i class="bi bi-trash3-fill text-danger fs-3"></i>
</div>
<div>
<p class="text-muted mb-0">This action is permanent and cannot be undone.</p>
</div>
</div>
<!-- What happens card -->
<div class="card border-danger mb-4">
<div class="card-header bg-danger text-white fw-semibold">
<i class="bi bi-exclamation-triangle-fill me-2"></i>What happens when you delete your account
</div>
<div class="card-body">
<p class="mb-3">
Deleting your account will permanently deactivate access to
<strong>@companyName</strong> and mark the following data for removal:
</p>
<ul class="list-unstyled mb-3">
<li class="mb-2">
<i class="bi bi-person-x-fill text-danger me-2"></i>
<strong>@userCount user account@(userCount != 1 ? "s" : "")</strong> will be deactivated — no one will be able to log in.
</li>
<li class="mb-2">
<i class="bi bi-briefcase-fill text-danger me-2"></i>
<strong>@jobCount job@(jobCount != 1 ? "s" : "")</strong>,
<strong>@quoteCount quote@(quoteCount != 1 ? "s" : "")</strong>, and
<strong>@invoiceCount invoice@(invoiceCount != 1 ? "s" : "")</strong> will be marked deleted.
</li>
<li class="mb-2">
<i class="bi bi-people-fill text-danger me-2"></i>
<strong>@customerCount customer record@(customerCount != 1 ? "s" : "")</strong> will be marked deleted.
</li>
<li class="mb-2">
<i class="bi bi-cloud-slash-fill text-danger me-2"></i>
All inventory, equipment, maintenance, and other company data will be marked deleted.
</li>
</ul>
<div class="alert alert-warning alert-permanent mb-0">
<i class="bi bi-lock-fill me-2"></i>
<strong>You will lose access to your data immediately.</strong>
Once your account is deleted you will no longer be able to export, download,
or access any of your data. All records are subject to purge and
<strong>cannot be recovered</strong> after deletion.
If you need a data export, please do so <em>before</em> deleting your account.
</div>
</div>
</div>
<!-- Alternatives card -->
<div class="card mb-4">
<div class="card-header fw-semibold">
<i class="bi bi-lightbulb me-2 text-warning"></i>Consider these alternatives first
</div>
<div class="card-body">
<ul class="mb-0">
<li class="mb-1">
<strong>Pause instead of delete</strong> — Contact support to temporarily suspend your account.
</li>
<li class="mb-1">
<strong>Cancel your subscription</strong> — Stop future billing without deleting your data.
</li>
<li>
<strong>Export your data first</strong> — Go to
<a asp-controller="Reports" asp-action="Index">Reports</a>
to export jobs, customers, invoices, and more before proceeding.
</li>
</ul>
</div>
</div>
<!-- Confirmation form -->
<div class="card border-danger">
<div class="card-header bg-danger bg-opacity-10 fw-semibold text-danger">
<i class="bi bi-shield-exclamation me-2"></i>Confirm account deletion
</div>
<div class="card-body">
<form asp-action="DeleteAccount" asp-controller="CompanySettings" method="post" id="deleteAccountForm">
@Html.AntiForgeryToken()
<!-- Acknowledgement checkbox -->
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="acknowledged" name="acknowledged" value="true" required />
<label class="form-check-label" for="acknowledged">
I understand that deleting my account is <strong>permanent</strong>.
I will no longer be able to export any data, and all records will be
purged. I accept that this action cannot be undone.
</label>
</div>
</div>
<!-- Type DELETE -->
<div class="mb-4">
<label for="confirmationWord" class="form-label fw-semibold">
To confirm, type <code class="text-danger fs-6">DELETE</code> in the box below:
</label>
<input type="text"
class="form-control form-control-lg"
id="confirmationWord"
name="confirmationWord"
placeholder="Type DELETE here"
autocomplete="off"
spellcheck="false" />
<div class="form-text text-muted">This field is case-sensitive. You must type it in all capitals.</div>
</div>
<!-- Action buttons -->
<div class="d-flex gap-2 flex-wrap">
<button type="submit" class="btn btn-danger px-4" id="deleteBtn" disabled>
<i class="bi bi-trash3 me-1"></i>Delete My Account Permanently
</button>
<a asp-controller="CompanySettings" asp-action="Index" class="btn btn-outline-secondary px-4">
Cancel — Keep My Account
</a>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
(function () {
const ackCheckbox = document.getElementById('acknowledged');
const confirmInput = document.getElementById('confirmationWord');
const deleteBtn = document.getElementById('deleteBtn');
/// Re-evaluates whether the submit button should be enabled.
/// Both the checkbox AND the exact word "DELETE" must be present.
function updateButton() {
const wordOk = confirmInput.value.trim() === 'DELETE';
const ackOk = ackCheckbox.checked;
deleteBtn.disabled = !(wordOk && ackOk);
}
confirmInput.addEventListener('input', updateButton);
ackCheckbox.addEventListener('change', updateButton);
// Extra guard: prevent accidental double-submit after the form is submitted.
document.getElementById('deleteAccountForm').addEventListener('submit', function () {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Deleting…';
});
})();
</script>
}
@@ -0,0 +1,206 @@
@model PowderCoating.Application.DTOs.Notification.NotificationTemplateDto
@{
ViewData["Title"] = $"Edit Template — {Model.DisplayName}";
ViewData["PageIcon"] = "bi-envelope-gear";
var placeholders = ViewBag.Placeholders as List<(string Placeholder, string Description)>
?? new List<(string, string)>();
var isEmail = Model.IsEmail;
}
@section Styles {
<style>
.placeholder-pill {
cursor: pointer;
transition: background-color 0.15s;
font-family: monospace;
font-size: 0.85rem;
}
.placeholder-pill:hover { background-color: #0d6efd !important; color: white !important; }
.copy-feedback { display: none; font-size: 0.75rem; color: #198754; }
</style>
}
<div class="container-fluid">
<div class="d-flex justify-content-start mb-4">
<a asp-controller="CompanySettings" asp-action="NotificationTemplates" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left"></i>
</a>
</div>
<div class="row g-4">
<!-- LEFT: Edit form -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center gap-2">
@if (isEmail)
{
<span class="badge bg-primary"><i class="bi bi-envelope"></i> Email</span>
}
else
{
<span class="badge bg-success"><i class="bi bi-phone"></i> SMS</span>
}
<span class="fw-semibold">@Model.NotificationType</span>
</div>
<div class="card-body">
<form asp-action="EditTemplate" asp-route-id="@Model.Id" method="post" id="templateForm">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Id" />
@if (isEmail)
{
<div class="mb-3">
<label asp-for="Subject" class="form-label fw-semibold">Subject</label>
<input asp-for="Subject" class="form-control" placeholder="Email subject line" />
<span asp-validation-for="Subject" class="text-danger small"></span>
</div>
}
<div class="mb-3">
<label class="form-label fw-semibold">
Body
@if (!isEmail)
{
<span class="text-muted fw-normal small ms-2">
<span id="charCount">@Model.Body.Length</span> characters
(<span id="segCount">@((Model.Body.Length / 160) + 1)</span> SMS segment@(((Model.Body.Length / 160) + 1) != 1 ? "s" : ""))
</span>
}
</label>
@if (isEmail)
{
<!-- Raw HTML textarea for email — supports {{placeholders}} and full HTML -->
<textarea asp-for="Body" class="form-control font-monospace" rows="16"
placeholder="Enter HTML email body..."></textarea>
<div class="form-text text-muted mt-1">
<i class="bi bi-code-slash me-1"></i>HTML is supported. Use <code>{{placeholder}}</code> tokens anywhere in the body.
</div>
}
else
{
<!-- Plain textarea for SMS -->
<textarea asp-for="Body" class="form-control" rows="5"
id="smsBody" maxlength="1000"
placeholder="Enter your SMS message text..."></textarea>
<span asp-validation-for="Body" class="text-danger small"></span>
<div class="form-text text-muted">
<i class="bi bi-info-circle"></i>
Standard SMS segments are 160 characters. Messages over 160 chars are split into multiple segments.
Always include <code>Reply STOP to opt out</code> for CTIA compliance.
</div>
}
</div>
<div class="d-flex justify-content-between align-items-center pt-2">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-floppy"></i> Save Template
</button>
<a asp-controller="CompanySettings" asp-action="NotificationTemplates"
class="btn btn-outline-secondary">
Cancel
</a>
</div>
<form asp-action="ResetTemplate" asp-route-id="@Model.Id" method="post"
onsubmit="return confirm('Reset this template to the built-in default? Your customisations will be lost.');">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-arrow-counterclockwise"></i> Reset to Default
</button>
</form>
</div>
</form>
</div>
</div>
</div>
<!-- RIGHT: Placeholder reference -->
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-braces"></i> Available Placeholders</h6>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Click any placeholder to copy it to your clipboard, then paste it into the template body.
</p>
<div class="d-flex flex-column gap-2">
@foreach (var (placeholder, description) in placeholders)
{
<div>
<span class="badge bg-light text-dark border placeholder-pill px-2 py-1"
onclick="copyPlaceholder('@placeholder', this)"
title="@description — click to copy">
@placeholder
</span>
<span class="copy-feedback ms-1">Copied!</span>
<div class="text-muted" style="font-size: 0.78rem;">@description</div>
</div>
}
</div>
</div>
</div>
<div class="card shadow-sm mt-3">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-lightbulb"></i> Tips</h6>
<ul class="small text-muted mb-0 ps-3">
<li>Placeholders are case-insensitive.</li>
<li>Unrecognised placeholders are left as-is.</li>
@if (isEmail)
{
<li>Edit raw HTML directly. A plain-text version is generated automatically for email clients that require it.</li>
<li>An unsubscribe link is always appended to comply with CAN-SPAM.</li>
}
else
{
<li>Keep messages under 160 characters to avoid splitting.</li>
<li>Always include opt-out instructions (e.g. "Reply STOP") to comply with CTIA guidelines.</li>
}
</ul>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@if (!isEmail)
{
<script>
const smsBody = document.getElementById('smsBody');
const charCount = document.getElementById('charCount');
const segCount = document.getElementById('segCount');
smsBody?.addEventListener('input', function () {
const len = this.value.length;
charCount.textContent = len;
const segs = Math.ceil(len / 160) || 1;
segCount.textContent = segs;
const suffixEl = segCount.nextSibling;
if (suffixEl) suffixEl.textContent = ` SMS segment${segs !== 1 ? 's' : ''}`;
});
</script>
}
<script>
function copyPlaceholder(text, el) {
navigator.clipboard.writeText(text).then(function () {
const feedback = el.nextElementSibling;
if (feedback) {
feedback.style.display = 'inline';
setTimeout(() => { feedback.style.display = 'none'; }, 1800);
}
}).catch(function () {
// Fallback for browsers that don't support clipboard API
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
}
</script>
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,500 @@
@* Lookup Management Modals *@
<!-- Job Status Modal -->
<div class="modal fade" id="jobStatusModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-list-check me-2"></i><span id="jobStatusModalTitle">Add Job Status</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="jobStatusForm">
<input type="hidden" id="jobStatusId" value="">
<div class="mb-3">
<label for="jobStatusDisplayName" class="form-label">Display Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="jobStatusDisplayName" placeholder="e.g., Custom Status" required maxlength="100">
</div>
<div class="mb-3">
<label for="jobStatusCode" class="form-label">Status Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="jobStatusCode" placeholder="e.g., CUSTOM_STATUS" required pattern="[A-Z_]+" maxlength="50">
<small class="form-text text-muted">Uppercase letters and underscores only. This cannot be changed later.</small>
</div>
<div class="mb-3">
<label for="jobStatusColorClass" class="form-label">Badge Color <span class="text-danger">*</span></label>
<select class="form-select" id="jobStatusColorClass" required>
<option value="primary">Primary (Blue)</option>
<option value="secondary">Secondary (Gray)</option>
<option value="success">Success (Green)</option>
<option value="danger">Danger (Red)</option>
<option value="warning">Warning (Yellow)</option>
<option value="info">Info (Cyan)</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="mb-3">
<label for="jobStatusCategory" class="form-label">Workflow Category</label>
<select class="form-select" id="jobStatusCategory">
<option value="">Select a category</option>
<option value="Pre-Production">Pre-Production</option>
<option value="Production">Production</option>
<option value="Post-Production">Post-Production</option>
<option value="Other">Other</option>
</select>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="jobStatusIsTerminal">
<label class="form-check-label" for="jobStatusIsTerminal">
Terminal Status
<small class="d-block text-muted">Job is completed/closed</small>
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="jobStatusIsWIP">
<label class="form-check-label" for="jobStatusIsWIP">
Work In Progress
<small class="d-block text-muted">Job is actively being worked on</small>
</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="jobStatusDescription" class="form-label">Description</label>
<textarea class="form-control" id="jobStatusDescription" rows="2" placeholder="Optional description..." maxlength="500"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveJobStatusBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
<!-- Job Priority Modal -->
<div class="modal fade" id="jobPriorityModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-arrow-up-circle me-2"></i><span id="jobPriorityModalTitle">Add Job Priority</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="jobPriorityForm">
<input type="hidden" id="jobPriorityId" value="">
<div class="mb-3">
<label for="jobPriorityDisplayName" class="form-label">Display Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="jobPriorityDisplayName" placeholder="e.g., Custom Priority" required maxlength="100">
</div>
<div class="mb-3">
<label for="jobPriorityCode" class="form-label">Priority Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="jobPriorityCode" placeholder="e.g., CUSTOM_PRIORITY" required pattern="[A-Z_]+" maxlength="50">
<small class="form-text text-muted">Uppercase letters and underscores only. This cannot be changed later.</small>
</div>
<div class="mb-3">
<label for="jobPriorityColorClass" class="form-label">Badge Color <span class="text-danger">*</span></label>
<select class="form-select" id="jobPriorityColorClass" required>
<option value="primary">Primary (Blue)</option>
<option value="secondary">Secondary (Gray)</option>
<option value="success">Success (Green)</option>
<option value="danger">Danger (Red)</option>
<option value="warning">Warning (Yellow)</option>
<option value="info">Info (Cyan)</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="mb-3">
<label for="jobPriorityDescription" class="form-label">Description</label>
<textarea class="form-control" id="jobPriorityDescription" rows="2" placeholder="Optional description..." maxlength="500"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveJobPriorityBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
<!-- Quote Status Modal -->
<div class="modal fade" id="quoteStatusModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-file-text me-2"></i><span id="quoteStatusModalTitle">Add Quote Status</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="quoteStatusForm">
<input type="hidden" id="quoteStatusId" value="">
<div class="mb-3">
<label for="quoteStatusDisplayName" class="form-label">Display Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="quoteStatusDisplayName" placeholder="e.g., Custom Status" required maxlength="100">
</div>
<div class="mb-3">
<label for="quoteStatusCode" class="form-label">Status Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="quoteStatusCode" placeholder="e.g., CUSTOM_STATUS" required pattern="[A-Z_]+" maxlength="50">
<small class="form-text text-muted">Uppercase letters and underscores only. This cannot be changed later.</small>
</div>
<div class="mb-3">
<label for="quoteStatusColorClass" class="form-label">Badge Color <span class="text-danger">*</span></label>
<select class="form-select" id="quoteStatusColorClass" required>
<option value="primary">Primary (Blue)</option>
<option value="secondary">Secondary (Gray)</option>
<option value="success">Success (Green)</option>
<option value="danger">Danger (Red)</option>
<option value="warning">Warning (Yellow)</option>
<option value="info">Info (Cyan)</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Business Flags</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="quoteStatusIsApproved">
<label class="form-check-label" for="quoteStatusIsApproved">
<strong>Approved Status</strong>
<small class="d-block text-muted">Marks quotes that can be converted to jobs</small>
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="quoteStatusIsConverted">
<label class="form-check-label" for="quoteStatusIsConverted">
<strong>Converted Status</strong>
<small class="d-block text-muted">Set automatically after converting to job</small>
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="quoteStatusIsDraft">
<label class="form-check-label" for="quoteStatusIsDraft">
<strong>Draft Status</strong>
<small class="d-block text-muted">Quote is being prepared</small>
</label>
</div>
</div>
<div class="mb-3">
<label for="quoteStatusDescription" class="form-label">Description</label>
<textarea class="form-control" id="quoteStatusDescription" rows="2" placeholder="Optional description..." maxlength="500"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveQuoteStatusBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
<!-- Inventory Category Modal -->
<div class="modal fade" id="inventoryCategoryModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-box-seam me-2"></i><span id="inventoryCategoryModalTitle">Add Inventory Category</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="inventoryCategoryForm">
<input type="hidden" id="inventoryCategoryId" value="">
<div class="mb-3">
<label for="inventoryCategoryDisplayName" class="form-label">Display Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="inventoryCategoryDisplayName" placeholder="e.g., Custom Category" required maxlength="100">
</div>
<div class="mb-3">
<label for="inventoryCategoryCode" class="form-label">Category Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="inventoryCategoryCode" placeholder="e.g., CUSTOM_CATEGORY" required pattern="[A-Z_]+" maxlength="50">
<small class="form-text text-muted">Uppercase letters and underscores only. This cannot be changed later.</small>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="inventoryCategoryIsCoating">
<label class="form-check-label" for="inventoryCategoryIsCoating">
<strong>Is Coating</strong>
<small class="d-block text-muted">Items in this category are coatings that can be applied to parts</small>
</label>
</div>
</div>
<div class="mb-3">
<label for="inventoryCategoryDescription" class="form-label">Description</label>
<textarea class="form-control" id="inventoryCategoryDescription" rows="2" placeholder="Optional description of what this category is used for..." maxlength="500"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveInventoryCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
<!-- Appointment Type Modal -->
<div class="modal fade" id="appointmentTypeModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-calendar-event me-2"></i><span id="appointmentTypeModalTitle">Add Appointment Type</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="appointmentTypeForm">
<input type="hidden" id="appointmentTypeId" value="">
<div class="mb-3">
<label for="appointmentTypeDisplayName" class="form-label">Display Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="appointmentTypeDisplayName" placeholder="e.g., Equipment Delivery" required maxlength="100">
</div>
<div class="mb-3">
<label for="appointmentTypeCode" class="form-label">Type Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="appointmentTypeCode" placeholder="e.g., DELIVERY" required pattern="[A-Z_]+" maxlength="50">
<small class="form-text text-muted">Uppercase letters and underscores only. This cannot be changed later.</small>
</div>
<div class="mb-3">
<label for="appointmentTypeColorClass" class="form-label">Calendar Color <span class="text-danger">*</span></label>
<div class="d-flex gap-2 align-items-center mb-2">
<span class="badge fs-6 px-3 py-2" id="appointmentTypeColorPreview">
<i class="bi bi-calendar-event me-1"></i>Preview
</span>
<small class="text-muted">Live preview of calendar color</small>
</div>
<select class="form-select" id="appointmentTypeColorClass" required onchange="updateAppointmentTypeColorPreview()">
<option value="purple">🟣 Purple</option>
<option value="green">🟢 Green</option>
<option value="blue">🔵 Blue</option>
<option value="orange">🟠 Orange</option>
<option value="red">🔴 Red</option>
<option value="yellow">🟡 Yellow</option>
<option value="pink">🩷 Pink</option>
<option value="cyan">🔷 Cyan</option>
<option value="teal">🩵 Teal</option>
<option value="indigo">🟣 Indigo</option>
<option value="lime">🟢 Lime</option>
<option value="brown">🟤 Brown</option>
<option value="gray">⚫ Gray</option>
<option value="success">✅ Success (Green)</option>
<option value="danger">❌ Danger (Red)</option>
<option value="warning">⚠️ Warning (Yellow)</option>
<option value="info">️ Info (Blue)</option>
<option value="primary">🔷 Primary (Blue)</option>
<option value="secondary">⚫ Secondary (Gray)</option>
<option value="dark">⬛ Dark</option>
</select>
<small class="form-text text-muted">Choose a color that will appear on the calendar for this appointment type</small>
</div>
<div class="mb-3">
<label for="appointmentTypeIconClass" class="form-label">Icon Class (Optional)</label>
<input type="text" class="form-control" id="appointmentTypeIconClass" placeholder="e.g., bi-truck, bi-box-arrow-down" maxlength="50">
<small class="form-text text-muted">Bootstrap icon class for visual identification</small>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="appointmentTypeRequiresJob">
<label class="form-check-label" for="appointmentTypeRequiresJob">
Require Job Link
<small class="d-block text-muted">This appointment type must be linked to an existing job</small>
</label>
</div>
</div>
<div class="mb-3" id="appointmentTypeActiveField" style="display: none;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="appointmentTypeIsActive" checked>
<label class="form-check-label" for="appointmentTypeIsActive">
Active
<small class="d-block text-muted">Inactive types won't appear in dropdown menus</small>
</label>
</div>
</div>
<div class="mb-3">
<label for="appointmentTypeDescription" class="form-label">Description</label>
<textarea class="form-control" id="appointmentTypeDescription" rows="2" placeholder="Optional description..." maxlength="500"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveAppointmentTypeBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
<!-- Prep Service Modal -->
<div class="modal fade" id="prepServiceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-tools me-2"></i><span id="prepServiceModalTitle">Add Prep Service</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="prepServiceForm">
<input type="hidden" id="prepServiceId" value="">
<div class="mb-3">
<label for="prepServiceName" class="form-label">Service Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="prepServiceName" placeholder="e.g., Sandblasting, Chemical Stripping" required maxlength="100">
<small class="form-text text-muted">Enter a descriptive name for this preparation service</small>
</div>
<div class="mb-3">
<label for="prepServiceDescription" class="form-label">Description</label>
<textarea class="form-control" id="prepServiceDescription" rows="3" placeholder="Optional description of this service..." maxlength="500"></textarea>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="prepServiceRequiresBlastSetup">
<label class="form-check-label" for="prepServiceRequiresBlastSetup">
<strong>Requires Blast Setup Selection</strong>
<small class="d-block text-muted">When checked, the item wizard shows a blast rig dropdown for this service so the correct throughput rate is used</small>
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="prepServiceIsActive" checked>
<label class="form-check-label" for="prepServiceIsActive">
<strong>Active</strong>
<small class="d-block text-muted">Inactive services won't appear in dropdown menus</small>
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="savePrepServiceBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
<!-- Blast Setup Modal -->
<div class="modal fade" id="blastSetupModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-fan me-2"></i><span id="blastSetupModalTitle">Add Blast Setup</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="blastSetupForm">
<input type="hidden" id="blastSetupId" value="">
<div class="row g-3">
<div class="col-12">
<label for="blastSetupName" class="form-label">Setup Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="blastSetupName"
placeholder="e.g., Main Cabinet, Outdoor Blast Pot, Blast Room" required maxlength="100">
</div>
<div class="col-sm-6">
<label for="blastSetupModalType" class="form-label">Setup Type <span class="text-danger">*</span></label>
<select class="form-select" id="blastSetupModalType">
<option value="0">Siphon Cabinet</option>
<option value="1">Siphon Pot</option>
<option value="2" selected>Pressure Pot</option>
<option value="3">Wet Blasting</option>
</select>
</div>
<div class="col-sm-6">
<label for="blastSetupCfm" class="form-label">Compressor CFM <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" class="form-control blast-modal-input" id="blastSetupCfm"
min="0" max="9999" step="0.5" placeholder="e.g. 40">
<span class="input-group-text">CFM</span>
</div>
</div>
<div class="col-sm-6">
<label for="blastSetupNozzleSize" class="form-label">Nozzle Size</label>
<select class="form-select blast-modal-input" id="blastSetupNozzleSize">
<option value="2">#2 (1/8") — Very small / entry level</option>
<option value="3">#3 (3/16") — Small / hobby</option>
<option value="4">#4 (1/4") — Light duty</option>
<option value="5" selected>#5 (5/16") — Medium (most common)</option>
<option value="6">#6 (3/8") — Heavy duty</option>
<option value="7">#7 (7/16") — High volume</option>
<option value="8">#8 (1/2") — Industrial</option>
</select>
</div>
<div class="col-sm-6">
<label for="blastSetupSubstrate" class="form-label">Primary Substrate</label>
<select class="form-select blast-modal-input" id="blastSetupSubstrate">
<option value="1">Paint / light coating</option>
<option value="0" selected>Mixed (typical)</option>
<option value="2">Rust &amp; scale</option>
<option value="3">Existing powder coat</option>
</select>
</div>
<div class="col-sm-6">
<label for="blastSetupOverride" class="form-label">Rate Override <small class="text-muted">(optional)</small></label>
<div class="input-group">
<input type="number" class="form-control" id="blastSetupOverride"
min="0" max="99999" step="0.1" placeholder="Leave blank to use formula">
<span class="input-group-text">sqft/hr</span>
</div>
<small class="text-muted">Enter your actual measured rate to bypass the formula</small>
</div>
<div class="col-sm-6 d-flex align-items-end">
<div class="w-100 p-3 bg-light rounded text-center">
<div class="text-muted small">Derived Rate</div>
<div class="fw-bold fs-5" id="blastSetupDerivedRate">—</div>
<div class="text-muted small">sqft/hr</div>
</div>
</div>
<div class="col-12">
<div class="d-flex gap-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="blastSetupIsDefault">
<label class="form-check-label" for="blastSetupIsDefault">
<strong>Default</strong>
<small class="d-block text-muted">Pre-selected in AI Photo Quotes</small>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="blastSetupIsActive" checked>
<label class="form-check-label" for="blastSetupIsActive">
<strong>Active</strong>
</label>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveBlastSetupBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
</div>
</div>
</div>
</div>