Design consistency audit fixes: alerts, cards, dark mode, utilities
Alert sweep (113 alerts, 79 files):
All persistent static banners now carry alert-permanent so the
layout's 5-second auto-dismiss cannot swallow guidance, warnings,
or validation errors. Transient dismissible toasts left untouched.
CSS fixes (site.css):
.card.shadow-sm — strips rogue border from ~40 drifted cards
.card-header.bg-white — rebinds to var(--bs-body-bg) so card
headers follow dark/light theme correctly
Typography utilities — .text-2xs (.68rem), .text-xs (.73rem)
Token color classes — .text-ember, .text-ok, .text-bad,
.text-warn, .text-cool, .bg-paper-2
Layout utilities — .mw-xs/sm/md/lg replace inline max-width
Comment — documents text-ember vs text-primary intent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
@model PowderCoating.Application.DTOs.Accounting.CreateAccountDto
|
@model PowderCoating.Application.DTOs.Accounting.CreateAccountDto
|
||||||
@using PowderCoating.Core.Enums
|
@using PowderCoating.Core.Enums
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "New Account";
|
ViewData["Title"] = "New Account";
|
||||||
ViewData["PageIcon"] = "bi-journal-plus";
|
ViewData["PageIcon"] = "bi-journal-plus";
|
||||||
ViewData["PageHelpTitle"] = "New Account";
|
ViewData["PageHelpTitle"] = "New Account";
|
||||||
ViewData["PageHelpContent"] = "Add a custom account to the Chart of Accounts. Select a Sub-Type first — it auto-sets the Account Type. Use conventional numbering: 1000s = Assets, 2000s = Liabilities, 3000s = Equity, 4000s = Revenue, 5000s = Cost of Goods, 6000s+ = Expenses.";
|
ViewData["PageHelpContent"] = "Add a custom account to the Chart of Accounts. Select a Sub-Type first — it auto-sets the Account Type. Use conventional numbering: 1000s = Assets, 2000s = Liabilities, 3000s = Equity, 4000s = Revenue, 5000s = Cost of Goods, 6000s+ = Expenses.";
|
||||||
bool isInline = ViewBag.Inline == true;
|
bool isInline = ViewBag.Inline == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
<form asp-action="Create" method="post">
|
<form asp-action="Create" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Account Number"
|
data-bs-title="Account Number"
|
||||||
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique. Sub-accounts can use decimals (e.g. 6100.1).">
|
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique. Sub-accounts can use decimals (e.g. 6100.1).">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="AccountType" asp-items="ViewBag.AccountTypes" class="form-select" id="accountTypeSelect">
|
<select asp-for="AccountType" asp-items="ViewBag.AccountTypes" class="form-select" id="accountTypeSelect">
|
||||||
<option value="">— Select Type —</option>
|
<option value="">— Select Type —</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="AccountType" class="text-danger small"></span>
|
<span asp-validation-for="AccountType" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="AccountSubType" asp-items="ViewBag.AccountSubTypes" class="form-select" id="accountSubTypeSelect">
|
<select asp-for="AccountSubType" asp-items="ViewBag.AccountSubTypes" class="form-select" id="accountSubTypeSelect">
|
||||||
<option value="">— Select Sub-Type —</option>
|
<option value="">— Select Sub-Type —</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="AccountSubType" class="text-danger small"></span>
|
<span asp-validation-for="AccountSubType" class="text-danger small"></span>
|
||||||
<div class="form-text text-primary" id="typeAutoSetHint" style="display:none">
|
<div class="form-text text-primary" id="typeAutoSetHint" style="display:none">
|
||||||
@@ -89,12 +89,12 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Parent Account"
|
data-bs-title="Parent Account"
|
||||||
data-bs-content="Nest this account under a parent to create a hierarchy — e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
|
data-bs-content="Nest this account under a parent to create a hierarchy — e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="ParentAccountId" asp-items="ViewBag.ParentAccounts" class="form-select">
|
<select asp-for="ParentAccountId" asp-items="ViewBag.ParentAccounts" class="form-select">
|
||||||
<option value="">— None (top-level account) —</option>
|
<option value="">— None (top-level account) —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
// SubType enum values → AccountType enum values (mirrors server-side mapping)
|
// SubType enum values → AccountType enum values (mirrors server-side mapping)
|
||||||
const subTypeToAccountType = {
|
const subTypeToAccountType = {
|
||||||
8: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Assets
|
8: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Assets
|
||||||
10: 2, 11: 2, 12: 2, 13: 2, // Liabilities
|
10: 2, 11: 2, 12: 2, 13: 2, // Liabilities
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.Accounting.EditAccountDto
|
@model PowderCoating.Application.DTOs.Accounting.EditAccountDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Account";
|
ViewData["Title"] = "Edit Account";
|
||||||
ViewData["PageIcon"] = "bi-pencil-square";
|
ViewData["PageIcon"] = "bi-pencil-square";
|
||||||
ViewData["PageHelpTitle"] = "Edit Account";
|
ViewData["PageHelpTitle"] = "Edit Account";
|
||||||
ViewData["PageHelpContent"] = "You can change number, name, type, sub-type, parent, and opening balance. Changing the account type or sub-type on an account that already has transactions is allowed but use caution — it changes how balances are reported going forward. Inactive accounts are hidden from pickers but preserved in history.";
|
ViewData["PageHelpContent"] = "You can change number, name, type, sub-type, parent, and opening balance. Changing the account type or sub-type on an account that already has transactions is allowed but use caution — it changes how balances are reported going forward. Inactive accounts are hidden from pickers but preserved in history.";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-start mb-4">
|
<div class="d-flex justify-content-start mb-4">
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<form asp-action="Edit" method="post">
|
<form asp-action="Edit" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input asp-for="Id" type="hidden" />
|
<input asp-for="Id" type="hidden" />
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Account Number"
|
data-bs-title="Account Number"
|
||||||
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique.">
|
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,12 +81,12 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Parent Account"
|
data-bs-title="Parent Account"
|
||||||
data-bs-content="Nest this account under a parent to create a hierarchy — e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
|
data-bs-content="Nest this account under a parent to create a hierarchy — e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="ParentAccountId" asp-items="ViewBag.ParentAccounts" class="form-select">
|
<select asp-for="ParentAccountId" asp-items="ViewBag.ParentAccounts" class="form-select">
|
||||||
<option value="">— None —</option>
|
<option value="">— None —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Year-End Close";
|
ViewData["Title"] = "Year-End Close";
|
||||||
ViewData["PageIcon"] = "bi-calendar-check";
|
ViewData["PageIcon"] = "bi-calendar-check";
|
||||||
@@ -30,10 +30,10 @@
|
|||||||
<h5 class="mb-0 fw-semibold"><i class="bi bi-calendar-check me-2 text-primary"></i>Close a Fiscal Year</h5>
|
<h5 class="mb-0 fw-semibold"><i class="bi bi-calendar-check me-2 text-primary"></i>Close a Fiscal Year</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-warning py-2 mb-4">
|
<div class="alert alert-warning alert-permanent py-2 mb-4">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong>What this does:</strong> Posts a Journal Entry dated December 31 that zeroes all Revenue
|
<strong>What this does:</strong> Posts a Journal Entry dated December 31 that zeroes all Revenue
|
||||||
and Expense account balances into Retained Earnings — the standard accounting close.
|
and Expense account balances into Retained Earnings — the standard accounting close.
|
||||||
Run this <strong>after</strong> all entries for the year are posted and the period is locked.
|
Run this <strong>after</strong> all entries for the year are posted and the period is locked.
|
||||||
A year can only be closed once.
|
A year can only be closed once.
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">@c.ClosedYear</td>
|
<td class="fw-bold">@c.ClosedYear</td>
|
||||||
<td>@c.ClosedAt.ToLocalTime().ToString("MM/dd/yyyy h:mm tt")</td>
|
<td>@c.ClosedAt.ToLocalTime().ToString("MM/dd/yyyy h:mm tt")</td>
|
||||||
<td>@(c.ClosedBy ?? "—")</td>
|
<td>@(c.ClosedBy ?? "—")</td>
|
||||||
<td>
|
<td>
|
||||||
@if (c.JournalEntry != null)
|
@if (c.JournalEntry != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.Appointment.CreateAppointmentDto
|
@model PowderCoating.Application.DTOs.Appointment.CreateAppointmentDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "New Appointment";
|
ViewData["Title"] = "New Appointment";
|
||||||
ViewData["PageIcon"] = "bi-calendar-plus";
|
ViewData["PageIcon"] = "bi-calendar-plus";
|
||||||
ViewData["PageHelpTitle"] = "New Appointment";
|
ViewData["PageHelpTitle"] = "New Appointment";
|
||||||
ViewData["PageHelpContent"] = "Create an appointment to schedule a customer visit, drop-off, pick-up, or consultation. Select the Type first — the Linked Job field appears once a type is chosen. Reminder notifications fire before the scheduled start time.";
|
ViewData["PageHelpContent"] = "Create an appointment to schedule a customer visit, drop-off, pick-up, or consultation. Select the Type first — the Linked Job field appears once a type is chosen. Reminder notifications fire before the scheduled start time.";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<form asp-action="Create" method="post">
|
<form asp-action="Create" method="post">
|
||||||
@if (!ViewData.ModelState.IsValid)
|
@if (!ViewData.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-2"></i>Please correct the following errors:</h6>
|
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-2"></i>Please correct the following errors:</h6>
|
||||||
<partial name="_ValidationSummary" />
|
<partial name="_ValidationSummary" />
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Reminder Settings"
|
data-bs-title="Reminder Settings"
|
||||||
data-bs-content="Enable a reminder to receive an in-app notification before the appointment. Set how many minutes in advance — e.g., 30 for a brief heads-up, 1440 for a full day before. Reminders are per-appointment and do not send external emails or SMS.">
|
data-bs-content="Enable a reminder to receive an in-app notification before the appointment. Set how many minutes in advance — e.g., 30 for a brief heads-up, 1440 for a full day before. Reminders are per-appointment and do not send external emails or SMS.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.Appointment.AppointmentDto
|
@model PowderCoating.Application.DTOs.Appointment.AppointmentDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Appointment {Model.AppointmentNumber}";
|
ViewData["Title"] = $"Appointment {Model.AppointmentNumber}";
|
||||||
ViewData["PageIcon"] = "bi-calendar-event";
|
ViewData["PageIcon"] = "bi-calendar-event";
|
||||||
ViewData["PageHelpTitle"] = "Appointment Details";
|
ViewData["PageHelpTitle"] = "Appointment Details";
|
||||||
ViewData["PageHelpContent"] = "View all details for this appointment. Edit to update status or record actual times. Deleting permanently removes the record — consider setting status to Cancelled instead to preserve history.";
|
ViewData["PageHelpContent"] = "View all details for this appointment. Edit to update status or record actual times. Deleting permanently removes the record — consider setting status to Cancelled instead to preserve history.";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info alert-permanent mb-0">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<strong>Internal Appointment</strong><br />
|
<strong>Internal Appointment</strong><br />
|
||||||
<small>This appointment is not associated with a customer.</small>
|
<small>This appointment is not associated with a customer.</small>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.DTOs.Subscription
|
@using PowderCoating.Application.DTOs.Subscription
|
||||||
@using PowderCoating.Core.Enums
|
@using PowderCoating.Core.Enums
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Billing & Subscription";
|
ViewData["Title"] = "Billing & Subscription";
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
@if (isExpiringSoon)
|
@if (isExpiringSoon)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center justify-content-between mb-4" role="alert">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center justify-content-between mb-4" role="alert">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-clock-history me-2"></i>
|
<i class="bi bi-clock-history me-2"></i>
|
||||||
<strong>Your subscription expires in @status.DaysRemaining day@(status.DaysRemaining == 1 ? "" : "s").</strong>
|
<strong>Your subscription expires in @status.DaysRemaining day@(status.DaysRemaining == 1 ? "" : "s").</strong>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
else if (status.IsGracePeriod)
|
else if (status.IsGracePeriod)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center justify-content-between mb-4" role="alert">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center justify-content-between mb-4" role="alert">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Your subscription has expired.</strong>
|
<strong>Your subscription has expired.</strong>
|
||||||
@@ -93,7 +93,7 @@ else if (status.IsGracePeriod)
|
|||||||
}
|
}
|
||||||
else if (status.IsExpired)
|
else if (status.IsExpired)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger d-flex align-items-center justify-content-between mb-4" role="alert">
|
<div class="alert alert-danger alert-permanent d-flex align-items-center justify-content-between mb-4" role="alert">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-x-octagon-fill me-2"></i>
|
<i class="bi bi-x-octagon-fill me-2"></i>
|
||||||
<strong>Your subscription has expired and access is restricted.</strong>
|
<strong>Your subscription has expired and access is restricted.</strong>
|
||||||
@@ -136,7 +136,7 @@ else if (status.IsExpired)
|
|||||||
}
|
}
|
||||||
else if (status.IsGracePeriod)
|
else if (status.IsGracePeriod)
|
||||||
{
|
{
|
||||||
<span class="text-warning">Grace period — expired @status.EndDate.Value.ToString("MMM d, yyyy")</span>
|
<span class="text-warning">Grace period — expired @status.EndDate.Value.ToString("MMM d, yyyy")</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -266,7 +266,7 @@ else if (status.IsExpired)
|
|||||||
<strong>Cancellation & Refund Policy:</strong>
|
<strong>Cancellation & Refund Policy:</strong>
|
||||||
You may cancel your subscription at any time from this page or by contacting
|
You may cancel your subscription at any time from this page or by contacting
|
||||||
<a href="mailto:support@powdercoatinglogix.com">support@powdercoatinglogix.com</a>.
|
<a href="mailto:support@powdercoatinglogix.com">support@powdercoatinglogix.com</a>.
|
||||||
Cancellation takes effect at the end of your current billing period — you retain full access until then.
|
Cancellation takes effect at the end of your current billing period — you retain full access until then.
|
||||||
All fees are <strong>non-refundable</strong>; unused time is not credited back.
|
All fees are <strong>non-refundable</strong>; unused time is not credited back.
|
||||||
See our <a asp-controller="Home" asp-action="TermsOfService" asp-fragment="section-5" target="_blank">full billing terms</a> for details.
|
See our <a asp-controller="Home" asp-action="TermsOfService" asp-fragment="section-5" target="_blank">full billing terms</a> for details.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.Accounting.CreateBillDto
|
@model PowderCoating.Application.DTOs.Accounting.CreateBillDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "New Bill";
|
ViewData["Title"] = "New Bill";
|
||||||
ViewData["PageIcon"] = "bi-receipt-cutoff";
|
ViewData["PageIcon"] = "bi-receipt-cutoff";
|
||||||
ViewData["PageHelpTitle"] = "New Bill";
|
ViewData["PageHelpTitle"] = "New Bill";
|
||||||
ViewData["PageHelpContent"] = "Record a vendor invoice to track what you owe. Bills start as Draft (editable) and become Open once confirmed. Partial payments are supported — each payment reduces the balance. Link line items to expense accounts and optionally to specific jobs for cost tracking.";
|
ViewData["PageHelpContent"] = "Record a vendor invoice to track what you owe. Bills start as Draft (editable) and become Open once confirmed. Partial payments are supported — each payment reduces the balance. Link line items to expense accounts and optionally to specific jobs for cost tracking.";
|
||||||
string? fromPoNumber = ViewBag.FromPoNumber as string;
|
string? fromPoNumber = ViewBag.FromPoNumber as string;
|
||||||
int? fromPoId = ViewBag.FromPoId as int?;
|
int? fromPoId = ViewBag.FromPoId as int?;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div>
|
<div>
|
||||||
@if (!string.IsNullOrEmpty(fromPoNumber))
|
@if (!string.IsNullOrEmpty(fromPoNumber))
|
||||||
{
|
{
|
||||||
<p class="text-muted mb-0 small"><i class="bi bi-box-arrow-in-down text-success me-1"></i> Pre-filled from <strong>@fromPoNumber</strong> — review and save</p>
|
<p class="text-muted mb-0 small"><i class="bi bi-box-arrow-in-down text-success me-1"></i> Pre-filled from <strong>@fromPoNumber</strong> — review and save</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (fromPoId.HasValue)
|
@if (fromPoId.HasValue)
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
{
|
{
|
||||||
<input type="hidden" name="PurchaseOrderId" value="@Model.PurchaseOrderId" />
|
<input type="hidden" name="PurchaseOrderId" value="@Model.PurchaseOrderId" />
|
||||||
}
|
}
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Left column: Bill header -->
|
<!-- Left column: Bill header -->
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Bill Details"
|
data-bs-title="Bill Details"
|
||||||
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation. Payment Terms auto-fill from the vendor record.">
|
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation. Payment Terms auto-fill from the vendor record.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,8 +66,8 @@
|
|||||||
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
|
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
|
||||||
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select" id="vendorSelect"
|
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select" id="vendorSelect"
|
||||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||||
<option value="">— Select Vendor —</option>
|
<option value="">— Select Vendor —</option>
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="VendorId" class="text-danger small"></span>
|
<span asp-validation-for="VendorId" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<label for="receiptFile" class="form-label fw-medium">Attach Receipt / Document</label>
|
<label for="receiptFile" class="form-label fw-medium">Attach Receipt / Document</label>
|
||||||
<input type="file" name="receiptFile" id="receiptFile" class="form-control"
|
<input type="file" name="receiptFile" id="receiptFile" class="form-control"
|
||||||
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
|
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
|
||||||
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
|
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Line Items"
|
data-bs-title="Line Items"
|
||||||
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
|
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
||||||
data-bs-title="Bill Summary"
|
data-bs-title="Bill Summary"
|
||||||
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid.">
|
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small fw-medium">Bank / Cash Account <span class="text-danger">*</span></label>
|
<label class="form-label small fw-medium">Bank / Cash Account <span class="text-danger">*</span></label>
|
||||||
<select name="bankAccountId" class="form-select form-select-sm" id="payNowBankAccount">
|
<select name="bankAccountId" class="form-select form-select-sm" id="payNowBankAccount">
|
||||||
<option value="">— Select Account —</option>
|
<option value="">— Select Account —</option>
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
|
||||||
{
|
{
|
||||||
<option value="@item.Value">@item.Text</option>
|
<option value="@item.Value">@item.Text</option>
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
<tr class="line-item-row">
|
<tr class="line-item-row">
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
|
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
|
||||||
<option value="">— Account —</option>
|
<option value="">— Account —</option>
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
||||||
{
|
{
|
||||||
<option value="@item.Value">@item.Text</option>
|
<option value="@item.Value">@item.Text</option>
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
|
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
|
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
|
||||||
{
|
{
|
||||||
<option value="@item.Value">@item.Text</option>
|
<option value="@item.Value">@item.Text</option>
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="scanReceiptFile" class="form-label fw-medium">Receipt / Invoice Document</label>
|
<label for="scanReceiptFile" class="form-label fw-medium">Receipt / Invoice Document</label>
|
||||||
<input type="file" id="scanReceiptFile" class="form-control" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
|
<input type="file" id="scanReceiptFile" class="form-control" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
|
||||||
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
|
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="scanReceiptStatus" class="text-muted small mt-2"></div>
|
<div id="scanReceiptStatus" class="text-muted small mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -393,8 +393,8 @@
|
|||||||
}
|
}
|
||||||
if (lineCount === 0) addLineItem();
|
if (lineCount === 0) addLineItem();
|
||||||
|
|
||||||
// ── AI Auto-suggest Account on description blur ───────────────────────
|
// ── AI Auto-suggest Account on description blur ───────────────────────
|
||||||
// Keyword shortcuts — handle common cases with zero API cost
|
// Keyword shortcuts — handle common cases with zero API cost
|
||||||
const _keywordMap = [
|
const _keywordMap = [
|
||||||
{ words: ['electric','power','utility','gas','water','internet','phone','telecom'], hint: 'utilities' },
|
{ words: ['electric','power','utility','gas','water','internet','phone','telecom'], hint: 'utilities' },
|
||||||
{ words: ['powder','paint','coat','material','supply','supplies','chemical','resin'], hint: 'materials' },
|
{ words: ['powder','paint','coat','material','supply','supplies','chemical','resin'], hint: 'materials' },
|
||||||
@@ -407,7 +407,7 @@
|
|||||||
{ words: ['advertising','marketing','promo'], hint: 'advertising' },
|
{ words: ['advertising','marketing','promo'], hint: 'advertising' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Session cache: description (lowercased) → { accountId, accountName }
|
// Session cache: description (lowercased) → { accountId, accountName }
|
||||||
const _suggestCache = new Map();
|
const _suggestCache = new Map();
|
||||||
|
|
||||||
function _keywordGuess(description) {
|
function _keywordGuess(description) {
|
||||||
@@ -480,7 +480,7 @@
|
|||||||
hint2.className = 'ai-account-hint text-muted small mt-1';
|
hint2.className = 'ai-account-hint text-muted small mt-1';
|
||||||
accountSel.parentNode.appendChild(hint2);
|
accountSel.parentNode.appendChild(hint2);
|
||||||
}
|
}
|
||||||
hint2.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:.75rem;height:.75rem"></span> Thinking…';
|
hint2.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:.75rem;height:.75rem"></span> Thinking…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||||||
@@ -501,14 +501,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event delegation — works for dynamically added rows
|
// Event delegation — works for dynamically added rows
|
||||||
document.getElementById('lineItemsBody').addEventListener('blur', function (e) {
|
document.getElementById('lineItemsBody').addEventListener('blur', function (e) {
|
||||||
if (e.target.matches('[name$=".Description"]')) {
|
if (e.target.matches('[name$=".Description"]')) {
|
||||||
_suggestAccountForRow(e.target.closest('tr'));
|
_suggestAccountForRow(e.target.closest('tr'));
|
||||||
}
|
}
|
||||||
}, true); // capture phase so blur bubbles
|
}, true); // capture phase so blur bubbles
|
||||||
|
|
||||||
// ── Scan Receipt ─────────────────────────────────────────────────────
|
// ── Scan Receipt ─────────────────────────────────────────────────────
|
||||||
document.getElementById('scanReceiptUploadBtn').addEventListener('click', async function () {
|
document.getElementById('scanReceiptUploadBtn').addEventListener('click', async function () {
|
||||||
const fileInput = document.getElementById('scanReceiptFile');
|
const fileInput = document.getElementById('scanReceiptFile');
|
||||||
if (!fileInput.files.length) { alert('Please select a file.'); return; }
|
if (!fileInput.files.length) { alert('Please select a file.'); return; }
|
||||||
@@ -535,7 +535,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-fill bill header — try to match vendor name to dropdown
|
// Auto-fill bill header — try to match vendor name to dropdown
|
||||||
if (data.vendorName) {
|
if (data.vendorName) {
|
||||||
const vendorSel = document.getElementById('vendorSelect');
|
const vendorSel = document.getElementById('vendorSelect');
|
||||||
if (vendorSel && !vendorSel.value) {
|
if (vendorSel && !vendorSel.value) {
|
||||||
@@ -553,7 +553,7 @@
|
|||||||
vendorSel.value = bestOption.value;
|
vendorSel.value = bestOption.value;
|
||||||
vendorSel.dispatchEvent(new Event('change'));
|
vendorSel.dispatchEvent(new Event('change'));
|
||||||
} else {
|
} else {
|
||||||
// No match — put the name in Memo so user knows what the AI saw
|
// No match — put the name in Memo so user knows what the AI saw
|
||||||
const memo = document.querySelector('[name="Memo"]');
|
const memo = document.querySelector('[name="Memo"]');
|
||||||
if (memo && !memo.value) memo.value = data.vendorName;
|
if (memo && !memo.value) memo.value = data.vendorName;
|
||||||
}
|
}
|
||||||
@@ -598,7 +598,7 @@
|
|||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal'));
|
const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal'));
|
||||||
if (modal) modal.hide();
|
if (modal) modal.hide();
|
||||||
|
|
||||||
statusEl.textContent = 'Scan complete — review and adjust as needed.';
|
statusEl.textContent = 'Scan complete — review and adjust as needed.';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = 'Error connecting to AI service.';
|
statusEl.textContent = 'Error connecting to AI service.';
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.Accounting.EditBillDto
|
@model PowderCoating.Application.DTOs.Accounting.EditBillDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Bill";
|
ViewData["Title"] = "Edit Bill";
|
||||||
ViewData["PageIcon"] = "bi-pencil-square";
|
ViewData["PageIcon"] = "bi-pencil-square";
|
||||||
ViewData["PageHelpTitle"] = "Edit Bill";
|
ViewData["PageHelpTitle"] = "Edit Bill";
|
||||||
ViewData["PageHelpContent"] = "Bills can only be edited while in Draft status. Once marked Open, they are locked — Void the bill and recreate it if corrections are needed after confirmation.";
|
ViewData["PageHelpContent"] = "Bills can only be edited while in Draft status. Once marked Open, they are locked — Void the bill and recreate it if corrections are needed after confirmation.";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-start mb-4">
|
<div class="d-flex justify-content-start mb-4">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<form asp-action="Edit" asp-route-id="@Model.Id" method="post" enctype="multipart/form-data" id="billForm">
|
<form asp-action="Edit" asp-route-id="@Model.Id" method="post" enctype="multipart/form-data" id="billForm">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input asp-for="Id" type="hidden" />
|
<input asp-for="Id" type="hidden" />
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Bill Details"
|
data-bs-title="Bill Details"
|
||||||
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation.">
|
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
|
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
|
||||||
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
|
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
|
||||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||||
<option value="">— Select Vendor —</option>
|
<option value="">— Select Vendor —</option>
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
<input type="file" name="receiptFile" id="receiptFile" class="form-control"
|
<input type="file" name="receiptFile" id="receiptFile" class="form-control"
|
||||||
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
|
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
|
||||||
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
|
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Line Items"
|
data-bs-title="Line Items"
|
||||||
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
|
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
||||||
data-bs-title="Bill Summary"
|
data-bs-title="Bill Summary"
|
||||||
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid.">
|
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
<tr class="line-item-row">
|
<tr class="line-item-row">
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
|
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
|
||||||
<option value="">— Account —</option>
|
<option value="">— Account —</option>
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
||||||
{
|
{
|
||||||
<option value="@item.Value">@item.Text</option>
|
<option value="@item.Value">@item.Text</option>
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
|
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
|
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
|
||||||
{
|
{
|
||||||
<option value="@item.Value">@item.Text</option>
|
<option value="@item.Value">@item.Text</option>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model List<PowderCoating.Application.DTOs.Accounting.BillExpenseListDto>
|
@model List<PowderCoating.Application.DTOs.Accounting.BillExpenseListDto>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Bills / Expenses";
|
ViewData["Title"] = "Bills / Expenses";
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
@if ((decimal)ViewBag.TotalOwed > 0)
|
@if ((decimal)ViewBag.TotalOwed > 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4">
|
||||||
<i class="bi bi-exclamation-circle fs-5"></i>
|
<i class="bi bi-exclamation-circle fs-5"></i>
|
||||||
<span>Outstanding bills: <strong>@(((decimal)ViewBag.TotalOwed).ToString("C"))</strong></span>
|
<span>Outstanding bills: <strong>@(((decimal)ViewBag.TotalOwed).ToString("C"))</strong></span>
|
||||||
<a asp-action="Index" asp-route-status="Unpaid" class="btn btn-sm btn-warning ms-auto">
|
<a asp-action="Index" asp-route-status="Unpaid" class="btn btn-sm btn-warning ms-auto">
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<form method="get" class="row g-2 align-items-end">
|
<form method="get" class="row g-2 align-items-end">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<input type="search" name="search" value="@ViewBag.Search" class="form-control"
|
<input type="search" name="search" value="@ViewBag.Search" class="form-control"
|
||||||
placeholder="Search by #, vendor, memo, amount…" />
|
placeholder="Search by #, vendor, memo, amount…" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<select name="type" class="form-select">
|
<select name="type" class="form-select">
|
||||||
@@ -153,13 +153,13 @@
|
|||||||
}
|
}
|
||||||
else if (entry.EntryType == "Expense")
|
else if (entry.EntryType == "Expense")
|
||||||
{
|
{
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td><span class="badge bg-@entry.StatusColor">@entry.StatusLabel</span></td>
|
<td><span class="badge bg-@entry.StatusColor">@entry.StatusLabel</span></td>
|
||||||
<td class="text-end">@entry.Total.ToString("C")</td>
|
<td class="text-end">@entry.Total.ToString("C")</td>
|
||||||
<td class="text-end fw-medium @(entry.BalanceDue > 0 ? "text-danger" : "text-muted")">
|
<td class="text-end fw-medium @(entry.BalanceDue > 0 ? "text-danger" : "text-muted")">
|
||||||
@(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "—")
|
@(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "—")
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (entry.EntryType == "Bill")
|
@if (entry.EntryType == "Bill")
|
||||||
@@ -206,7 +206,7 @@ else
|
|||||||
asp-route-status="@ViewBag.StatusFilter"
|
asp-route-status="@ViewBag.StatusFilter"
|
||||||
asp-route-search="@ViewBag.Search"
|
asp-route-search="@ViewBag.Search"
|
||||||
asp-route-page="@((int)ViewBag.Page - 1)"
|
asp-route-page="@((int)ViewBag.Page - 1)"
|
||||||
asp-route-pageSize="@ViewBag.PageSize">‹ Prev</a>
|
asp-route-pageSize="@ViewBag.PageSize">‹ Prev</a>
|
||||||
</li>
|
</li>
|
||||||
@for (var p = 1; p <= (int)ViewBag.TotalPages; p++)
|
@for (var p = 1; p <= (int)ViewBag.TotalPages; p++)
|
||||||
{
|
{
|
||||||
@@ -225,11 +225,11 @@ else
|
|||||||
asp-route-status="@ViewBag.StatusFilter"
|
asp-route-status="@ViewBag.StatusFilter"
|
||||||
asp-route-search="@ViewBag.Search"
|
asp-route-search="@ViewBag.Search"
|
||||||
asp-route-page="@((int)ViewBag.Page + 1)"
|
asp-route-page="@((int)ViewBag.Page + 1)"
|
||||||
asp-route-pageSize="@ViewBag.PageSize">Next ›</a>
|
asp-route-pageSize="@ViewBag.PageSize">Next ›</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-center text-muted small">
|
<p class="text-center text-muted small">
|
||||||
Showing @(((int)ViewBag.Page - 1) * (int)ViewBag.PageSize + 1)–@(Math.Min((int)ViewBag.Page * (int)ViewBag.PageSize, (int)ViewBag.TotalCount))
|
Showing @(((int)ViewBag.Page - 1) * (int)ViewBag.PageSize + 1)–@(Math.Min((int)ViewBag.Page * (int)ViewBag.PageSize, (int)ViewBag.TotalCount))
|
||||||
of @ViewBag.TotalCount entries
|
of @ViewBag.TotalCount entries
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@using PowderCoating.Web.Controllers
|
@using PowderCoating.Web.Controllers
|
||||||
@model BudgetCreateVm
|
@model BudgetCreateVm
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Edit Budget — {Model.Name}";
|
ViewData["Title"] = $"Edit Budget — {Model.Name}";
|
||||||
ViewData["PageIcon"] = "bi-pencil";
|
ViewData["PageIcon"] = "bi-pencil";
|
||||||
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-header bg-white border-0 py-3">
|
<div class="card-header bg-white border-0 py-3">
|
||||||
<h5 class="mb-0 fw-semibold">
|
<h5 class="mb-0 fw-semibold">
|
||||||
<i class="bi bi-pie-chart me-2 text-primary"></i>@Model.Name — @Model.FiscalYear
|
<i class="bi bi-pie-chart me-2 text-primary"></i>@Model.Name — @Model.FiscalYear
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info py-2 mx-3 mt-3 mb-0 small">
|
<div class="alert alert-info alert-permanent py-2 mx-3 mt-3 mb-0 small">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
Enter monthly amounts for each Revenue and Expense account. Leave a row at zero to exclude that account from the budget. Amounts represent expected <strong>activity</strong> for the period (not running totals).
|
Enter monthly amounts for each Revenue and Expense account. Leave a row at zero to exclude that account from the budget. Amounts represent expected <strong>activity</strong> for the period (not running totals).
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<input type="hidden" asp-for="CompanyName" />
|
<input type="hidden" asp-for="CompanyName" />
|
||||||
<input type="hidden" asp-for="CreatedAt" />
|
<input type="hidden" asp-for="CreatedAt" />
|
||||||
<input type="hidden" asp-for="CompanyId" />
|
<input type="hidden" asp-for="CompanyId" />
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent" role="alert"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="Title" class="form-label fw-semibold">
|
<label asp-for="Title" class="form-label fw-semibold">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.BugReport.CreateBugReportDto
|
@model PowderCoating.Application.DTOs.BugReport.CreateBugReportDto
|
||||||
@using PowderCoating.Core.Enums
|
@using PowderCoating.Core.Enums
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Report a Bug";
|
ViewData["Title"] = "Report a Bug";
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-action="Submit" method="post" enctype="multipart/form-data">
|
<form asp-action="Submit" method="post" enctype="multipart/form-data">
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent" role="alert"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="Title" class="form-label fw-semibold">
|
<label asp-for="Title" class="form-label fw-semibold">
|
||||||
@@ -59,10 +59,10 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label asp-for="Priority" class="form-label fw-semibold">Priority</label>
|
<label asp-for="Priority" class="form-label fw-semibold">Priority</label>
|
||||||
<select asp-for="Priority" class="form-select">
|
<select asp-for="Priority" class="form-select">
|
||||||
<option value="@((int)BugReportPriority.Low)">Low – Minor inconvenience, workaround exists</option>
|
<option value="@((int)BugReportPriority.Low)">Low – Minor inconvenience, workaround exists</option>
|
||||||
<option value="@((int)BugReportPriority.Normal)" selected>Normal – Affects workflow but not critical</option>
|
<option value="@((int)BugReportPriority.Normal)" selected>Normal – Affects workflow but not critical</option>
|
||||||
<option value="@((int)BugReportPriority.High)">High – Significantly impacts operations</option>
|
<option value="@((int)BugReportPriority.High)">High – Significantly impacts operations</option>
|
||||||
<option value="@((int)BugReportPriority.Critical)">Critical – System unusable or data loss risk</option>
|
<option value="@((int)BugReportPriority.Critical)">Critical – System unusable or data loss risk</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="Priority" class="text-danger small"></span>
|
<span asp-validation-for="Priority" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const sizeMb = (f.size / 1024 / 1024).toFixed(1);
|
const sizeMb = (f.size / 1024 / 1024).toFixed(1);
|
||||||
if (f.size > maxBytes) {
|
if (f.size > maxBytes) {
|
||||||
li.innerHTML = `<i class="bi bi-exclamation-triangle text-danger"></i> ${f.name} (${sizeMb} MB) — exceeds 100 MB limit`;
|
li.innerHTML = `<i class="bi bi-exclamation-triangle text-danger"></i> ${f.name} (${sizeMb} MB) — exceeds 100 MB limit`;
|
||||||
} else {
|
} else {
|
||||||
li.innerHTML = `<i class="bi bi-file-earmark text-secondary"></i> ${f.name} (${sizeMb} MB)`;
|
li.innerHTML = `<i class="bi bi-file-earmark text-secondary"></i> ${f.name} (${sizeMb} MB)`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Catalog.CategoryDto
|
@model PowderCoating.Application.DTOs.Catalog.CategoryDto
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Category";
|
ViewData["Title"] = "Delete Category";
|
||||||
var canDelete = ViewBag.CanDelete ?? false;
|
var canDelete = ViewBag.CanDelete ?? false;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if (!canDelete)
|
@if (!canDelete)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<i class="bi bi-x-circle-fill me-2"></i>
|
<i class="bi bi-x-circle-fill me-2"></i>
|
||||||
<strong>Cannot Delete:</strong> This category cannot be deleted because it contains @(hasItems ? "items" : "") @(hasItems && hasSubCategories ? "and" : "") @(hasSubCategories ? "subcategories" : "").
|
<strong>Cannot Delete:</strong> This category cannot be deleted because it contains @(hasItems ? "items" : "") @(hasItems && hasSubCategories ? "and" : "") @(hasSubCategories ? "subcategories" : "").
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Warning:</strong> You are about to delete this category. This action cannot be undone.
|
<strong>Warning:</strong> You are about to delete this category. This action cannot be undone.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Catalog.CatalogItemDto
|
@model PowderCoating.Application.DTOs.Catalog.CatalogItemDto
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Catalog Item";
|
ViewData["Title"] = "Delete Catalog Item";
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Warning:</strong> You are about to delete this catalog item. This action cannot be undone.
|
<strong>Warning:</strong> You are about to delete this catalog item. This action cannot be undone.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Company.CreateCompanyAdminDto
|
@model PowderCoating.Application.DTOs.Company.CreateCompanyAdminDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Company Admin";
|
ViewData["Title"] = "Create Company Admin";
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<h5 class="border-bottom pb-2 mb-3">
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
<i class="bi bi-building me-2 text-primary"></i>Company
|
<i class="bi bi-building me-2 text-primary"></i>Company
|
||||||
</h5>
|
</h5>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
Creating admin user for: <strong>@Model.CompanyName</strong>
|
Creating admin user for: <strong>@Model.CompanyName</strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Permissions Notice -->
|
<!-- Permissions Notice -->
|
||||||
<div class="alert alert-success mb-4">
|
<div class="alert alert-success alert-permanent mb-4">
|
||||||
<h6 class="alert-heading">
|
<h6 class="alert-heading">
|
||||||
<i class="bi bi-shield-check me-2"></i>Administrator Permissions
|
<i class="bi bi-shield-check me-2"></i>Administrator Permissions
|
||||||
</h6>
|
</h6>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Company.CompanyDto
|
@model PowderCoating.Application.DTOs.Company.CompanyDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = Model.CompanyName;
|
ViewData["Title"] = Model.CompanyName;
|
||||||
@@ -470,7 +470,7 @@
|
|||||||
<i class="bi bi-fire me-1"></i>Reset All Company Data
|
<i class="bi bi-fire me-1"></i>Reset All Company Data
|
||||||
</h6>
|
</h6>
|
||||||
<p class="text-muted small mb-0">
|
<p class="text-muted small mb-0">
|
||||||
Permanently deletes <strong>all business data</strong> — customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
|
Permanently deletes <strong>all business data</strong> — customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
|
||||||
The company record, user accounts, and system configuration are preserved.
|
The company record, user accounts, and system configuration are preserved.
|
||||||
Use this to wipe a migration and start fresh.
|
Use this to wipe a migration and start fresh.
|
||||||
</p>
|
</p>
|
||||||
@@ -491,7 +491,7 @@
|
|||||||
There is no going back.
|
There is no going back.
|
||||||
@if (Model.UserCount > 0)
|
@if (Model.UserCount > 0)
|
||||||
{
|
{
|
||||||
<br /><strong class="text-danger">This company has @Model.UserCount user(s) — remove them first.</strong>
|
<br /><strong class="text-danger">This company has @Model.UserCount user(s) — remove them first.</strong>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -523,7 +523,7 @@
|
|||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
|
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Loading…</span>
|
<span class="visually-hidden">Loading…</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -545,14 +545,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-sm table-borderless mb-0 small">
|
<table class="table table-sm table-borderless mb-0 small">
|
||||||
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role">—</td></tr>
|
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">—</td></tr>
|
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">—</td></tr>
|
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">—</td></tr>
|
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">—</td></tr>
|
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">—</td></tr>
|
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">—</td></tr>
|
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">—</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">—</td></tr>
|
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">—</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -680,7 +680,7 @@
|
|||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" id="resetUserId" name="id" />
|
<input type="hidden" id="resetUserId" name="id" />
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
You are about to reset the password for <strong id="resetUserName"></strong>.
|
You are about to reset the password for <strong id="resetUserName"></strong>.
|
||||||
</div>
|
</div>
|
||||||
@@ -706,7 +706,7 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
// Reset Data Modal — enable submit only when user types "DELETE"
|
// Reset Data Modal — enable submit only when user types "DELETE"
|
||||||
(function () {
|
(function () {
|
||||||
var input = document.getElementById('resetDataConfirmInput');
|
var input = document.getElementById('resetDataConfirmInput');
|
||||||
var hidden = document.getElementById('resetDataConfirmHidden');
|
var hidden = document.getElementById('resetDataConfirmHidden');
|
||||||
@@ -725,7 +725,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Hard Delete Modal — enable submit only when user types "DELETE"
|
// Hard Delete Modal — enable submit only when user types "DELETE"
|
||||||
(function () {
|
(function () {
|
||||||
var input = document.getElementById('hardDeleteConfirmInput');
|
var input = document.getElementById('hardDeleteConfirmInput');
|
||||||
var hidden = document.getElementById('hardDeleteConfirmHidden');
|
var hidden = document.getElementById('hardDeleteConfirmHidden');
|
||||||
@@ -760,7 +760,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── User Details Modal ────────────────────────────────────────────────
|
// ── User Details Modal ────────────────────────────────────────────────
|
||||||
(function () {
|
(function () {
|
||||||
const offcanvasEl = document.getElementById('userDetailOffcanvas');
|
const offcanvasEl = document.getElementById('userDetailOffcanvas');
|
||||||
const oc = new bootstrap.Modal(offcanvasEl);
|
const oc = new bootstrap.Modal(offcanvasEl);
|
||||||
@@ -806,12 +806,12 @@
|
|||||||
badge.textContent = u.isActive ? 'Active' : 'Inactive';
|
badge.textContent = u.isActive ? 'Active' : 'Inactive';
|
||||||
badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger');
|
badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger');
|
||||||
|
|
||||||
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '—';
|
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '—';
|
||||||
document.getElementById('oc-dept').textContent = u.department || '—';
|
document.getElementById('oc-dept').textContent = u.department || '—';
|
||||||
document.getElementById('oc-position').textContent = u.position || '—';
|
document.getElementById('oc-position').textContent = u.position || '—';
|
||||||
document.getElementById('oc-phone').textContent = u.phone || '—';
|
document.getElementById('oc-phone').textContent = u.phone || '—';
|
||||||
document.getElementById('oc-hire').textContent = u.hireDate || '—';
|
document.getElementById('oc-hire').textContent = u.hireDate || '—';
|
||||||
document.getElementById('oc-created').textContent = u.createdAt || '—';
|
document.getElementById('oc-created').textContent = u.createdAt || '—';
|
||||||
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
|
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
|
||||||
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
|
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
|
||||||
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
|
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
|
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
|
||||||
|
|
||||||
@section Styles {
|
@section Styles {
|
||||||
<style>
|
<style>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
<input type="text" name="searchTerm" class="form-control"
|
<input type="text" name="searchTerm" class="form-control"
|
||||||
placeholder="Search by name, code, email, phone…"
|
placeholder="Search by name, code, email, phone…"
|
||||||
value="@searchTerm" />
|
value="@searchTerm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
|
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
|
||||||
<div class="mobile-card-view">
|
<div class="mobile-card-view">
|
||||||
<div class="mobile-card-list">
|
<div class="mobile-card-list">
|
||||||
@foreach (var company in Model)
|
@foreach (var company in Model)
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-card-footer">
|
<div class="mobile-card-footer">
|
||||||
<span class="btn btn-sm btn-outline-primary">View →</span>
|
<span class="btn btn-sm btn-outline-primary">View →</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
{
|
{
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||||
<div class="text-muted small">
|
<div class="text-muted small">
|
||||||
Showing @((pageNumber - 1) * pageSize + 1)–@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
|
Showing @((pageNumber - 1) * pageSize + 1)–@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -413,10 +413,10 @@
|
|||||||
<h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6>
|
<h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6>
|
||||||
<p class="text-muted small mb-2">
|
<p class="text-muted small mb-2">
|
||||||
<strong class="text-danger">This cannot be undone.</strong>
|
<strong class="text-danger">This cannot be undone.</strong>
|
||||||
All company data — users, jobs, quotes, customers, invoices, and everything else — will be
|
All company data — users, jobs, quotes, customers, invoices, and everything else — will be
|
||||||
<strong>permanently and irreversibly deleted</strong> from the database.
|
<strong>permanently and irreversibly deleted</strong> from the database.
|
||||||
</p>
|
</p>
|
||||||
<div class="alert alert-danger py-2 mb-3">
|
<div class="alert alert-danger alert-permanent py-2 mb-3">
|
||||||
<i class="bi bi-exclamation-octagon-fill me-2"></i>
|
<i class="bi bi-exclamation-octagon-fill me-2"></i>
|
||||||
Type <strong>DELETE</strong> below to enable permanent deletion.
|
Type <strong>DELETE</strong> below to enable permanent deletion.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.User.CreateCompanyUserDto
|
@model PowderCoating.Application.DTOs.User.CreateCompanyUserDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Add New User";
|
ViewData["Title"] = "Add New User";
|
||||||
ViewData["PageIcon"] = "bi-person-plus";
|
ViewData["PageIcon"] = "bi-person-plus";
|
||||||
ViewData["PageHelpTitle"] = "Add New User";
|
ViewData["PageHelpTitle"] = "Add New User";
|
||||||
ViewData["PageHelpContent"] = "Creates a new login account for a member of your company. The email address doubles as the login username. Set a temporary password — the user can change it from their Profile page after their first login. Assign a Role, then fine-tune individual permissions below.";
|
ViewData["PageHelpContent"] = "Creates a new login account for a member of your company. The email address doubles as the login username. Set a temporary password — the user can change it from their Profile page after their first login. Assign a Role, then fine-tune individual permissions below.";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Basic Information"
|
data-bs-title="Basic Information"
|
||||||
data-bs-content="First Name, Last Name, and Email are required. The email is used as the login username — it must be unique across the system. Employee Number is an optional internal reference. The user can update their name and phone from their own Profile page after logging in.">
|
data-bs-content="First Name, Last Name, and Email are required. The email is used as the login username — it must be unique across the system. Employee Number is an optional internal reference. The user can update their name and phone from their own Profile page after logging in.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="companyAdminAlert" class="alert alert-info" style="display: none;">
|
<div id="companyAdminAlert" class="alert alert-info alert-permanent" style="display: none;">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<strong>Company Admins automatically have all permissions.</strong> These checkboxes are disabled because Company Admins always have full access to all features.
|
<strong>Company Admins automatically have all permissions.</strong> These checkboxes are disabled because Company Admins always have full access to all features.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.User.UpdateCompanyUserDto
|
@model PowderCoating.Application.DTOs.User.UpdateCompanyUserDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit User";
|
ViewData["Title"] = "Edit User";
|
||||||
ViewData["PageIcon"] = "bi-person-gear";
|
ViewData["PageIcon"] = "bi-person-gear";
|
||||||
ViewData["PageHelpTitle"] = "Edit User";
|
ViewData["PageHelpTitle"] = "Edit User";
|
||||||
ViewData["PageHelpContent"] = "Update this user's account details, role, and permissions. Unchecking User Active prevents the user from logging in without deleting their account or history. Changing the email here also changes their login username — notify them so they can log in with the new address.";
|
ViewData["PageHelpContent"] = "Update this user's account details, role, and permissions. Unchecking User Active prevents the user from logging in without deleting their account or history. Changing the email here also changes their login username — notify them so they can log in with the new address.";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Basic Information"
|
data-bs-title="Basic Information"
|
||||||
data-bs-content="Email is this user's login username — changing it here means they must use the new address to log in. User Active controls whether the account can sign in; deactivating preserves all data without deleting the account. To reset a password, the user can use Forgot Password on the login page, or a SuperAdmin can set one directly.">
|
data-bs-content="Email is this user's login username — changing it here means they must use the new address to log in. User Active controls whether the account can sign in; deactivating preserves all data without deleting the account. To reset a password, the user can use Forgot Password on the login page, or a SuperAdmin can set one directly.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Role & Department"
|
data-bs-title="Role & Department"
|
||||||
data-bs-content="Changing the Role updates the user's base access level immediately on save. Termination Date is informational — to actually prevent login, also uncheck User Active above. Department and Position appear on the user's profile card and in the Manage Users list.">
|
data-bs-content="Changing the Role updates the user's base access level immediately on save. Termination Date is informational — to actually prevent login, also uncheck User Active above. Department and Position appear on the user's profile card and in the Manage Users list.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="companyAdminAlert" class="alert alert-info" style="display: none;">
|
<div id="companyAdminAlert" class="alert alert-info alert-permanent" style="display: none;">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<strong>Company Admins automatically have all permissions.</strong> These checkboxes are disabled because Company Admins always have full access to all features.
|
<strong>Company Admins automatically have all permissions.</strong> These checkboxes are disabled because Company Admins always have full access to all features.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PagedResult<PowderCoating.Application.DTOs.User.CompanyUserListDto>
|
@model PagedResult<PowderCoating.Application.DTOs.User.CompanyUserListDto>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Manage Users";
|
ViewData["Title"] = "Manage Users";
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" name="id" id="banUserId" />
|
<input type="hidden" name="id" id="banUserId" />
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
Banning <strong id="banUserName"></strong> will immediately prevent them from logging in.
|
Banning <strong id="banUserName"></strong> will immediately prevent them from logging in.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Customer.CustomerDto
|
@model PowderCoating.Application.DTOs.Customer.CustomerDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Customer";
|
ViewData["Title"] = "Delete Customer";
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warning Banner -->
|
<!-- Warning Banner -->
|
||||||
<div class="alert alert-danger d-flex align-items-start mb-4">
|
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="alert-heading mb-2">Are you sure you want to delete this customer?</h5>
|
<h5 class="alert-heading mb-2">Are you sure you want to delete this customer?</h5>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
@if (Model.CurrentBalance > 0)
|
@if (Model.CurrentBalance > 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center">
|
||||||
<i class="bi bi-exclamation-circle me-2"></i>
|
<i class="bi bi-exclamation-circle me-2"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>Warning:</strong> This customer has an outstanding balance of @Model.CurrentBalance.ToString("C"). Please ensure all balances are settled before deletion.
|
<strong>Warning:</strong> This customer has an outstanding balance of @Model.CurrentBalance.ToString("C"). Please ensure all balances are settled before deletion.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Customer.CustomerDto
|
@model PowderCoating.Application.DTOs.Customer.CustomerDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = !string.IsNullOrWhiteSpace(Model.CompanyName)
|
ViewData["Title"] = !string.IsNullOrWhiteSpace(Model.CompanyName)
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">Not set — invoices go to contact email</span>
|
<span class="text-muted">Not set — invoices go to contact email</span>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
{
|
{
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
|
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
|
||||||
<div class="alert alert-success d-flex justify-content-between align-items-center mb-0 mt-2">
|
<div class="alert alert-success alert-permanent d-flex justify-content-between align-items-center mb-0 mt-2">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-file-earmark-check me-2"></i>
|
<i class="bi bi-file-earmark-check me-2"></i>
|
||||||
<strong>File on record:</strong> @Model.TaxExemptCertificateFileName
|
<strong>File on record:</strong> @Model.TaxExemptCertificateFileName
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
{
|
{
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
|
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
|
||||||
<div class="alert alert-warning mb-0 mt-2">
|
<div class="alert alert-warning alert-permanent mb-0 mt-2">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong>No certificate on file.</strong> Please upload a tax exempt certificate to complete the record.
|
<strong>No certificate on file.</strong> Please upload a tax exempt certificate to complete the record.
|
||||||
</div>
|
</div>
|
||||||
@@ -514,7 +514,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Reason <span class="text-danger">*</span></label>
|
<label class="form-label fw-semibold">Reason <span class="text-danger">*</span></label>
|
||||||
<select name="Reason" class="form-select" required id="creditReasonSelect">
|
<select name="Reason" class="form-select" required id="creditReasonSelect">
|
||||||
<option value="">— Select reason —</option>
|
<option value="">— Select reason —</option>
|
||||||
<option value="Pre-payment / Deposit">Pre-payment / Deposit</option>
|
<option value="Pre-payment / Deposit">Pre-payment / Deposit</option>
|
||||||
<option value="Gift / Gift Card">Gift / Gift Card</option>
|
<option value="Gift / Gift Card">Gift / Gift Card</option>
|
||||||
<option value="Overpayment credit">Overpayment credit</option>
|
<option value="Overpayment credit">Overpayment credit</option>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Customer.UpdateCustomerDto
|
@model PowderCoating.Application.DTOs.Customer.UpdateCustomerDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Customer";
|
ViewData["Title"] = "Edit Customer";
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Company Information"
|
data-bs-title="Company Information"
|
||||||
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Customer Type controls which features are available — Commercial customers get payment terms, credit limits, and pricing tier discounts. Status Inactive hides the customer from new quote/job dropdowns but preserves all history.">
|
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Customer Type controls which features are available — Commercial customers get payment terms, credit limits, and pricing tier discounts. Status Inactive hides the customer from new quote/job dropdowns but preserves all history.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Business Information"
|
data-bs-title="Business Information"
|
||||||
data-bs-content="Payment Terms sets the default due date on invoices (e.g., Net 30 = 30 days from invoice date). Credit Limit is a soft warning cap — the system alerts when exceeded. Tax Exempt removes tax from all invoices; upload the exemption certificate in the Tax Exempt Certificate section below.">
|
data-bs-content="Payment Terms sets the default due date on invoices (e.g., Net 30 = 30 days from invoice date). Credit Limit is a soft warning cap — the system alerts when exceeded. Tax Exempt removes tax from all invoices; upload the exemption certificate in the Tax Exempt Certificate section below.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label asp-for="PricingTierId" class="form-label">Pricing Tier</label>
|
<label asp-for="PricingTierId" class="form-label">Pricing Tier</label>
|
||||||
<select asp-for="PricingTierId" asp-items="ViewBag.PricingTiers" class="form-select">
|
<select asp-for="PricingTierId" asp-items="ViewBag.PricingTiers" class="form-select">
|
||||||
<option value="">— No tier —</option>
|
<option value="">— No tier —</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="text-muted">Applies a discount to all quotes for this customer.</small>
|
<small class="text-muted">Applies a discount to all quotes for this customer.</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Notification Preferences"
|
data-bs-title="Notification Preferences"
|
||||||
data-bs-content="Controls when the customer receives automatic updates. Email notifications send status change alerts (e.g., job ready for pickup) to the customer's email address. SMS requires separate TCPA consent — uncheck 'SMS Notifications Active' to temporarily pause without revoking consent.">
|
data-bs-content="Controls when the customer receives automatic updates. Email notifications send status change alerts (e.g., job ready for pickup) to the customer's email address. SMS requires separate TCPA consent — uncheck 'SMS Notifications Active' to temporarily pause without revoking consent.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@if (Model.SmsConsentedAt.HasValue)
|
@if (Model.SmsConsentedAt.HasValue)
|
||||||
{
|
{
|
||||||
<!-- Consent already recorded — show status and allow pause/resume -->
|
<!-- Consent already recorded — show status and allow pause/resume -->
|
||||||
<div class="card border-success bg-success-subtle p-3 mb-2">
|
<div class="card border-success bg-success-subtle p-3 mb-2">
|
||||||
<div class="d-flex align-items-start gap-3">
|
<div class="d-flex align-items-start gap-3">
|
||||||
<i class="bi bi-shield-fill-check text-success fs-4 mt-1"></i>
|
<i class="bi bi-shield-fill-check text-success fs-4 mt-1"></i>
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<!-- No consent on file — show the compliance notice and consent checkbox -->
|
<!-- No consent on file — show the compliance notice and consent checkbox -->
|
||||||
<div class="alert alert-warning border-warning alert-permanent" role="alert">
|
<div class="alert alert-warning border-warning alert-permanent" role="alert">
|
||||||
<h6 class="alert-heading fw-bold mb-2">
|
<h6 class="alert-heading fw-bold mb-2">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>SMS Consent Requirement (TCPA)
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>SMS Consent Requirement (TCPA)
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@if (Model.HasTaxExemptCertificate)
|
@if (Model.HasTaxExemptCertificate)
|
||||||
{
|
{
|
||||||
<div class="alert alert-success d-flex justify-content-between align-items-center">
|
<div class="alert alert-success alert-permanent d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-file-earmark-check me-2"></i>
|
<i class="bi bi-file-earmark-check me-2"></i>
|
||||||
<strong>Certificate on file:</strong> @Model.TaxExemptCertificateFileName
|
<strong>Certificate on file:</strong> @Model.TaxExemptCertificateFileName
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
|
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
|
||||||
@using Microsoft.AspNetCore.Html
|
@using Microsoft.AspNetCore.Html
|
||||||
@using PowderCoating.Application.DTOs.Health
|
@using PowderCoating.Application.DTOs.Health
|
||||||
@using PowderCoating.Web.ViewModels.Dashboard
|
@using PowderCoating.Web.ViewModels.Dashboard
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<p class="mb-3" style="font-family:var(--font-display);font-size:1.35rem;font-weight:500;line-height:1.4;color:var(--pcl-ink);">
|
<p class="mb-3" style="font-family:var(--font-display);font-size:1.35rem;font-weight:500;line-height:1.4;color:var(--pcl-ink);">
|
||||||
@if (_attnCount > 0)
|
@if (_attnCount > 0)
|
||||||
{
|
{
|
||||||
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
|
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* PWA install banner — rendered by JS only on mobile, hidden once dismissed or already installed *@
|
@* PWA install banner — rendered by JS only on mobile, hidden once dismissed or already installed *@
|
||||||
<div id="pwa-install-banner" class="row mb-4" style="display:none!important;">
|
<div id="pwa-install-banner" class="row mb-4" style="display:none!important;">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-permanent mb-0 d-flex align-items-start gap-3 py-3"
|
<div class="alert alert-permanent mb-0 d-flex align-items-start gap-3 py-3"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
|
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Config health alert — only shown when there are setup gaps *@
|
@* Config health alert — only shown when there are setup gaps *@
|
||||||
@if (configHealth != null && !configHealth.IsHealthy)
|
@if (configHealth != null && !configHealth.IsHealthy)
|
||||||
{
|
{
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@@ -406,15 +406,15 @@
|
|||||||
}
|
}
|
||||||
@if (Model.AgingDays1To30 > 0)
|
@if (Model.AgingDays1To30 > 0)
|
||||||
{
|
{
|
||||||
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>1–30d @Model.AgingDays1To30.ToString("C0")</span>
|
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>1–30d @Model.AgingDays1To30.ToString("C0")</span>
|
||||||
}
|
}
|
||||||
@if (Model.AgingDays31To60 > 0)
|
@if (Model.AgingDays31To60 > 0)
|
||||||
{
|
{
|
||||||
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>31–60d @Model.AgingDays31To60.ToString("C0")</span>
|
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>31–60d @Model.AgingDays31To60.ToString("C0")</span>
|
||||||
}
|
}
|
||||||
@if (Model.AgingDays61To90 > 0)
|
@if (Model.AgingDays61To90 > 0)
|
||||||
{
|
{
|
||||||
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>61–90d @Model.AgingDays61To90.ToString("C0")</span>
|
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>61–90d @Model.AgingDays61To90.ToString("C0")</span>
|
||||||
}
|
}
|
||||||
@if (Model.AgingDaysOver90 > 0)
|
@if (Model.AgingDaysOver90 > 0)
|
||||||
{
|
{
|
||||||
@@ -536,7 +536,7 @@
|
|||||||
@if (line.EstCost.HasValue)
|
@if (line.EstCost.HasValue)
|
||||||
{<span>@line.EstCost.Value.ToString("C")</span>}
|
{<span>@line.EstCost.Value.ToString("C")</span>}
|
||||||
else
|
else
|
||||||
{<span class="text-muted">—</span>}
|
{<span class="text-muted">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
|
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
|
||||||
@@ -552,7 +552,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">Vendor Total</td>
|
<td colspan="2">Vendor Total</td>
|
||||||
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
|
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
|
||||||
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
|
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@@ -571,7 +571,7 @@
|
|||||||
<div class="card border-0 shadow-sm dashboard-card" style="border-left: 4px solid #0d6efd !important;">
|
<div class="card border-0 shadow-sm dashboard-card" style="border-left: 4px solid #0d6efd !important;">
|
||||||
<div class="card-header bg-body border-0 d-flex justify-content-between align-items-center pt-4 pb-3">
|
<div class="card-header bg-body border-0 d-flex justify-content-between align-items-center pt-4 pb-3">
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered — Awaiting Receipt
|
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered — Awaiting Receipt
|
||||||
<span class="ms-2 text-muted fw-normal small" id="placed-count-label">@Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")</span>
|
<span class="ms-2 text-muted fw-normal small" id="placed-count-label">@Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")</span>
|
||||||
</h5>
|
</h5>
|
||||||
<small class="text-muted">Grouped by vendor · Enter lbs received to update inventory</small>
|
<small class="text-muted">Grouped by vendor · Enter lbs received to update inventory</small>
|
||||||
@@ -630,13 +630,13 @@
|
|||||||
@if (line.EstCost.HasValue)
|
@if (line.EstCost.HasValue)
|
||||||
{<span>@line.EstCost.Value.ToString("C")</span>}
|
{<span>@line.EstCost.Value.ToString("C")</span>}
|
||||||
else
|
else
|
||||||
{<span class="text-muted">—</span>}
|
{<span class="text-muted">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted small">
|
<td class="text-muted small">
|
||||||
@if (line.OrderedAt.HasValue)
|
@if (line.OrderedAt.HasValue)
|
||||||
{<span title="@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("g")">@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")</span>}
|
{<span title="@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("g")">@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")</span>}
|
||||||
else
|
else
|
||||||
{<span>—</span>}
|
{<span>—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
|
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
|
||||||
@@ -669,7 +669,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">Vendor Total</td>
|
<td colspan="2">Vendor Total</td>
|
||||||
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
|
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
|
||||||
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
|
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
|
||||||
<td colspan="2"></td>
|
<td colspan="2"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@@ -704,7 +704,7 @@
|
|||||||
|
|
||||||
<div id="apm-ai-status" class="d-none py-2 small mb-3"></div>
|
<div id="apm-ai-status" class="d-none py-2 small mb-3"></div>
|
||||||
|
|
||||||
<div class="alert alert-info py-2 small mb-3">
|
<div class="alert alert-info alert-permanent py-2 small mb-3">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
Fields pre-filled from the powder order. Fill in any missing details then click Save.
|
Fields pre-filled from the powder order. Fill in any missing details then click Save.
|
||||||
</div>
|
</div>
|
||||||
@@ -739,7 +739,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-medium">Category</label>
|
<label class="form-label fw-medium">Category</label>
|
||||||
<select class="form-select" id="apm-categoryId" name="inventoryCategoryId">
|
<select class="form-select" id="apm-categoryId" name="inventoryCategoryId">
|
||||||
<option value="">— Select category —</option>
|
<option value="">— Select category —</option>
|
||||||
@if (ViewBag.InventoryCategories != null)
|
@if (ViewBag.InventoryCategories != null)
|
||||||
{
|
{
|
||||||
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
|
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
|
||||||
@@ -753,7 +753,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-medium">Primary Vendor</label>
|
<label class="form-label fw-medium">Primary Vendor</label>
|
||||||
<select class="form-select" id="apm-vendorId" name="primaryVendorId">
|
<select class="form-select" id="apm-vendorId" name="primaryVendorId">
|
||||||
<option value="">— Select vendor —</option>
|
<option value="">— Select vendor —</option>
|
||||||
@if (ViewBag.VendorList != null)
|
@if (ViewBag.VendorList != null)
|
||||||
{
|
{
|
||||||
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
|
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
|
||||||
@@ -883,7 +883,7 @@
|
|||||||
|
|
||||||
const esc = s => s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : '';
|
const esc = s => s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : '';
|
||||||
const estCost = (c.lbsToOrder && c.costPerLb) ? (c.lbsToOrder * c.costPerLb) : null;
|
const estCost = (c.lbsToOrder && c.costPerLb) ? (c.lbsToOrder * c.costPerLb) : null;
|
||||||
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '—';
|
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '—';
|
||||||
const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
|
const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
|
||||||
|
|
||||||
// Find or create vendor group
|
// Find or create vendor group
|
||||||
@@ -928,7 +928,7 @@
|
|||||||
${c.finish ? `<span class="badge bg-light text-dark border ms-1">${esc(c.finish)}</span>` : ''}
|
${c.finish ? `<span class="badge bg-light text-dark border ms-1">${esc(c.finish)}</span>` : ''}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-medium">${lbsFmt} lbs</td>
|
<td class="text-end fw-medium">${lbsFmt} lbs</td>
|
||||||
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted">—</span>'}</td>
|
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted">—</span>'}</td>
|
||||||
<td class="text-muted small">${orderedDate}</td>
|
<td class="text-muted small">${orderedDate}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-${c.coatId}">
|
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-${c.coatId}">
|
||||||
@@ -979,7 +979,7 @@
|
|||||||
}
|
}
|
||||||
qtyInput.classList.remove('is-invalid');
|
qtyInput.classList.remove('is-invalid');
|
||||||
|
|
||||||
// Custom powder (no inventory item) → open modal to add to inventory
|
// Custom powder (no inventory item) → open modal to add to inventory
|
||||||
if (!hasInv) {
|
if (!hasInv) {
|
||||||
const modal = document.getElementById('addPowderModal');
|
const modal = document.getElementById('addPowderModal');
|
||||||
// Pre-fill hidden + text fields
|
// Pre-fill hidden + text fields
|
||||||
@@ -1024,7 +1024,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inventory item exists → receive directly
|
// Inventory item exists → receive directly
|
||||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
|
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
|
||||||
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
|
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
|
||||||
|
|
||||||
@@ -1065,7 +1065,7 @@
|
|||||||
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
|
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
|
||||||
|
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
|
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', {
|
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', {
|
||||||
@@ -1094,7 +1094,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
|
// ââ€â‚¬Ã¢â€â‚¬ AI Lookup for Add Powder modal ââ€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬
|
||||||
(function () {
|
(function () {
|
||||||
const apmBtn = document.getElementById('apm-ai-btn');
|
const apmBtn = document.getElementById('apm-ai-btn');
|
||||||
const apmStatusEl = document.getElementById('apm-ai-status');
|
const apmStatusEl = document.getElementById('apm-ai-status');
|
||||||
@@ -1144,7 +1144,7 @@
|
|||||||
|
|
||||||
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
|
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
|
||||||
if (!hasInput) {
|
if (!hasInput) {
|
||||||
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field — Manufacturer, Color Name, Color Code, or Item Name — then try again.');
|
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field — Manufacturer, Color Name, Color Code, or Item Name — then try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,7 +1153,7 @@
|
|||||||
document.getElementById('apm-bad-match-btn')?.remove();
|
document.getElementById('apm-bad-match-btn')?.remove();
|
||||||
apmBtn.disabled = true;
|
apmBtn.disabled = true;
|
||||||
apmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...';
|
apmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...';
|
||||||
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications…');
|
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications…');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -1220,7 +1220,7 @@
|
|||||||
: '';
|
: '';
|
||||||
apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`);
|
apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`);
|
||||||
} else {
|
} else {
|
||||||
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill — they may already be populated, or the product wasn\'t found.');
|
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill — they may already be populated, or the product wasn\'t found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1274,7 +1274,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed';
|
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed';
|
||||||
|
|
||||||
// Already installed as standalone — never show
|
// Already installed as standalone — never show
|
||||||
var isStandalone = window.navigator.standalone === true ||
|
var isStandalone = window.navigator.standalone === true ||
|
||||||
window.matchMedia('(display-mode: standalone)').matches;
|
window.matchMedia('(display-mode: standalone)').matches;
|
||||||
if (isStandalone) return;
|
if (isStandalone) return;
|
||||||
@@ -1298,7 +1298,7 @@
|
|||||||
var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua);
|
var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua);
|
||||||
if (isSafari) {
|
if (isSafari) {
|
||||||
titleEl.textContent = 'Add to Home Screen';
|
titleEl.textContent = 'Add to Home Screen';
|
||||||
msgEl.innerHTML = 'For the best experience — and so the camera only asks once — open the ' +
|
msgEl.innerHTML = 'For the best experience — and so the camera only asks once — open the ' +
|
||||||
'<strong>Share menu</strong> <span style="font-size:1.1em">▲</span> at the bottom of Safari ' +
|
'<strong>Share menu</strong> <span style="font-size:1.1em">▲</span> at the bottom of Safari ' +
|
||||||
'and tap <strong>Add to Home Screen</strong>.';
|
'and tap <strong>Add to Home Screen</strong>.';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Web.Controllers
|
@using PowderCoating.Web.Controllers
|
||||||
@model List<EntityPurgeStat>
|
@model List<EntityPurgeStat>
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Data Purge & Cleanup";
|
ViewData["Title"] = "Data Purge & Cleanup";
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@* Warning banner *@
|
@* Warning banner *@
|
||||||
<div class="alert alert-warning d-flex gap-3 align-items-start mb-3">
|
<div class="alert alert-warning alert-permanent d-flex gap-3 align-items-start mb-3">
|
||||||
<i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i>
|
<i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>Destructive operation — this cannot be undone.</strong>
|
<strong>Destructive operation — this cannot be undone.</strong>
|
||||||
Purging permanently deletes records from the database. Soft-deleted records are hidden from users but still occupy database space. Use this tool periodically to reclaim space and keep the database clean.
|
Purging permanently deletes records from the database. Soft-deleted records are hidden from users but still occupy database space. Use this tool periodically to reclaim space and keep the database clean.
|
||||||
Job photo blobs in Azure Storage are also deleted when purging job photo records.
|
Job photo blobs in Azure Storage are also deleted when purging job photo records.
|
||||||
</div>
|
</div>
|
||||||
@@ -83,8 +83,8 @@
|
|||||||
<th style="width:36px"></th>
|
<th style="width:36px"></th>
|
||||||
<th>Entity</th>
|
<th>Entity</th>
|
||||||
<th class="text-end" style="width:90px">Total</th>
|
<th class="text-end" style="width:90px">Total</th>
|
||||||
<th class="text-end" style="width:100px">0–30d</th>
|
<th class="text-end" style="width:100px">0–30d</th>
|
||||||
<th class="text-end" style="width:100px">30–90d</th>
|
<th class="text-end" style="width:100px">30–90d</th>
|
||||||
<th class="text-end" style="width:100px">>90d</th>
|
<th class="text-end" style="width:100px">>90d</th>
|
||||||
<th style="width:130px">Oldest</th>
|
<th style="width:130px">Oldest</th>
|
||||||
<th style="width:42px">
|
<th style="width:42px">
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@@ -117,24 +117,24 @@
|
|||||||
{
|
{
|
||||||
<span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span>
|
<span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span>
|
||||||
}
|
}
|
||||||
else { <span class="text-muted">—</span> }
|
else { <span class="text-muted">—</span> }
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@if (s.Deleted30To90Days > 0)
|
@if (s.Deleted30To90Days > 0)
|
||||||
{
|
{
|
||||||
<span class="badge bg-warning-subtle text-warning">@s.Deleted30To90Days</span>
|
<span class="badge bg-warning-subtle text-warning">@s.Deleted30To90Days</span>
|
||||||
}
|
}
|
||||||
else { <span class="text-muted">—</span> }
|
else { <span class="text-muted">—</span> }
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@if (s.DeletedOlderThan90Days > 0)
|
@if (s.DeletedOlderThan90Days > 0)
|
||||||
{
|
{
|
||||||
<span class="badge bg-danger-subtle text-danger">@s.DeletedOlderThan90Days</span>
|
<span class="badge bg-danger-subtle text-danger">@s.DeletedOlderThan90Days</span>
|
||||||
}
|
}
|
||||||
else { <span class="text-muted">—</span> }
|
else { <span class="text-muted">—</span> }
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted">
|
<td class="text-muted">
|
||||||
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")
|
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<input type="checkbox" class="form-check-input entity-select"
|
<input type="checkbox" class="form-check-input entity-select"
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile card view for this group — shown on screens < 992px -->
|
<!-- Mobile card view for this group — shown on screens < 992px -->
|
||||||
<div class="mobile-card-view">
|
<div class="mobile-card-view">
|
||||||
<div class="px-3 pt-2 pb-1">
|
<div class="px-3 pt-2 pb-1">
|
||||||
<span class="text-muted text-uppercase fw-semibold" style="font-size:0.7rem;letter-spacing:.05em">@group.Key</span>
|
<span class="text-muted text-uppercase fw-semibold" style="font-size:0.7rem;letter-spacing:.05em">@group.Key</span>
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mobile-card-title">
|
<div class="mobile-card-title">
|
||||||
<h6>@s.Label</h6>
|
<h6>@s.Label</h6>
|
||||||
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")</small>
|
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-card-body">
|
<div class="mobile-card-body">
|
||||||
@@ -175,11 +175,11 @@
|
|||||||
{
|
{
|
||||||
<span class="badge bg-secondary">@s.Total</span>
|
<span class="badge bg-secondary">@s.Total</span>
|
||||||
}
|
}
|
||||||
else { <span class="text-muted">—</span> }
|
else { <span class="text-muted">—</span> }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-card-row">
|
<div class="mobile-card-row">
|
||||||
<span class="mobile-card-label">0–30d / 30–90d / >90d</span>
|
<span class="mobile-card-label">0–30d / 30–90d / >90d</span>
|
||||||
<span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span>
|
<span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||||
const confirmSummary= document.getElementById('confirmSummary');
|
const confirmSummary= document.getElementById('confirmSummary');
|
||||||
|
|
||||||
// ── Select all ──────────────────────────────────────────────────────────
|
// ── Select all ──────────────────────────────────────────────────────────
|
||||||
selectAll.addEventListener('change', () => {
|
selectAll.addEventListener('change', () => {
|
||||||
document.querySelectorAll('.entity-select:not(:disabled)').forEach(cb => {
|
document.querySelectorAll('.entity-select:not(:disabled)').forEach(cb => {
|
||||||
cb.checked = selectAll.checked;
|
cb.checked = selectAll.checked;
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
updatePurgeBtn();
|
updatePurgeBtn();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Group select all ────────────────────────────────────────────────────
|
// ── Group select all ────────────────────────────────────────────────────
|
||||||
document.querySelectorAll('.group-select-all').forEach(ga => {
|
document.querySelectorAll('.group-select-all').forEach(ga => {
|
||||||
ga.addEventListener('change', () => {
|
ga.addEventListener('change', () => {
|
||||||
document.querySelectorAll(`.entity-select[data-group="${ga.dataset.group}"]:not(:disabled)`)
|
document.querySelectorAll(`.entity-select[data-group="${ga.dataset.group}"]:not(:disabled)`)
|
||||||
@@ -335,7 +335,7 @@
|
|||||||
previewRes.classList.add('d-none');
|
previewRes.classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Preview ─────────────────────────────────────────────────────────────
|
// ── Preview ─────────────────────────────────────────────────────────────
|
||||||
previewBtn.addEventListener('click', async () => {
|
previewBtn.addEventListener('click', async () => {
|
||||||
const entities = getSelectedEntities();
|
const entities = getSelectedEntities();
|
||||||
if (!entities.length) {
|
if (!entities.length) {
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
previewBtn.disabled = true;
|
previewBtn.disabled = true;
|
||||||
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading…';
|
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading…';
|
||||||
|
|
||||||
const days = document.getElementById('olderThanDays').value;
|
const days = document.getElementById('olderThanDays').value;
|
||||||
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Purge button → modal ────────────────────────────────────────────────
|
// ── Purge button → modal ────────────────────────────────────────────────
|
||||||
purgeBtn.addEventListener('click', () => {
|
purgeBtn.addEventListener('click', () => {
|
||||||
const entities = getSelectedEntities();
|
const entities = getSelectedEntities();
|
||||||
const days = document.getElementById('olderThanDays').value;
|
const days = document.getElementById('olderThanDays').value;
|
||||||
@@ -390,7 +390,7 @@
|
|||||||
confirmModal.show();
|
confirmModal.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Confirm → submit form ───────────────────────────────────────────────
|
// ── Confirm → submit form ───────────────────────────────────────────────
|
||||||
document.getElementById('confirmPurgeBtn').addEventListener('click', () => {
|
document.getElementById('confirmPurgeBtn').addEventListener('click', () => {
|
||||||
const entities = getSelectedEntities();
|
const entities = getSelectedEntities();
|
||||||
const days = document.getElementById('olderThanDays').value;
|
const days = document.getElementById('olderThanDays').value;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Web.Controllers.DiagnosticsInfo
|
@model PowderCoating.Web.Controllers.DiagnosticsInfo
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "System Diagnostics";
|
ViewData["Title"] = "System Diagnostics";
|
||||||
ViewData["PageIcon"] = "bi-activity";
|
ViewData["PageIcon"] = "bi-activity";
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
<td>
|
<td>
|
||||||
@if (Model.CanWriteToAppPath)
|
@if (Model.CanWriteToAppPath)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success">✓ YES</span>
|
<span class="badge bg-success">✓ YES</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-danger">✗ NO - PERMISSION ISSUE</span>
|
<span class="badge bg-danger">✗ NO - PERMISSION ISSUE</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -60,11 +60,11 @@
|
|||||||
<td>
|
<td>
|
||||||
@if (Model.LogsDirectoryExists)
|
@if (Model.LogsDirectoryExists)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success">✓ YES</span>
|
<span class="badge bg-success">✓ YES</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-warning">✗ NO</span>
|
<span class="badge bg-warning">✗ NO</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -73,11 +73,11 @@
|
|||||||
<td>
|
<td>
|
||||||
@if (Model.CanWriteToLogsPath)
|
@if (Model.CanWriteToLogsPath)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success">✓ YES</span>
|
<span class="badge bg-success">✓ YES</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-danger">✗ NO - PERMISSION ISSUE</span>
|
<span class="badge bg-danger">✗ NO - PERMISSION ISSUE</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<p><strong>Message:</strong> @Model.LoggingTestMessage</p>
|
<p><strong>Message:</strong> @Model.LoggingTestMessage</p>
|
||||||
@if (Model.LoggingTestSuccess)
|
@if (Model.LoggingTestSuccess)
|
||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle"></i> A test log entry was written. Check the log files below to see if it appears.
|
<i class="bi bi-info-circle"></i> A test log entry was written. Check the log files below to see if it appears.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if (Model.LogFilesError != null)
|
@if (Model.LogFilesError != null)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<strong>Error reading log files:</strong> @Model.LogFilesError
|
<strong>Error reading log files:</strong> @Model.LogFilesError
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle"></i> <strong>No log files found!</strong>
|
<i class="bi bi-exclamation-triangle"></i> <strong>No log files found!</strong>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="mb-0">This means logs are not being written. Check the permissions above.</p>
|
<p class="mb-0">This means logs are not being written. Check the permissions above.</p>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<h6><i class="bi bi-lightbulb"></i> Troubleshooting Tips</h6>
|
<h6><i class="bi bi-lightbulb"></i> Troubleshooting Tips</h6>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>If "Can Write to Logs" shows NO, the IIS Application Pool doesn't have write permissions</li>
|
<li>If "Can Write to Logs" shows NO, the IIS Application Pool doesn't have write permissions</li>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Web.Controllers.LogViewerModel
|
@model PowderCoating.Web.Controllers.LogViewerModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Log Viewer";
|
ViewData["Title"] = "Log Viewer";
|
||||||
ViewData["PageIcon"] = "bi-file-text";
|
ViewData["PageIcon"] = "bi-file-text";
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.Error))
|
@if (!string.IsNullOrEmpty(Model.Error))
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle"></i> @Model.Error
|
<i class="bi bi-exclamation-triangle"></i> @Model.Error
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle"></i> No log files found in <code>@Model.LogsPath</code>
|
<i class="bi bi-info-circle"></i> No log files found in <code>@Model.LogsPath</code>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Web.Controllers
|
@using PowderCoating.Web.Controllers
|
||||||
@model AdminEmailComposeModel
|
@model AdminEmailComposeModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Admin Email";
|
ViewData["Title"] = "Admin Email";
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<div class="row g-4 align-items-start mb-4">
|
<div class="row g-4 align-items-start mb-4">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info alert-permanent mb-0">
|
||||||
The email sends one company at a time to that company's <strong>Primary Contact Email</strong>.
|
The email sends one company at a time to that company's <strong>Primary Contact Email</strong>.
|
||||||
Rich text is supported, and the preview step will render one merged sample before anything sends.
|
Rich text is supported, and the preview step will render one merged sample before anything sends.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Web.Controllers
|
@using PowderCoating.Web.Controllers
|
||||||
@model AdminEmailPreviewModel
|
@model AdminEmailPreviewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Preview Admin Email";
|
ViewData["Title"] = "Preview Admin Email";
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<div class="card shadow-sm border-0 mb-4">
|
<div class="card shadow-sm border-0 mb-4">
|
||||||
<div class="card-header bg-transparent fw-semibold py-3">Delivery Summary</div>
|
<div class="card-header bg-transparent fw-semibold py-3">Delivery Summary</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info alert-permanent mb-0">
|
||||||
The system will process each selected company one at a time.
|
The system will process each selected company one at a time.
|
||||||
The sample shown on the left uses the first available recipient after token replacement.
|
The sample shown on the left uses the first available recipient after token replacement.
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="small text-muted">@(string.IsNullOrWhiteSpace(row.RecipientEmail) ? "No primary contact email configured" : row.RecipientEmail)</div>
|
<div class="small text-muted">@(string.IsNullOrWhiteSpace(row.RecipientEmail) ? "No primary contact email configured" : row.RecipientEmail)</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>@(string.IsNullOrWhiteSpace(row.CompanyAdminName) ? "—" : row.CompanyAdminName)</div>
|
<div>@(string.IsNullOrWhiteSpace(row.CompanyAdminName) ? "—" : row.CompanyAdminName)</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(row.CompanyAdminEmail))
|
@if (!string.IsNullOrWhiteSpace(row.CompanyAdminEmail))
|
||||||
{
|
{
|
||||||
<div class="small text-muted">@row.CompanyAdminEmail</div>
|
<div class="small text-muted">@row.CompanyAdminEmail</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@model PowderCoating.Application.DTOs.Accounting.CreateExpenseDto
|
@model PowderCoating.Application.DTOs.Accounting.CreateExpenseDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "New Expense";
|
ViewData["Title"] = "New Expense";
|
||||||
ViewData["PageIcon"] = "bi-receipt";
|
ViewData["PageIcon"] = "bi-receipt";
|
||||||
ViewData["PageHelpTitle"] = "New Expense";
|
ViewData["PageHelpTitle"] = "New Expense";
|
||||||
ViewData["PageHelpContent"] = "Use this for purchases paid immediately — credit card swipes, cash payments, debit transactions. For vendor invoices paid later, use Bills instead. Select the expense account (what was bought) and the payment account (where the money came from).";
|
ViewData["PageHelpContent"] = "Use this for purchases paid immediately — credit card swipes, cash payments, debit transactions. For vendor invoices paid later, use Bills instead. Select the expense account (what was bought) and the payment account (where the money came from).";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-start mb-4">
|
<div class="d-flex justify-content-start mb-4">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-action="Create" method="post" enctype="multipart/form-data">
|
<form asp-action="Create" method="post" enctype="multipart/form-data">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@@ -40,13 +40,13 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Expense Account"
|
data-bs-title="Expense Account"
|
||||||
data-bs-content="The expense category this purchase belongs to — e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
|
data-bs-content="The expense category this purchase belongs to — e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 align-items-center">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
<select asp-for="ExpenseAccountId" asp-items="ViewBag.ExpenseAccounts" class="form-select" id="expenseAccountSelect">
|
<select asp-for="ExpenseAccountId" asp-items="ViewBag.ExpenseAccounts" class="form-select" id="expenseAccountSelect">
|
||||||
<option value="">— Select Account —</option>
|
<option value="">— Select Account —</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary text-nowrap" id="expAiSuggestBtn" title="AI-suggest expense account">
|
<button type="button" class="btn btn-sm btn-outline-primary text-nowrap" id="expAiSuggestBtn" title="AI-suggest expense account">
|
||||||
<i class="bi bi-stars me-1"></i>AI Suggest
|
<i class="bi bi-stars me-1"></i>AI Suggest
|
||||||
@@ -66,12 +66,12 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Paid From"
|
data-bs-title="Paid From"
|
||||||
data-bs-content="The bank or cash account the money came out of — e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
|
data-bs-content="The bank or cash account the money came out of — e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="PaymentAccountId" asp-items="ViewBag.PaymentAccounts" class="form-select">
|
<select asp-for="PaymentAccountId" asp-items="ViewBag.PaymentAccounts" class="form-select">
|
||||||
<option value="">— Select Account —</option>
|
<option value="">— Select Account —</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="PaymentAccountId" class="text-danger small"></span>
|
<span asp-validation-for="PaymentAccountId" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,8 +84,8 @@
|
|||||||
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-muted small">(optional)</span></label>
|
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-muted small">(optional)</span></label>
|
||||||
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
|
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
|
||||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||||
<option value="">— None —</option>
|
<option value="">— None —</option>
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="JobId" asp-items="ViewBag.Jobs" class="form-select">
|
<select asp-for="JobId" asp-items="ViewBag.Jobs" class="form-select">
|
||||||
<option value="">— None —</option>
|
<option value="">— None —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── AI Suggest Account ────────────────────────────────────────────────
|
// ── AI Suggest Account ────────────────────────────────────────────────
|
||||||
let _expAiSuggestedAccountId = null;
|
let _expAiSuggestedAccountId = null;
|
||||||
|
|
||||||
document.getElementById('expAiSuggestBtn').addEventListener('click', async function () {
|
document.getElementById('expAiSuggestBtn').addEventListener('click', async function () {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Accounting.EditExpenseDto
|
@model PowderCoating.Application.DTOs.Accounting.EditExpenseDto
|
||||||
@* Note: ReceiptFilePath is carried via hidden field to detect existing receipt *@
|
@* Note: ReceiptFilePath is carried via hidden field to detect existing receipt *@
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input asp-for="Id" type="hidden" />
|
<input asp-for="Id" type="hidden" />
|
||||||
<input asp-for="ReceiptFilePath" type="hidden" />
|
<input asp-for="ReceiptFilePath" type="hidden" />
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@@ -40,12 +40,12 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Expense Account"
|
data-bs-title="Expense Account"
|
||||||
data-bs-content="The expense category this purchase belongs to — e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
|
data-bs-content="The expense category this purchase belongs to — e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="ExpenseAccountId" asp-items="ViewBag.ExpenseAccounts" class="form-select">
|
<select asp-for="ExpenseAccountId" asp-items="ViewBag.ExpenseAccounts" class="form-select">
|
||||||
<option value="">— Select Account —</option>
|
<option value="">— Select Account —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@@ -54,12 +54,12 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Paid From"
|
data-bs-title="Paid From"
|
||||||
data-bs-content="The bank or cash account the money came out of — e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
|
data-bs-content="The bank or cash account the money came out of — e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="PaymentAccountId" asp-items="ViewBag.PaymentAccounts" class="form-select">
|
<select asp-for="PaymentAccountId" asp-items="ViewBag.PaymentAccounts" class="form-select">
|
||||||
<option value="">— Select Account —</option>
|
<option value="">— Select Account —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
<label asp-for="VendorId" class="form-label fw-medium">Vendor</label>
|
<label asp-for="VendorId" class="form-label fw-medium">Vendor</label>
|
||||||
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
|
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
|
||||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||||
<option value="">— None —</option>
|
<option value="">— None —</option>
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<select asp-for="JobId" asp-items="ViewBag.Jobs" class="form-select">
|
<select asp-for="JobId" asp-items="ViewBag.Jobs" class="form-select">
|
||||||
<option value="">— None —</option>
|
<option value="">— None —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model List<PowderCoating.Application.DTOs.Accounting.ExpenseListDto>
|
@model List<PowderCoating.Application.DTOs.Accounting.ExpenseListDto>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Expenses";
|
ViewData["Title"] = "Expenses";
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
@if ((decimal)ViewBag.TotalAmount > 0)
|
@if ((decimal)ViewBag.TotalAmount > 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-info d-flex align-items-center gap-2 mb-4">
|
<div class="alert alert-info alert-permanent d-flex align-items-center gap-2 mb-4">
|
||||||
<i class="bi bi-info-circle fs-5"></i>
|
<i class="bi bi-info-circle fs-5"></i>
|
||||||
<span>Total shown: <strong>@(((decimal)ViewBag.TotalAmount).ToString("C"))</strong></span>
|
<span>Total shown: <strong>@(((decimal)ViewBag.TotalAmount).ToString("C"))</strong></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<form method="get" class="row g-2 align-items-end">
|
<form method="get" class="row g-2 align-items-end">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<input type="search" name="search" value="@ViewBag.Search" class="form-control"
|
<input type="search" name="search" value="@ViewBag.Search" class="form-control"
|
||||||
placeholder="Search memo or vendor…" />
|
placeholder="Search memo or vendor…" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<select name="accountId" class="form-select">
|
<select name="accountId" class="form-select">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-@statusClass d-flex align-items-center mb-4">
|
<div class="alert alert-@statusClass alert-permanent d-flex align-items-center mb-4">
|
||||||
<i class="bi bi-gift me-2" style="font-size:1.4rem;"></i>
|
<i class="bi bi-gift me-2" style="font-size:1.4rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>@statusLabel</strong>
|
<strong>@statusLabel</strong>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Error";
|
ViewData["Title"] = "Error";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
@if (Context.TraceIdentifier != null)
|
@if (Context.TraceIdentifier != null)
|
||||||
{
|
{
|
||||||
<div class="alert alert-secondary mt-3">
|
<div class="alert alert-secondary alert-permanent mt-3">
|
||||||
<strong>Request ID:</strong> <code>@Context.TraceIdentifier</code>
|
<strong>Request ID:</strong> <code>@Context.TraceIdentifier</code>
|
||||||
<br />
|
<br />
|
||||||
<small class="text-muted">Please provide this ID when contacting support.</small>
|
<small class="text-muted">Please provide this ID when contacting support.</small>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
|
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
|
||||||
{
|
{
|
||||||
var logFileName = $"logs/errors-{DateTime.Now:yyyyMMdd}.txt";
|
var logFileName = $"logs/errors-{DateTime.Now:yyyyMMdd}.txt";
|
||||||
<div class="alert alert-warning mt-3">
|
<div class="alert alert-warning alert-permanent mt-3">
|
||||||
<strong><i class="bi bi-code-slash"></i> Development Mode:</strong>
|
<strong><i class="bi bi-code-slash"></i> Development Mode:</strong>
|
||||||
Check the error logs at <code>@logFileName</code> for details.
|
Check the error logs at <code>@logFileName</code> for details.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Welcome";
|
ViewData["Title"] = "Welcome";
|
||||||
Layout = null; // Use custom layout for login page
|
Layout = null; // Use custom layout for login page
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
|
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success alert-permanent">
|
||||||
<i class="bi bi-check-circle me-2"></i>
|
<i class="bi bi-check-circle me-2"></i>
|
||||||
You're already logged in as <strong>@User.Identity.Name</strong>
|
You're already logged in as <strong>@User.Identity.Name</strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
<a href="/Identity/Account/Register">
|
<a href="/Identity/Account/Register">
|
||||||
<i class="bi bi-person-plus me-1"></i>Create an account
|
<i class="bi bi-person-plus me-1"></i>Create an account
|
||||||
</a>
|
</a>
|
||||||
<span class="mx-2">•</span>
|
<span class="mx-2">•</span>
|
||||||
<a href="/Identity/Account/ForgotPassword">
|
<a href="/Identity/Account/ForgotPassword">
|
||||||
<i class="bi bi-question-circle me-1"></i>Forgot password?
|
<i class="bi bi-question-circle me-1"></i>Forgot password?
|
||||||
</a>
|
</a>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
|
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<p style="color: rgba(255, 255, 255, 0.8); font-size: 0.875rem;">
|
<p style="color: rgba(255, 255, 255, 0.8); font-size: 0.875rem;">
|
||||||
© 2024 Powder Coating Logix. All rights reserved.
|
© 2024 Powder Coating Logix. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
|
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Inventory Item";
|
ViewData["Title"] = "Delete Inventory Item";
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warning Banner -->
|
<!-- Warning Banner -->
|
||||||
<div class="alert alert-danger d-flex align-items-start mb-4">
|
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="alert-heading mb-2">Are you sure you want to delete this inventory item?</h5>
|
<h5 class="alert-heading mb-2">Are you sure you want to delete this inventory item?</h5>
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
|
|
||||||
@if (Model.QuantityOnHand > 0)
|
@if (Model.QuantityOnHand > 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center">
|
||||||
<i class="bi bi-exclamation-circle me-2"></i>
|
<i class="bi bi-exclamation-circle me-2"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>Warning:</strong> This item still has @Model.QuantityOnHand.ToString("N2") @Model.UnitOfMeasure in stock (value: @((Model.QuantityOnHand * Model.UnitCost).ToString("C"))). Consider transferring or adjusting inventory before deletion.
|
<strong>Warning:</strong> This item still has @Model.QuantityOnHand.ToString("N2") @Model.UnitOfMeasure in stock (value: @((Model.QuantityOnHand * Model.UnitCost).ToString("C"))). Consider transferring or adjusting inventory before deletion.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.DTOs.Invoice
|
@using PowderCoating.Application.DTOs.Invoice
|
||||||
@using PowderCoating.Core.Enums
|
@using PowderCoating.Core.Enums
|
||||||
@using PowderCoating.Web.Controllers
|
@using PowderCoating.Web.Controllers
|
||||||
@model InvoiceDto
|
@model InvoiceDto
|
||||||
@@ -66,17 +66,17 @@
|
|||||||
|
|
||||||
@if (!hasEmail)
|
@if (!hasEmail)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4">
|
||||||
<i class="bi bi-envelope-slash fs-5"></i>
|
<i class="bi bi-envelope-slash fs-5"></i>
|
||||||
<span>
|
<span>
|
||||||
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending.
|
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending.
|
||||||
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
|
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (emailOptedOut)
|
else if (emailOptedOut)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4">
|
||||||
<i class="bi bi-envelope-x fs-5"></i>
|
<i class="bi bi-envelope-x fs-5"></i>
|
||||||
<span>
|
<span>
|
||||||
<strong>@Model.CustomerName</strong> has email notifications turned off.
|
<strong>@Model.CustomerName</strong> has email notifications turned off.
|
||||||
@@ -168,12 +168,12 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="text-muted small mb-1">Due Date</label>
|
<label class="text-muted small mb-1">Due Date</label>
|
||||||
<p class="mb-0 @(Model.Status == InvoiceStatus.Overdue ? "text-danger fw-bold" : "")">
|
<p class="mb-0 @(Model.Status == InvoiceStatus.Overdue ? "text-danger fw-bold" : "")">
|
||||||
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—")
|
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—")
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="text-muted small mb-1">Sent Date</label>
|
<label class="text-muted small mb-1">Sent Date</label>
|
||||||
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")</p>
|
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")</p>
|
||||||
</div>
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
|
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
|
||||||
{
|
{
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted">
|
<td class="text-muted">
|
||||||
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—")
|
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—")
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td>
|
<td class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>@p.PaymentDate.ToString("MM/dd/yyyy")</td>
|
<td>@p.PaymentDate.ToString("MM/dd/yyyy")</td>
|
||||||
<td>@p.PaymentMethodDisplay</td>
|
<td>@p.PaymentMethodDisplay</td>
|
||||||
<td>@(p.Reference ?? "—")</td>
|
<td>@(p.Reference ?? "—")</td>
|
||||||
<td>
|
<td>
|
||||||
@if (!string.IsNullOrEmpty(p.DepositAccountName))
|
@if (!string.IsNullOrEmpty(p.DepositAccountName))
|
||||||
{
|
{
|
||||||
@@ -393,10 +393,10 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>@(p.RecordedByName ?? "—")</td>
|
<td>@(p.RecordedByName ?? "—")</td>
|
||||||
<td class="text-end fw-semibold text-success">@p.Amount.ToString("C")</td>
|
<td class="text-end fw-semibold text-success">@p.Amount.ToString("C")</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@if (!isVoided)
|
@if (!isVoided)
|
||||||
@@ -452,7 +452,7 @@
|
|||||||
<td>@r.RefundDate.ToString("MM/dd/yyyy")</td>
|
<td>@r.RefundDate.ToString("MM/dd/yyyy")</td>
|
||||||
<td>@r.RefundMethodDisplay</td>
|
<td>@r.RefundMethodDisplay</td>
|
||||||
<td>@r.Reason</td>
|
<td>@r.Reason</td>
|
||||||
<td>@(r.Reference ?? "—")</td>
|
<td>@(r.Reference ?? "—")</td>
|
||||||
<td><span class="badge bg-@refundStatusColor">@r.Status</span></td>
|
<td><span class="badge bg-@refundStatusColor">@r.Status</span></td>
|
||||||
<td class="text-end fw-semibold text-danger">(@r.Amount.ToString("C"))</td>
|
<td class="text-end fw-semibold text-danger">(@r.Amount.ToString("C"))</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
@@ -564,7 +564,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
||||||
data-bs-title="Invoice Actions"
|
data-bs-title="Invoice Actions"
|
||||||
data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
|
data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -761,7 +761,7 @@
|
|||||||
{
|
{
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<span class="badge bg-success-subtle text-success mb-2">
|
<span class="badge bg-success-subtle text-success mb-2">
|
||||||
<i class="bi bi-check-circle me-1"></i>Active — expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
|
<i class="bi bi-check-circle me-1"></i>Active — expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
|
||||||
</span>
|
</span>
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="text" id="paymentLinkInput" class="form-control font-monospace"
|
<input type="text" id="paymentLinkInput" class="form-control font-monospace"
|
||||||
@@ -784,7 +784,7 @@
|
|||||||
}
|
}
|
||||||
else if (linkExpired)
|
else if (linkExpired)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning py-2 small mb-2">
|
<div class="alert alert-warning alert-permanent py-2 small mb-2">
|
||||||
<i class="bi bi-clock me-1"></i>Payment link expired.
|
<i class="bi bi-clock me-1"></i>Payment link expired.
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary btn-sm w-100"
|
<button type="button" class="btn btn-primary btn-sm w-100"
|
||||||
@@ -899,7 +899,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
||||||
data-bs-title="Payment Reference"
|
data-bs-title="Payment Reference"
|
||||||
data-bs-content="Optional identifier for reconciliation — e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
|
data-bs-content="Optional identifier for reconciliation — e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1112,13 +1112,13 @@
|
|||||||
<form asp-action="IssueRefund" asp-route-invoiceId="@Model.Id" method="post">
|
<form asp-action="IssueRefund" asp-route-invoiceId="@Model.Id" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="refundAlertCash" class="alert alert-info small mb-3">
|
<div id="refundAlertCash" class="alert alert-info alert-permanent small mb-3">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
This records the refund intent. You still need to issue the actual refund (cash, check, etc.) manually.
|
This records the refund intent. You still need to issue the actual refund (cash, check, etc.) manually.
|
||||||
</div>
|
</div>
|
||||||
<div id="refundAlertCredit" class="alert alert-success small mb-3 d-none">
|
<div id="refundAlertCredit" class="alert alert-success small mb-3 d-none">
|
||||||
<i class="bi bi-piggy-bank me-1"></i>
|
<i class="bi bi-piggy-bank me-1"></i>
|
||||||
The refund amount will be added to the customer's store credit balance immediately — no manual action needed.
|
The refund amount will be added to the customer's store credit balance immediately — no manual action needed.
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
|
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
|
||||||
@@ -1188,7 +1188,7 @@
|
|||||||
<form asp-action="IssueCreditMemo" asp-route-invoiceId="@Model.Id" method="post">
|
<form asp-action="IssueCreditMemo" asp-route-invoiceId="@Model.Id" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-info small mb-3">
|
<div class="alert alert-info alert-permanent small mb-3">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
A credit memo adds store credit to the customer's account that can be applied against future invoices.
|
A credit memo adds store credit to the customer's account that can be applied against future invoices.
|
||||||
</div>
|
</div>
|
||||||
@@ -1240,7 +1240,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label>
|
<label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label>
|
||||||
<select name="CreditMemoId" class="form-select" required>
|
<select name="CreditMemoId" class="form-select" required>
|
||||||
<option value="">— Select —</option>
|
<option value="">— Select —</option>
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos)
|
||||||
{
|
{
|
||||||
<option value="@item.Value">@item.Text</option>
|
<option value="@item.Value">@item.Text</option>
|
||||||
@@ -1254,7 +1254,7 @@
|
|||||||
<input type="number" name="Amount" class="form-control" step="0.01" min="0.01"
|
<input type="number" name="Amount" class="form-control" step="0.01" min="0.01"
|
||||||
max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required />
|
max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") — the system will cap at the memo's remaining balance.</div>
|
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") — the system will cap at the memo's remaining balance.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -1328,7 +1328,7 @@
|
|||||||
<form asp-action="WriteOff" asp-route-id="@Model.Id" method="post">
|
<form asp-action="WriteOff" asp-route-id="@Model.Id" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning py-2 mb-3">
|
<div class="alert alert-warning alert-permanent py-2 mb-3">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
This will write off the remaining balance of <strong>@Model.BalanceDue.ToString("C")</strong>
|
This will write off the remaining balance of <strong>@Model.BalanceDue.ToString("C")</strong>
|
||||||
as bad debt. A GL journal entry will be posted. This action cannot be undone.
|
as bad debt. A GL journal entry will be posted. This action cannot be undone.
|
||||||
@@ -1336,7 +1336,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Bad Debt Expense Account</label>
|
<label class="form-label">Bad Debt Expense Account</label>
|
||||||
<select name="expenseAccountId" class="form-select">
|
<select name="expenseAccountId" class="form-select">
|
||||||
<option value="">— Use default bad debt account —</option>
|
<option value="">— Use default bad debt account —</option>
|
||||||
@if (ViewBag.ExpenseAccounts != null)
|
@if (ViewBag.ExpenseAccounts != null)
|
||||||
{
|
{
|
||||||
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
|
||||||
@@ -1397,8 +1397,8 @@
|
|||||||
const max = Math.min(data.remainingBalance, @Model.BalanceDue.ToString("F2", System.Globalization.CultureInfo.InvariantCulture));
|
const max = Math.min(data.remainingBalance, @Model.BalanceDue.ToString("F2", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
document.getElementById('gcAmountInput').value = max.toFixed(2);
|
document.getElementById('gcAmountInput').value = max.toFixed(2);
|
||||||
document.getElementById('gcAmountInput').max = max;
|
document.getElementById('gcAmountInput').max = max;
|
||||||
const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : '';
|
const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : '';
|
||||||
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> — $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
|
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> — $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
|
||||||
}
|
}
|
||||||
} catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; }
|
} catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; }
|
||||||
document.getElementById('gcLookupSpinner').style.display = 'none';
|
document.getElementById('gcLookupSpinner').style.display = 'none';
|
||||||
@@ -1511,7 +1511,7 @@
|
|||||||
<td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td>
|
<td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td>
|
||||||
<td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td>
|
<td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td>
|
||||||
<td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td>
|
<td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td>
|
||||||
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">—</span>'}</td>
|
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">—</span>'}</td>
|
||||||
<td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span>${expandBtn}</td>
|
<td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span>${expandBtn}</td>
|
||||||
</tr>${errorRow}`;
|
</tr>${errorRow}`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -1555,7 +1555,7 @@
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if (msg) msg.innerHTML = `
|
if (msg) msg.innerHTML = `
|
||||||
<div class="alert alert-success py-2 small">
|
<div class="alert alert-success alert-permanent py-2 small">
|
||||||
<i class="bi bi-check-circle me-1"></i>New link generated!
|
<i class="bi bi-check-circle me-1"></i>New link generated!
|
||||||
<a href="${data.paymentUrl}" target="_blank" class="alert-link ms-1">Open</a>
|
<a href="${data.paymentUrl}" target="_blank" class="alert-link ms-1">Open</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Invoice.UpdateInvoiceDto
|
@model PowderCoating.Application.DTOs.Invoice.UpdateInvoiceDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Invoice";
|
ViewData["Title"] = "Edit Invoice";
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Invoice Details"
|
data-bs-title="Invoice Details"
|
||||||
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice — changing it here only affects this invoice. Draft, Sent, and Overdue invoices can be edited; Paid and Partially Paid invoices are locked.">
|
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice — changing it here only affects this invoice. Draft, Sent, and Overdue invoices can be edited; Paid and Partially Paid invoices are locked.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Line Items"
|
data-bs-title="Line Items"
|
||||||
data-bs-content="Each row is a billable line on the invoice. Qty × Unit Price = Total per line; you can also override Total directly. Color is optional and appears under the description when printed. Add manual lines for charges not in the original job (e.g., rush fee, pickup charge).">
|
data-bs-content="Each row is a billable line on the invoice. Qty × Unit Price = Total per line; you can also override Total directly. Color is optional and appears under the description when printed. Add manual lines for charges not in the original job (e.g., rush fee, pickup charge).">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Notes"
|
data-bs-title="Notes"
|
||||||
data-bs-content="Customer Notes appear on the printed and emailed invoice — use these for payment instructions, thank-you messages, or job-specific reminders. Internal Notes are only visible to staff in the app and are never sent to the customer.">
|
data-bs-content="Customer Notes appear on the printed and emailed invoice — use these for payment instructions, thank-you messages, or job-specific reminders. Internal Notes are only visible to staff in the app and are never sent to the customer.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Totals"
|
data-bs-title="Totals"
|
||||||
data-bs-content="Subtotal = sum of all line item totals. Discount is a flat dollar amount deducted before tax. Tax % is applied to (Subtotal − Discount). Both default from the company settings but can be overridden for this invoice.">
|
data-bs-content="Subtotal = sum of all line item totals. Discount is a flat dollar amount deducted before tax. Tax % is applied to (Subtotal − Discount). Both default from the company settings but can be overridden for this invoice.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer border-0 pt-0">
|
<div class="card-footer border-0 pt-0">
|
||||||
<div class="alert alert-info mb-0 small py-2">
|
<div class="alert alert-info alert-permanent mb-0 small py-2">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
<strong>Draft, Sent,</strong> and <strong>Overdue</strong> invoices can be edited.
|
<strong>Draft, Sent,</strong> and <strong>Overdue</strong> invoices can be edited.
|
||||||
Paid invoices are locked.
|
Paid invoices are locked.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Core.Entities.JobTemplate
|
@model PowderCoating.Core.Entities.JobTemplate
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Edit Template: {Model.Name}";
|
ViewData["Title"] = $"Edit Template: {Model.Name}";
|
||||||
ViewData["PageIcon"] = "bi-pencil-square";
|
ViewData["PageIcon"] = "bi-pencil-square";
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Default Customer</label>
|
<label class="form-label fw-semibold">Default Customer</label>
|
||||||
<select name="customerId" class="form-select">
|
<select name="customerId" class="form-select">
|
||||||
<option value="">— Any customer —</option>
|
<option value="">— Any customer —</option>
|
||||||
@foreach (SelectListItem item in (ViewBag.Customers as IEnumerable<SelectListItem> ?? Enumerable.Empty<SelectListItem>()))
|
@foreach (SelectListItem item in (ViewBag.Customers as IEnumerable<SelectListItem> ?? Enumerable.Empty<SelectListItem>()))
|
||||||
{
|
{
|
||||||
if (item.Selected)
|
if (item.Selected)
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info mt-3">
|
<div class="alert alert-info alert-permanent mt-3">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
To update the <strong>items and coatings</strong> on this template, create a new job with the desired configuration and save it as a template.
|
To update the <strong>items and coatings</strong> on this template, create a new job with the desired configuration and save it as a template.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Job.CreateJobDto
|
@model PowderCoating.Application.DTOs.Job.CreateJobDto
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<i class="bi bi-layout-text-window-reverse fs-5"></i>
|
<i class="bi bi-layout-text-window-reverse fs-5"></i>
|
||||||
<div>
|
<div>
|
||||||
Pre-filled from template <strong>@ViewBag.TemplateName</strong>.
|
Pre-filled from template <strong>@ViewBag.TemplateName</strong>.
|
||||||
Items and coatings have been loaded — review and adjust before saving.
|
Items and coatings have been loaded — review and adjust before saving.
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Details"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Priority"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Due Date"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Special Instructions"
|
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'.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Items"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
|
<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="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
|
||||||
<p class="mb-1 d-none" id="ovenBatchCostRow">
|
<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):
|
<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>
|
<strong id="ovenBatchCostDisplay">$0.00</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
|
<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>
|
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="cylinderInputs" style="display:none">
|
<div id="cylinderInputs" style="display:none">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Job.JobDto
|
@model PowderCoating.Application.DTOs.Job.JobDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Job";
|
ViewData["Title"] = "Delete Job";
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warning Banner -->
|
<!-- Warning Banner -->
|
||||||
<div class="alert alert-danger d-flex align-items-start mb-4">
|
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="alert-heading mb-2">Are you sure you want to delete this job?</h5>
|
<h5 class="alert-heading mb-2">Are you sure you want to delete this job?</h5>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
|
|
||||||
@if (Model.StatusIsWIP)
|
@if (Model.StatusIsWIP)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-center">
|
<div class="alert alert-warning alert-permanent d-flex align-items-center">
|
||||||
<i class="bi bi-exclamation-circle me-2"></i>
|
<i class="bi bi-exclamation-circle me-2"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>Warning:</strong> This job is currently in progress. Please ensure all work is properly documented before deletion.
|
<strong>Warning:</strong> This job is currently in progress. Please ensure all work is properly documented before deletion.
|
||||||
|
|||||||
@@ -2107,7 +2107,7 @@
|
|||||||
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
@@ -3224,7 +3224,7 @@
|
|||||||
placeholder="Brief description of when to use this template"></textarea>
|
placeholder="Brief description of when to use this template"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-light border mb-0">
|
<div class="alert alert-light alert-permanent border mb-0">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
The customer, special instructions, and all items with their coatings will be copied.
|
The customer, special instructions, and all items with their coatings will be copied.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Job.UpdateJobDto
|
@model PowderCoating.Application.DTOs.Job.UpdateJobDto
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Details"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Status"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Priority"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Due Date"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Job Items"
|
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.">
|
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>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
|
<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="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
|
||||||
<p class="mb-1 d-none" id="ovenBatchCostRow">
|
<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):
|
<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>
|
<strong id="ovenBatchCostDisplay">$0.00</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
|
<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>
|
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="cylinderInputs" style="display:none">
|
<div id="cylinderInputs" style="display:none">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
|
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Edit Items — {Model.JobNumber}";
|
ViewData["Title"] = $"Edit Items — {Model.JobNumber}";
|
||||||
ViewData["PageIcon"] = "bi-list-check";
|
ViewData["PageIcon"] = "bi-list-check";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
@if (!ViewData.ModelState.IsValid)
|
@if (!ViewData.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger mb-4" role="alert">
|
<div class="alert alert-danger alert-permanent mb-4" role="alert">
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
||||||
{
|
{
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
|
<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="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
|
||||||
<p class="mb-1 d-none" id="ovenBatchCostRow">
|
<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):
|
<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>
|
<strong id="ovenBatchCostDisplay">$0.00</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
|
<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>
|
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="cylinderInputs" style="display:none">
|
<div id="cylinderInputs" style="display:none">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Job.JobDto
|
@model PowderCoating.Application.DTOs.Job.JobDto
|
||||||
@{
|
@{
|
||||||
var emailDefault = ViewBag.EmailDefaultOnComplete == true;
|
var emailDefault = ViewBag.EmailDefaultOnComplete == true;
|
||||||
var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>();
|
var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>();
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
@if (preFilledLbs > 0)
|
@if (preFilledLbs > 0)
|
||||||
{
|
{
|
||||||
<small class="text-success d-block mt-1">
|
<small class="text-success d-block mt-1">
|
||||||
<i class="bi bi-check-circle me-1"></i>Already logged — inventory adjusted
|
<i class="bi bi-check-circle me-1"></i>Already logged — inventory adjusted
|
||||||
</small>
|
</small>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<small class="text-muted fst-italic">
|
<small class="text-muted fst-italic">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
@item.Description — No coat information available (legacy job item)
|
@item.Description — No coat information available (legacy job item)
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -120,9 +120,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info alert-permanent mb-0">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<small>Pre-filled values were already logged via scan — inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small>
|
<small>Pre-filled values were already logged via scan — inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
|
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
|
||||||
@using PowderCoating.Application.DTOs.Job
|
@using PowderCoating.Application.DTOs.Job
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
@using PowderCoating.Core.Enums
|
@using PowderCoating.Core.Enums
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Carried-Over (Overdue) Jobs ──────────────────────────────────────── *@
|
@* ── Carried-Over (Overdue) Jobs ──────────────────────────────────────── *@
|
||||||
@if (overdueJobs.Any())
|
@if (overdueJobs.Any())
|
||||||
{
|
{
|
||||||
<div class="card border-danger mb-4">
|
<div class="card border-danger mb-4">
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Carried Over — Not Yet Completed</strong>
|
<strong>Carried Over — Not Yet Completed</strong>
|
||||||
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
|
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Carried Over Jobs"
|
data-bs-title="Carried Over Jobs"
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
|
|
||||||
@if (!Model.Any())
|
@if (!Model.Any())
|
||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
No jobs scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
|
No jobs scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
|
||||||
</div>
|
</div>
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
|
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Reorder & Quick Edit"
|
data-bs-title="Reorder & Quick Edit"
|
||||||
data-bs-content="Drag the ⠿ grip on the left of any row to change processing order — saved automatically. Click a Priority badge to change priority. Click the Worker cell to reassign. Click Scheduled Date or Due Date to update dates inline without navigating away.">
|
data-bs-content="Drag the ⠿ grip on the left of any row to change processing order — saved automatically. Click a Priority badge to change priority. Click the Worker cell to reassign. Click Scheduled Date or Due Date to update dates inline without navigating away.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,7 +320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@* ── Scheduled Maintenance for the Day ──────────────────────────────── *@
|
@* ── Scheduled Maintenance for the Day ──────────────────────────────── *@
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-warning text-dark">
|
<div class="card-header bg-warning text-dark">
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
<tr onclick="window.location='@Url.Action("Details", "Maintenance", new { id = item.Id })'"
|
<tr onclick="window.location='@Url.Action("Details", "Maintenance", new { id = item.Id })'"
|
||||||
style="cursor: pointer;">
|
style="cursor: pointer;">
|
||||||
<td>
|
<td>
|
||||||
<strong>@(item.Equipment?.EquipmentName ?? "—")</strong>
|
<strong>@(item.Equipment?.EquipmentName ?? "—")</strong>
|
||||||
@if (!string.IsNullOrEmpty(item.Equipment?.Location))
|
@if (!string.IsNullOrEmpty(item.Equipment?.Location))
|
||||||
{
|
{
|
||||||
<br /><small class="text-muted"><i class="bi bi-geo-alt me-1"></i>@item.Equipment.Location</small>
|
<br /><small class="text-muted"><i class="bi bi-geo-alt me-1"></i>@item.Equipment.Location</small>
|
||||||
@@ -930,14 +930,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// SignalR — real-time status updates from shop display
|
// SignalR — real-time status updates from shop display
|
||||||
(function () {
|
(function () {
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl('/hubs/shop')
|
.withUrl('/hubs/shop')
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// A worker advanced a job status on the shop display — update badge in place
|
// A worker advanced a job status on the shop display — update badge in place
|
||||||
connection.on('JobStatusChanged', function (data) {
|
connection.on('JobStatusChanged', function (data) {
|
||||||
const badge = document.getElementById(`status-badge-${data.jobId}`);
|
const badge = document.getElementById(`status-badge-${data.jobId}`);
|
||||||
if (badge) {
|
if (badge) {
|
||||||
@@ -946,7 +946,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Order/priority/worker changed by another Daily Board session — reload
|
// Order/priority/worker changed by another Daily Board session — reload
|
||||||
connection.on('DailyBoardUpdated', function () {
|
connection.on('DailyBoardUpdated', function () {
|
||||||
// Only reload if this tab didn't trigger it (we can't easily tell, so just reload)
|
// Only reload if this tab didn't trigger it (we can't easily tell, so just reload)
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Maintenance.CreateMaintenanceDto
|
@model PowderCoating.Application.DTOs.Maintenance.CreateMaintenanceDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Schedule Maintenance";
|
ViewData["Title"] = "Schedule Maintenance";
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Basic Information"
|
data-bs-title="Basic Information"
|
||||||
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
|
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
|
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mt-3 small mb-0">
|
<div class="alert alert-info alert-permanent mt-3 small mb-0">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
If no end date is set, occurrences will be generated for:
|
If no end date is set, occurrences will be generated for:
|
||||||
Daily = 90 days • Weekly / Bi-Weekly = 1 year • Monthly = 2 years • Quarterly / Annually = 3 years • Bi-Annually = 18 months.
|
Daily = 90 days • Weekly / Bi-Weekly = 1 year • Monthly = 2 years • Quarterly / Annually = 3 years • Bi-Annually = 18 months.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Maintenance.MaintenanceRecordDto
|
@model PowderCoating.Application.DTOs.Maintenance.MaintenanceRecordDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Maintenance Record";
|
ViewData["Title"] = "Delete Maintenance Record";
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger alert-permanent" role="alert">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong>Warning:</strong> This action cannot be undone.
|
<strong>Warning:</strong> This action cannot be undone.
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-header bg-danger bg-opacity-10 border-0 py-3">
|
<div class="card-header bg-danger bg-opacity-10 border-0 py-3">
|
||||||
<h5 class="mb-0 text-danger">
|
<h5 class="mb-0 text-danger">
|
||||||
<i class="bi bi-wrench me-2"></i>@Model.MaintenanceType — @Model.EquipmentName
|
<i class="bi bi-wrench me-2"></i>@Model.MaintenanceType — @Model.EquipmentName
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Maintenance.UpdateMaintenanceDto
|
@model PowderCoating.Application.DTOs.Maintenance.UpdateMaintenanceDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Maintenance";
|
ViewData["Title"] = "Edit Maintenance";
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Basic Information"
|
data-bs-title="Basic Information"
|
||||||
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
|
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (ViewBag.IsRecurringSeries as bool? == true)
|
@if (ViewBag.IsRecurringSeries as bool? == true)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning mb-3 small">
|
<div class="alert alert-warning alert-permanent mb-3 small">
|
||||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
<strong>This is part of a recurring series.</strong>
|
<strong>This is part of a recurring series.</strong>
|
||||||
Changing recurrence settings will delete all future scheduled/overdue occurrences and regenerate them from this record's date.
|
Changing recurrence settings will delete all future scheduled/overdue occurrences and regenerate them from this record's date.
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
|
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mt-3 small mb-0">
|
<div class="alert alert-info alert-permanent mt-3 small mb-0">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
If no end date is set, occurrences will be generated for:
|
If no end date is set, occurrences will be generated for:
|
||||||
Daily = 90 days • Weekly / Bi-Weekly = 1 year • Monthly = 2 years • Quarterly / Annually = 3 years • Bi-Annually = 18 months.
|
Daily = 90 days • Weekly / Bi-Weekly = 1 year • Monthly = 2 years • Quarterly / Annually = 3 years • Bi-Annually = 18 months.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
@model ManufacturerLookupPattern
|
@model ManufacturerLookupPattern
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Add Manufacturer Pattern";
|
ViewData["Title"] = "Add Manufacturer Pattern";
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<form asp-action="Create" method="post">
|
<form asp-action="Create" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3 small"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="ManufacturerName" class="form-label fw-medium">Manufacturer Name <span class="text-danger">*</span></label>
|
<label asp-for="ManufacturerName" class="form-label fw-medium">Manufacturer Name <span class="text-danger">*</span></label>
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
|
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Supported placeholders:
|
Supported placeholders:
|
||||||
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
|
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
|
||||||
<code>{slug}</code> — color name transformed by Slug Transform below,
|
<code>{slug}</code> — color name transformed by Slug Transform below,
|
||||||
<code>{colorCode}</code> — color code as-is.
|
<code>{colorCode}</code> — color code as-is.
|
||||||
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
|
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
|
||||||
</div>
|
</div>
|
||||||
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
|
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
|
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
|
||||||
<select asp-for="SlugTransform" class="form-select">
|
<select asp-for="SlugTransform" class="form-select">
|
||||||
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option>
|
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option>
|
||||||
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option>
|
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option>
|
||||||
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option>
|
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option>
|
||||||
<option value="AsIs">AsIs — color name unchanged</option>
|
<option value="AsIs">AsIs — color name unchanged</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
|
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
|
||||||
<span asp-validation-for="SlugTransform" class="text-danger small"></span>
|
<span asp-validation-for="SlugTransform" class="text-danger small"></span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
@model ManufacturerLookupPattern
|
@model ManufacturerLookupPattern
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Pattern";
|
ViewData["Title"] = "Edit Pattern";
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input asp-for="Id" type="hidden" />
|
<input asp-for="Id" type="hidden" />
|
||||||
|
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3 small"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="ManufacturerName" class="form-label fw-medium">Manufacturer Name <span class="text-danger">*</span></label>
|
<label asp-for="ManufacturerName" class="form-label fw-medium">Manufacturer Name <span class="text-danger">*</span></label>
|
||||||
@@ -40,9 +40,9 @@
|
|||||||
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
|
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Supported placeholders:
|
Supported placeholders:
|
||||||
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
|
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
|
||||||
<code>{slug}</code> — color name transformed by Slug Transform below,
|
<code>{slug}</code> — color name transformed by Slug Transform below,
|
||||||
<code>{colorCode}</code> — color code as-is.
|
<code>{colorCode}</code> — color code as-is.
|
||||||
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
|
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
|
||||||
</div>
|
</div>
|
||||||
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
|
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
|
||||||
@@ -51,10 +51,10 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
|
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
|
||||||
<select asp-for="SlugTransform" class="form-select">
|
<select asp-for="SlugTransform" class="form-select">
|
||||||
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option>
|
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option>
|
||||||
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option>
|
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option>
|
||||||
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option>
|
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option>
|
||||||
<option value="AsIs">AsIs — color name unchanged</option>
|
<option value="AsIs">AsIs — color name unchanged</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
|
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
|
||||||
<span asp-validation-for="SlugTransform" class="text-danger small"></span>
|
<span asp-validation-for="SlugTransform" class="text-danger small"></span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Scheduling.OvenSchedulerViewModel
|
@model PowderCoating.Application.DTOs.Scheduling.OvenSchedulerViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Oven Scheduler";
|
ViewData["Title"] = "Oven Scheduler";
|
||||||
var dateStr = Model.ScheduledDate.ToString("yyyy-MM-dd");
|
var dateStr = Model.ScheduledDate.ToString("yyyy-MM-dd");
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
@section Styles {
|
@section Styles {
|
||||||
<style>
|
<style>
|
||||||
/* ─── Layout ─────────────────────────────────────── */
|
/* ─── Layout ─────────────────────────────────────── */
|
||||||
.scheduler-layout {
|
.scheduler-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Batch cards ────────────────────────────────── */
|
/* ─── Batch cards ────────────────────────────────── */
|
||||||
.batch-card {
|
.batch-card {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -52,14 +52,14 @@
|
|||||||
.batch-card.status-completed { border-color: #198754; opacity: .85; }
|
.batch-card.status-completed { border-color: #198754; opacity: .85; }
|
||||||
.batch-card.drag-over { border-color: #0d6efd !important; box-shadow: 0 0 12px rgba(13,110,253,.4) !important; }
|
.batch-card.drag-over { border-color: #0d6efd !important; box-shadow: 0 0 12px rgba(13,110,253,.4) !important; }
|
||||||
|
|
||||||
/* ─── Capacity bar ───────────────────────────────── */
|
/* ─── Capacity bar ───────────────────────────────── */
|
||||||
.capacity-bar-wrap { height: 6px; border-radius: 3px; background: #e9ecef; overflow: hidden; }
|
.capacity-bar-wrap { height: 6px; border-radius: 3px; background: #e9ecef; overflow: hidden; }
|
||||||
.capacity-bar-fill { height: 100%; border-radius: 3px; transition: width .3s; }
|
.capacity-bar-fill { height: 100%; border-radius: 3px; transition: width .3s; }
|
||||||
.cap-ok { background: #198754; }
|
.cap-ok { background: #198754; }
|
||||||
.cap-warn { background: #ffc107; }
|
.cap-warn { background: #ffc107; }
|
||||||
.cap-over { background: #dc3545; }
|
.cap-over { background: #dc3545; }
|
||||||
|
|
||||||
/* ─── Queue items ────────────────────────────────── */
|
/* ─── Queue items ────────────────────────────────── */
|
||||||
.queue-job-card {
|
.queue-job-card {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
.batch-item-row:hover { background: var(--bs-secondary-bg); }
|
.batch-item-row:hover { background: var(--bs-secondary-bg); }
|
||||||
.batch-item-row.dragging { opacity: .5; }
|
.batch-item-row.dragging { opacity: .5; }
|
||||||
|
|
||||||
/* ─── AI panel ───────────────────────────────────── */
|
/* ─── AI panel ───────────────────────────────────── */
|
||||||
.ai-suggestion-panel {
|
.ai-suggestion-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0; right: 0;
|
top: 0; right: 0;
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
}
|
}
|
||||||
.ai-panel-backdrop.open { display: block; }
|
.ai-panel-backdrop.open { display: block; }
|
||||||
|
|
||||||
/* ─── Drop zone hint ─────────────────────────────── */
|
/* ─── Drop zone hint ─────────────────────────────── */
|
||||||
.drop-zone-empty {
|
.drop-zone-empty {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
border: 2px dashed var(--bs-border-color);
|
border: 2px dashed var(--bs-border-color);
|
||||||
@@ -275,7 +275,7 @@
|
|||||||
<p class="small text-muted mb-2">
|
<p class="small text-muted mb-2">
|
||||||
The Oven Scheduler helps you plan which jobs go into which oven on a given day.
|
The Oven Scheduler helps you plan which jobs go into which oven on a given day.
|
||||||
Jobs waiting to be coated appear in the <strong>Queue</strong> on the left.
|
Jobs waiting to be coated appear in the <strong>Queue</strong> on the left.
|
||||||
Each oven gets its own column — create <strong>batches</strong> and drag jobs into them
|
Each oven gets its own column — create <strong>batches</strong> and drag jobs into them
|
||||||
to build your day's run sheet.
|
to build your day's run sheet.
|
||||||
</p>
|
</p>
|
||||||
<p class="small text-muted mb-0">
|
<p class="small text-muted mb-0">
|
||||||
@@ -292,8 +292,8 @@
|
|||||||
<li class="mb-1">Use the <strong>date arrows</strong> to navigate to the day you want to schedule.</li>
|
<li class="mb-1">Use the <strong>date arrows</strong> to navigate to the day you want to schedule.</li>
|
||||||
<li class="mb-1">Click <strong>New Batch</strong> and pick an oven to create a batch slot.</li>
|
<li class="mb-1">Click <strong>New Batch</strong> and pick an oven to create a batch slot.</li>
|
||||||
<li class="mb-1"><strong>Drag job coat items</strong> from the Queue into a batch, or drag between batches to reorder.</li>
|
<li class="mb-1"><strong>Drag job coat items</strong> from the Queue into a batch, or drag between batches to reorder.</li>
|
||||||
<li class="mb-1">Set a <strong>start time</strong> on each batch and monitor the capacity bar — it turns yellow at 80 % and red when over capacity.</li>
|
<li class="mb-1">Set a <strong>start time</strong> on each batch and monitor the capacity bar — it turns yellow at 80 % and red when over capacity.</li>
|
||||||
<li class="mb-1">Use the batch <strong>status buttons</strong> (Planned → Loading → In Progress → Completed) to track real-time progress.</li>
|
<li class="mb-1">Use the batch <strong>status buttons</strong> (Planned → Loading → In Progress → Completed) to track real-time progress.</li>
|
||||||
<li class="mb-0">Drag an item back to the Queue to unschedule it.</li>
|
<li class="mb-0">Drag an item back to the Queue to unschedule it.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,23 +303,23 @@
|
|||||||
<h6 class="fw-semibold mb-2"><i class="bi bi-gear-fill text-warning me-1"></i>Setup for best results</h6>
|
<h6 class="fw-semibold mb-2"><i class="bi bi-gear-fill text-warning me-1"></i>Setup for best results</h6>
|
||||||
<ul class="small text-muted mb-0 ps-3">
|
<ul class="small text-muted mb-0 ps-3">
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<strong>Ovens in Equipment</strong> — each oven must exist as an Equipment record
|
<strong>Ovens in Equipment</strong> — each oven must exist as an Equipment record
|
||||||
with <em>Type = Oven</em> and Status = Operational for it to appear as a column.
|
with <em>Type = Oven</em> and Status = Operational for it to appear as a column.
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<strong>Oven capacity</strong> — set a <em>capacity (sq ft)</em> on each oven so
|
<strong>Oven capacity</strong> — set a <em>capacity (sq ft)</em> on each oven so
|
||||||
the capacity bar can warn you before you overload a batch.
|
the capacity bar can warn you before you overload a batch.
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<strong>Job items with surface area</strong> — enter <em>surface area (sq ft)</em>
|
<strong>Job items with surface area</strong> — enter <em>surface area (sq ft)</em>
|
||||||
on each job item so the scheduler can calculate load accurately.
|
on each job item so the scheduler can calculate load accurately.
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<strong>Coat quantities</strong> — job items need at least one coat defined
|
<strong>Coat quantities</strong> — job items need at least one coat defined
|
||||||
(powder color + quantity) so they appear as draggable coat rows in the Queue.
|
(powder color + quantity) so they appear as draggable coat rows in the Queue.
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-0">
|
<li class="mb-0">
|
||||||
<strong>Due dates & priorities</strong> — set due dates and priorities on
|
<strong>Due dates & priorities</strong> — set due dates and priorities on
|
||||||
jobs so the AI and sorting tools can recommend the most urgent work first.
|
jobs so the AI and sorting tools can recommend the most urgent work first.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
<!-- No ovens warning -->
|
<!-- No ovens warning -->
|
||||||
@if (!Model.Ovens.Any())
|
@if (!Model.Ovens.Any())
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
No active ovens found. Go to <a asp-controller="Equipment" asp-action="Index">Equipment</a> and add a piece of equipment with Type = "Oven".
|
No active ovens found. Go to <a asp-controller="Equipment" asp-action="Index">Equipment</a> and add a piece of equipment with Type = "Oven".
|
||||||
</div>
|
</div>
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
<!-- Main layout -->
|
<!-- Main layout -->
|
||||||
<div class="scheduler-layout">
|
<div class="scheduler-layout">
|
||||||
|
|
||||||
<!-- ── JOB QUEUE (left sidebar) ───────────────── -->
|
<!-- ── JOB QUEUE (left sidebar) ───────────────── -->
|
||||||
<div class="scheduler-queue">
|
<div class="scheduler-queue">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header d-flex align-items-center py-2">
|
<div class="card-header d-flex align-items-center py-2">
|
||||||
@@ -408,11 +408,11 @@
|
|||||||
<span class="color-dot" style="background:@GetColorHex(coat.ColorName, coat.ColorCode)"></span>
|
<span class="color-dot" style="background:@GetColorHex(coat.ColorName, coat.ColorCode)"></span>
|
||||||
}
|
}
|
||||||
<span class="fw-medium">@coat.CoatName</span>
|
<span class="fw-medium">@coat.CoatName</span>
|
||||||
<span class="text-muted ms-1">— @coat.ItemDescription</span>
|
<span class="text-muted ms-1">— @coat.ItemDescription</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted" style="font-size:.75rem;">
|
<div class="text-muted" style="font-size:.75rem;">
|
||||||
Pass @coat.CoatPassNumber · @coat.SurfaceAreaSqFt.ToString("F1") sqft
|
Pass @coat.CoatPassNumber · @coat.SurfaceAreaSqFt.ToString("F1") sqft
|
||||||
@if (!string.IsNullOrEmpty(coat.ColorName)) { <span>· @coat.ColorName</span> }
|
@if (!string.IsNullOrEmpty(coat.ColorName)) { <span>· @coat.ColorName</span> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i class="bi bi-grip-vertical text-muted ms-1"></i>
|
<i class="bi bi-grip-vertical text-muted ms-1"></i>
|
||||||
@@ -426,7 +426,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── OVEN COLUMNS ───────────────────────────── -->
|
<!-- ── OVEN COLUMNS ───────────────────────────── -->
|
||||||
<div class="scheduler-board">
|
<div class="scheduler-board">
|
||||||
<div class="oven-columns" id="ovenColumns">
|
<div class="oven-columns" id="ovenColumns">
|
||||||
@foreach (var oven in Model.Ovens)
|
@foreach (var oven in Model.Ovens)
|
||||||
@@ -447,7 +447,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
@(oven.MaxLoadSqFt.HasValue ? $"{oven.MaxLoadSqFt:F0} sqft max" : "No capacity set")
|
@(oven.MaxLoadSqFt.HasValue ? $"{oven.MaxLoadSqFt:F0} sqft max" : "No capacity set")
|
||||||
· @oven.CycleMinutes min cycle
|
· @oven.CycleMinutes min cycle
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-outline-primary ms-auto"
|
<button class="btn btn-sm btn-outline-primary ms-auto"
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
|
|
||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
<div id="aiError" class="d-none p-4">
|
<div id="aiError" class="d-none p-4">
|
||||||
<div class="alert alert-danger mb-0">
|
<div class="alert alert-danger alert-permanent mb-0">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<span id="aiErrorText"></span>
|
<span id="aiErrorText"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -546,8 +546,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
@if (!Model.QueuedJobs.Any())
|
@if (!Model.QueuedJobs.Any())
|
||||||
{
|
{
|
||||||
<div class="alert alert-info small">
|
<div class="alert alert-info alert-permanent small">
|
||||||
<i class="bi bi-info-circle me-1"></i>The queue is empty — no jobs need oven scheduling right now.
|
<i class="bi bi-info-circle me-1"></i>The queue is empty — no jobs need oven scheduling right now.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
// ── Info panel (always visible on load; dismiss hides for this visit only) ─
|
// ── Info panel (always visible on load; dismiss hides for this visit only) ─
|
||||||
document.getElementById('btnDismissInfo').addEventListener('click', function () {
|
document.getElementById('btnDismissInfo').addEventListener('click', function () {
|
||||||
document.getElementById('schedulerInfoPanel').style.display = 'none';
|
document.getElementById('schedulerInfoPanel').style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.DTOs.Subscription
|
@using PowderCoating.Application.DTOs.Subscription
|
||||||
@model UpdateSubscriptionPlanConfigDto
|
@model UpdateSubscriptionPlanConfigDto
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Edit {ViewBag.PlanName} Plan";
|
ViewData["Title"] = $"Edit {ViewBag.PlanName} Plan";
|
||||||
@@ -179,12 +179,12 @@
|
|||||||
|
|
||||||
<h5 class="mb-3 pb-2 border-bottom mt-4">Stripe Integration</h5>
|
<h5 class="mb-3 pb-2 border-bottom mt-4">Stripe Integration</h5>
|
||||||
|
|
||||||
<div class="alert alert-info small mb-3" role="alert">
|
<div class="alert alert-info alert-permanent small mb-3" role="alert">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<strong>Where to find price IDs:</strong>
|
<strong>Where to find price IDs:</strong>
|
||||||
In your <a href="https://dashboard.stripe.com/products" target="_blank" class="alert-link">Stripe Dashboard</a>,
|
In your <a href="https://dashboard.stripe.com/products" target="_blank" class="alert-link">Stripe Dashboard</a>,
|
||||||
open the product, then look in the <strong>Pricing</strong> section for the specific price.
|
open the product, then look in the <strong>Pricing</strong> section for the specific price.
|
||||||
The ID starts with <code>price_</code> — <em>not</em> the product ID which starts with <code>prod_</code>.
|
The ID starts with <code>price_</code> — <em>not</em> the product ID which starts with <code>prod_</code>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.User.CreateSuperAdminDto
|
@model PowderCoating.Application.DTOs.User.CreateSuperAdminDto
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create SuperAdmin";
|
ViewData["Title"] = "Create SuperAdmin";
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
<strong>Important:</strong> SuperAdmins have full platform access across all companies. Use this feature carefully.
|
<strong>Important:</strong> SuperAdmins have full platform access across all companies. Use this feature carefully.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto
|
@model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "User Details";
|
ViewData["Title"] = "User Details";
|
||||||
var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false;
|
var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false;
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
@if (Model.IsBanned)
|
@if (Model.IsBanned)
|
||||||
{
|
{
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-danger mb-0">
|
<div class="alert alert-danger alert-permanent mb-0">
|
||||||
<i class="bi bi-slash-circle-fill me-2"></i>
|
<i class="bi bi-slash-circle-fill me-2"></i>
|
||||||
<strong>This account is banned.</strong>
|
<strong>This account is banned.</strong>
|
||||||
@if (Model.BannedAt.HasValue)
|
@if (Model.BannedAt.HasValue)
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
asp-route-returnUrl="@detailsReturnUrl" method="post">
|
asp-route-returnUrl="@detailsReturnUrl" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
Banning <strong>@Model.FullName</strong> will immediately prevent them from logging in.
|
Banning <strong>@Model.FullName</strong> will immediately prevent them from logging in.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Grant SuperAdmin Access";
|
ViewData["Title"] = "Grant SuperAdmin Access";
|
||||||
ViewData["PageIcon"] = "bi-shield-plus";
|
ViewData["PageIcon"] = "bi-shield-plus";
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<i class="bi bi-shield-exclamation me-2"></i>
|
<i class="bi bi-shield-exclamation me-2"></i>
|
||||||
<strong>Caution:</strong> SuperAdmins have full platform access across <em>all companies</em>. Only promote users you trust completely with platform-level administration.
|
<strong>Caution:</strong> SuperAdmins have full platform access across <em>all companies</em>. Only promote users you trust completely with platform-level administration.
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" name="userId" id="grantUserId" />
|
<input type="hidden" name="userId" id="grantUserId" />
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Warning:</strong> This user will gain full platform access across all companies.
|
<strong>Warning:</strong> This user will gain full platform access across all companies.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
|
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
|
||||||
|
|
||||||
@section Styles {
|
@section Styles {
|
||||||
<style>
|
<style>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="btn btn-outline-secondary disabled" title="Root account — protected">
|
<span class="btn btn-outline-secondary disabled" title="Root account — protected">
|
||||||
<i class="bi bi-shield-lock"></i>
|
<i class="bi bi-shield-lock"></i>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
|
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
|
||||||
<div class="mobile-card-view">
|
<div class="mobile-card-view">
|
||||||
<div class="mobile-card-list">
|
<div class="mobile-card-list">
|
||||||
@foreach (var user in Model.Items)
|
@foreach (var user in Model.Items)
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-card-footer">
|
<div class="mobile-card-footer">
|
||||||
<span class="btn btn-sm btn-outline-primary">View →</span>
|
<span class="btn btn-sm btn-outline-primary">View →</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -373,7 +373,7 @@
|
|||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" name="id" id="revokeUserId" />
|
<input type="hidden" name="id" id="revokeUserId" />
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Warning:</strong> This will remove platform-wide access. The user will be demoted to Company Admin.
|
<strong>Warning:</strong> This will remove platform-wide access. The user will be demoted to Company Admin.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel
|
@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var enableAiLookup = ViewData["EnableAiLookup"] as bool? == true;
|
var enableAiLookup = ViewData["EnableAiLookup"] as bool? == true;
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3 small"></div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
|
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Powder Insights";
|
ViewData["Title"] = "Powder Insights";
|
||||||
ViewData["PageIcon"] = "bi-graph-up";
|
ViewData["PageIcon"] = "bi-graph-up";
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── KPI cards ── *@
|
@* ── KPI cards ── *@
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Tabs ── *@
|
@* ── Tabs ── *@
|
||||||
<ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist">
|
<ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="forecast-tab" data-bs-toggle="tab" data-bs-target="#forecast" type="button">
|
<button class="nav-link active" id="forecast-tab" data-bs-toggle="tab" data-bs-target="#forecast" type="button">
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
|
||||||
@* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@
|
@* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@
|
||||||
<div class="tab-pane fade show active" id="forecast" role="tabpanel">
|
<div class="tab-pane fade show active" id="forecast" role="tabpanel">
|
||||||
@if (!Model.LowStockAlerts.Any())
|
@if (!Model.LowStockAlerts.Any())
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
{
|
{
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-transparent">
|
<div class="card-header bg-transparent">
|
||||||
<h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock — Active Jobs</h6>
|
<h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock — Active Jobs</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<td class="text-end fw-semibold">@item.CurrentStockLbs.ToString("0.##") lbs</td>
|
<td class="text-end fw-semibold">@item.CurrentStockLbs.ToString("0.##") lbs</td>
|
||||||
<td class="text-end">@item.ScheduledDemandLbs.ToString("0.##") lbs</td>
|
<td class="text-end">@item.ScheduledDemandLbs.ToString("0.##") lbs</td>
|
||||||
<td class="text-end @(item.ShortfallLbs > 0 ? "text-danger fw-bold" : "text-muted")">
|
<td class="text-end @(item.ShortfallLbs > 0 ? "text-danger fw-bold" : "text-muted")">
|
||||||
@(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "—")
|
@(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "—")
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">@item.ActiveJobCount</td>
|
<td class="text-center">@item.ActiveJobCount</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
|
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
|
||||||
<div class="tab-pane fade" id="efficiency" role="tabpanel">
|
<div class="tab-pane fade" id="efficiency" role="tabpanel">
|
||||||
@if (!readiness.IsLayer2Ready)
|
@if (!readiness.IsLayer2Ready)
|
||||||
{
|
{
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
else if (!Model.EfficiencyBySku.Any())
|
else if (!Model.EfficiencyBySku.Any())
|
||||||
{
|
{
|
||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<p>No efficiency data yet — record actual powder usage on completed jobs to see this chart.</p>
|
<p>No efficiency data yet — record actual powder usage on completed jobs to see this chart.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -188,11 +188,11 @@
|
|||||||
<strong>@eff.Name</strong>
|
<strong>@eff.Name</strong>
|
||||||
@if (!string.IsNullOrEmpty(eff.ColorName))
|
@if (!string.IsNullOrEmpty(eff.ColorName))
|
||||||
{
|
{
|
||||||
<br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small>
|
<br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small>
|
||||||
}
|
}
|
||||||
@if (!eff.HasEnoughData)
|
@if (!eff.HasEnoughData)
|
||||||
{
|
{
|
||||||
<br /><small class="text-muted fst-italic">Low confidence — need 5+ samples</small>
|
<br /><small class="text-muted fst-italic">Low confidence — need 5+ samples</small>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
|
<td class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
|
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
|
||||||
<div class="tab-pane fade" id="predictive" role="tabpanel">
|
<div class="tab-pane fade" id="predictive" role="tabpanel">
|
||||||
@if (!readiness.IsLayer3Ready)
|
@if (!readiness.IsLayer3Ready)
|
||||||
{
|
{
|
||||||
@@ -235,12 +235,12 @@
|
|||||||
<strong>@readiness.JobsWithActualData</strong> of <strong>@readiness.Layer3MinJobs</strong> jobs recorded
|
<strong>@readiness.JobsWithActualData</strong> of <strong>@readiness.Layer3MinJobs</strong> jobs recorded
|
||||||
(@readiness.Layer3ProgressPercent%)
|
(@readiness.Layer3ProgressPercent%)
|
||||||
</p>
|
</p>
|
||||||
<div class="alert alert-info text-start">
|
<div class="alert alert-info alert-permanent text-start">
|
||||||
<h6 class="alert-heading"><i class="bi bi-lightbulb me-2"></i>What unlocks here</h6>
|
<h6 class="alert-heading"><i class="bi bi-lightbulb me-2"></i>What unlocks here</h6>
|
||||||
<ul class="mb-0 small">
|
<ul class="mb-0 small">
|
||||||
<li><strong>Smart reorder suggestions</strong> — quantity recommendations based on your actual usage history + scheduled job pipeline</li>
|
<li><strong>Smart reorder suggestions</strong> — quantity recommendations based on your actual usage history + scheduled job pipeline</li>
|
||||||
<li><strong>Waste pattern detection</strong> — identifies jobs and powder types that consistently over-consume</li>
|
<li><strong>Waste pattern detection</strong> — identifies jobs and powder types that consistently over-consume</li>
|
||||||
<li><strong>Per-powder efficiency corrections</strong> — suggests updating coverage defaults based on real data</li>
|
<li><strong>Per-powder efficiency corrections</strong> — suggests updating coverage defaults based on real data</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (!Model.ReorderSuggestions.Any())
|
@if (!Model.ReorderSuggestions.Any())
|
||||||
{
|
{
|
||||||
<div class="card-body text-muted text-center py-4">No reorder suggestions — stock levels look good for upcoming pipeline.</div>
|
<div class="card-body text-muted text-center py-4">No reorder suggestions — stock levels look good for upcoming pipeline.</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
<strong>@s.Name</strong>
|
<strong>@s.Name</strong>
|
||||||
@if (!string.IsNullOrEmpty(s.ColorName))
|
@if (!string.IsNullOrEmpty(s.ColorName))
|
||||||
{
|
{
|
||||||
<br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small>
|
<br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td>
|
<td class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td>
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
@* Waste Patterns *@
|
@* Waste Patterns *@
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-transparent">
|
<div class="card-header bg-transparent">
|
||||||
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal">— coats that used >20% more than estimated</small></h6>
|
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal">— coats that used >20% more than estimated</small></h6>
|
||||||
</div>
|
</div>
|
||||||
@if (!Model.WastePatterns.Any())
|
@if (!Model.WastePatterns.Any())
|
||||||
{
|
{
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
<br /><small class="text-muted">@w.CoatName</small>
|
<br /><small class="text-muted">@w.CoatName</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted small">@(w.InventoryItemName ?? "Custom")</td>
|
<td class="text-muted small">@(w.InventoryItemName ?? "Custom")</td>
|
||||||
<td>@(w.Complexity ?? "—")</td>
|
<td>@(w.Complexity ?? "—")</td>
|
||||||
<td class="text-end">@w.EstimatedLbs.ToString("0.##") lbs</td>
|
<td class="text-end">@w.EstimatedLbs.ToString("0.##") lbs</td>
|
||||||
<td class="text-end">@w.ActualLbs.ToString("0.##") lbs</td>
|
<td class="text-end">@w.ActualLbs.ToString("0.##") lbs</td>
|
||||||
<td class="text-end text-danger fw-bold">+@w.OveragePct.ToString("0.#")%</td>
|
<td class="text-end text-danger fw-bold">+@w.OveragePct.ToString("0.#")%</td>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.User.UserProfileDto
|
@model PowderCoating.Application.DTOs.User.UserProfileDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "My Profile";
|
ViewData["Title"] = "My Profile";
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Personal Information"
|
data-bs-title="Personal Information"
|
||||||
data-bs-content="First Name, Last Name, and Phone are editable and saved when you click Save Profile. Email is shown here for reference — change it on the Security tab. Department, Position, Role, and Employee Number are set by an administrator and cannot be changed here.">
|
data-bs-content="First Name, Last Name, and Phone are editable and saved when you click Save Profile. Email is shown here for reference — change it on the Security tab. Department, Position, Role, and Employee Number are set by an administrator and cannot be changed here.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<input type="password" class="form-control" id="confirmPassword" name="ConfirmPassword" required />
|
<input type="password" class="form-control" id="confirmPassword" name="ConfirmPassword" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mt-3 mb-0">
|
<div class="alert alert-info alert-permanent mt-3 mb-0">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<strong>Password requirements:</strong> At least 8 characters with uppercase, lowercase, and a digit.
|
<strong>Password requirements:</strong> At least 8 characters with uppercase, lowercase, and a digit.
|
||||||
</div>
|
</div>
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Appearance Settings"
|
data-bs-title="Appearance Settings"
|
||||||
data-bs-content="Theme switches the app between Light and Dark mode. Sidebar Color changes the navigation panel background — click a swatch to preview it instantly. Date Format controls how dates display throughout the app. Timezone is used to localize timestamps. Click Save Appearance to persist your choices.">
|
data-bs-content="Theme switches the app between Light and Dark mode. Sidebar Color changes the navigation panel background — click a swatch to preview it instantly. Date Format controls how dates display throughout the app. Timezone is used to localize timestamps. Click Save Appearance to persist your choices.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,39 +364,39 @@
|
|||||||
<label class="form-label fw-semibold">Timezone</label>
|
<label class="form-label fw-semibold">Timezone</label>
|
||||||
<select class="form-select" id="timezoneInput" name="TimeZone" style="max-width:350px;">
|
<select class="form-select" id="timezoneInput" name="TimeZone" style="max-width:350px;">
|
||||||
<optgroup label="United States">
|
<optgroup label="United States">
|
||||||
<option value="America/New_York" selected="@(Model.TimeZone == "America/New_York" ? "selected" : null)">Eastern (ET) — New York</option>
|
<option value="America/New_York" selected="@(Model.TimeZone == "America/New_York" ? "selected" : null)">Eastern (ET) — New York</option>
|
||||||
<option value="America/Chicago" selected="@(Model.TimeZone == "America/Chicago" ? "selected" : null)">Central (CT) — Chicago</option>
|
<option value="America/Chicago" selected="@(Model.TimeZone == "America/Chicago" ? "selected" : null)">Central (CT) — Chicago</option>
|
||||||
<option value="America/Denver" selected="@(Model.TimeZone == "America/Denver" ? "selected" : null)">Mountain (MT) — Denver</option>
|
<option value="America/Denver" selected="@(Model.TimeZone == "America/Denver" ? "selected" : null)">Mountain (MT) — Denver</option>
|
||||||
<option value="America/Phoenix" selected="@(Model.TimeZone == "America/Phoenix" ? "selected" : null)">Mountain no-DST — Phoenix</option>
|
<option value="America/Phoenix" selected="@(Model.TimeZone == "America/Phoenix" ? "selected" : null)">Mountain no-DST — Phoenix</option>
|
||||||
<option value="America/Los_Angeles" selected="@(Model.TimeZone == "America/Los_Angeles" ? "selected" : null)">Pacific (PT) — Los Angeles</option>
|
<option value="America/Los_Angeles" selected="@(Model.TimeZone == "America/Los_Angeles" ? "selected" : null)">Pacific (PT) — Los Angeles</option>
|
||||||
<option value="America/Anchorage" selected="@(Model.TimeZone == "America/Anchorage" ? "selected" : null)">Alaska (AKT) — Anchorage</option>
|
<option value="America/Anchorage" selected="@(Model.TimeZone == "America/Anchorage" ? "selected" : null)">Alaska (AKT) — Anchorage</option>
|
||||||
<option value="Pacific/Honolulu" selected="@(Model.TimeZone == "Pacific/Honolulu" ? "selected" : null)">Hawaii (HT) — Honolulu</option>
|
<option value="Pacific/Honolulu" selected="@(Model.TimeZone == "Pacific/Honolulu" ? "selected" : null)">Hawaii (HT) — Honolulu</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="Canada">
|
<optgroup label="Canada">
|
||||||
<option value="America/Halifax" selected="@(Model.TimeZone == "America/Halifax" ? "selected" : null)">Atlantic (AT) — Halifax</option>
|
<option value="America/Halifax" selected="@(Model.TimeZone == "America/Halifax" ? "selected" : null)">Atlantic (AT) — Halifax</option>
|
||||||
<option value="America/Toronto" selected="@(Model.TimeZone == "America/Toronto" ? "selected" : null)">Eastern — Toronto</option>
|
<option value="America/Toronto" selected="@(Model.TimeZone == "America/Toronto" ? "selected" : null)">Eastern — Toronto</option>
|
||||||
<option value="America/Winnipeg" selected="@(Model.TimeZone == "America/Winnipeg" ? "selected" : null)">Central — Winnipeg</option>
|
<option value="America/Winnipeg" selected="@(Model.TimeZone == "America/Winnipeg" ? "selected" : null)">Central — Winnipeg</option>
|
||||||
<option value="America/Edmonton" selected="@(Model.TimeZone == "America/Edmonton" ? "selected" : null)">Mountain — Edmonton</option>
|
<option value="America/Edmonton" selected="@(Model.TimeZone == "America/Edmonton" ? "selected" : null)">Mountain — Edmonton</option>
|
||||||
<option value="America/Vancouver" selected="@(Model.TimeZone == "America/Vancouver" ? "selected" : null)">Pacific — Vancouver</option>
|
<option value="America/Vancouver" selected="@(Model.TimeZone == "America/Vancouver" ? "selected" : null)">Pacific — Vancouver</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="Europe">
|
<optgroup label="Europe">
|
||||||
<option value="Europe/London" selected="@(Model.TimeZone == "Europe/London" ? "selected" : null)">GMT/BST — London</option>
|
<option value="Europe/London" selected="@(Model.TimeZone == "Europe/London" ? "selected" : null)">GMT/BST — London</option>
|
||||||
<option value="Europe/Paris" selected="@(Model.TimeZone == "Europe/Paris" ? "selected" : null)">CET — Paris / Berlin</option>
|
<option value="Europe/Paris" selected="@(Model.TimeZone == "Europe/Paris" ? "selected" : null)">CET — Paris / Berlin</option>
|
||||||
<option value="Europe/Helsinki" selected="@(Model.TimeZone == "Europe/Helsinki" ? "selected" : null)">EET — Helsinki</option>
|
<option value="Europe/Helsinki" selected="@(Model.TimeZone == "Europe/Helsinki" ? "selected" : null)">EET — Helsinki</option>
|
||||||
<option value="Europe/Moscow" selected="@(Model.TimeZone == "Europe/Moscow" ? "selected" : null)">MSK — Moscow</option>
|
<option value="Europe/Moscow" selected="@(Model.TimeZone == "Europe/Moscow" ? "selected" : null)">MSK — Moscow</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="Asia / Pacific">
|
<optgroup label="Asia / Pacific">
|
||||||
<option value="Asia/Dubai" selected="@(Model.TimeZone == "Asia/Dubai" ? "selected" : null)">GST — Dubai</option>
|
<option value="Asia/Dubai" selected="@(Model.TimeZone == "Asia/Dubai" ? "selected" : null)">GST — Dubai</option>
|
||||||
<option value="Asia/Kolkata" selected="@(Model.TimeZone == "Asia/Kolkata" ? "selected" : null)">IST — India</option>
|
<option value="Asia/Kolkata" selected="@(Model.TimeZone == "Asia/Kolkata" ? "selected" : null)">IST — India</option>
|
||||||
<option value="Asia/Bangkok" selected="@(Model.TimeZone == "Asia/Bangkok" ? "selected" : null)">ICT — Bangkok</option>
|
<option value="Asia/Bangkok" selected="@(Model.TimeZone == "Asia/Bangkok" ? "selected" : null)">ICT — Bangkok</option>
|
||||||
<option value="Asia/Shanghai" selected="@(Model.TimeZone == "Asia/Shanghai" ? "selected" : null)">CST — Beijing / Shanghai</option>
|
<option value="Asia/Shanghai" selected="@(Model.TimeZone == "Asia/Shanghai" ? "selected" : null)">CST — Beijing / Shanghai</option>
|
||||||
<option value="Asia/Tokyo" selected="@(Model.TimeZone == "Asia/Tokyo" ? "selected" : null)">JST — Tokyo</option>
|
<option value="Asia/Tokyo" selected="@(Model.TimeZone == "Asia/Tokyo" ? "selected" : null)">JST — Tokyo</option>
|
||||||
<option value="Australia/Sydney" selected="@(Model.TimeZone == "Australia/Sydney" ? "selected" : null)">AEST — Sydney</option>
|
<option value="Australia/Sydney" selected="@(Model.TimeZone == "Australia/Sydney" ? "selected" : null)">AEST — Sydney</option>
|
||||||
<option value="Pacific/Auckland" selected="@(Model.TimeZone == "Pacific/Auckland" ? "selected" : null)">NZST — Auckland</option>
|
<option value="Pacific/Auckland" selected="@(Model.TimeZone == "Pacific/Auckland" ? "selected" : null)">NZST — Auckland</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="South America">
|
<optgroup label="South America">
|
||||||
<option value="America/Sao_Paulo" selected="@(Model.TimeZone == "America/Sao_Paulo" ? "selected" : null)">BRT — São Paulo</option>
|
<option value="America/Sao_Paulo" selected="@(Model.TimeZone == "America/Sao_Paulo" ? "selected" : null)">BRT — São Paulo</option>
|
||||||
<option value="America/Buenos_Aires" selected="@(Model.TimeZone == "America/Buenos_Aires" ? "selected" : null)">ART — Buenos Aires</option>
|
<option value="America/Buenos_Aires" selected="@(Model.TimeZone == "America/Buenos_Aires" ? "selected" : null)">ART — Buenos Aires</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="UTC">
|
<optgroup label="UTC">
|
||||||
<option value="UTC" selected="@(Model.TimeZone == "UTC" ? "selected" : null)">UTC</option>
|
<option value="UTC" selected="@(Model.TimeZone == "UTC" ? "selected" : null)">UTC</option>
|
||||||
@@ -538,7 +538,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Theme radio — map light/dark → paper/ink surface system
|
// Theme radio — map light/dark → paper/ink surface system
|
||||||
document.querySelectorAll('input[name="theme"]').forEach(radio => {
|
document.querySelectorAll('input[name="theme"]').forEach(radio => {
|
||||||
radio.addEventListener('change', function () {
|
radio.addEventListener('change', function () {
|
||||||
var surface = this.value === 'dark' ? 'ink' : 'paper';
|
var surface = this.value === 'dark' ? 'ink' : 'paper';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.DTOs.PurchaseOrder
|
@using PowderCoating.Application.DTOs.PurchaseOrder
|
||||||
@model CreatePurchaseOrderDto
|
@model CreatePurchaseOrderDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<h4 class="mb-0">
|
<h4 class="mb-0">
|
||||||
@if (fromLowStock)
|
@if (fromLowStock)
|
||||||
{
|
{
|
||||||
<span><i class="bi bi-lightning-charge text-warning me-1"></i> New PO — From Low Stock</span>
|
<span><i class="bi bi-lightning-charge text-warning me-1"></i> New PO — From Low Stock</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="New Purchase Order"
|
data-bs-title="New Purchase Order"
|
||||||
data-bs-content="POs are saved as Draft and can be edited before submitting. Add line items from your inventory catalog or enter a custom description for items not in the system. Once submitted you can receive goods against the PO — receiving automatically updates inventory quantities. After receiving, use Create Bill to convert the PO into a payable bill.">
|
data-bs-content="POs are saved as Draft and can be edited before submitting. Add line items from your inventory catalog or enter a custom description for items not in the system. Once submitted you can receive goods against the PO — receiving automatically updates inventory quantities. After receiving, use Create Bill to convert the PO into a payable bill.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
@if (!ViewData.ModelState.IsValid)
|
@if (!ViewData.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Order Details"
|
data-bs-title="Order Details"
|
||||||
data-bs-content="Select the vendor you're ordering from. Order Date defaults to today — adjust if you're entering a past order. Expected Delivery is used to flag the PO as Overdue on the list view if goods haven't arrived by that date.">
|
data-bs-content="Select the vendor you're ordering from. Order Date defaults to today — adjust if you're entering a past order. Expected Delivery is used to flag the PO as Overdue on the list view if goods haven't arrived by that date.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<label asp-for="VendorId" class="form-label fw-semibold">Vendor <span class="text-danger">*</span></label>
|
<label asp-for="VendorId" class="form-label fw-semibold">Vendor <span class="text-danger">*</span></label>
|
||||||
<select asp-for="VendorId" asp-items="@ViewBag.Vendors" class="form-select"
|
<select asp-for="VendorId" asp-items="@ViewBag.Vendors" class="form-select"
|
||||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="VendorId" class="text-danger small"></span>
|
<span asp-validation-for="VendorId" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,12 +147,12 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
|
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
|
||||||
<textarea asp-for="Notes" class="form-control" rows="3"
|
<textarea asp-for="Notes" class="form-control" rows="3"
|
||||||
placeholder="Visible on PO…"></textarea>
|
placeholder="Visible on PO…"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
|
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
|
||||||
<textarea asp-for="InternalNotes" class="form-control" rows="3"
|
<textarea asp-for="InternalNotes" class="form-control" rows="3"
|
||||||
placeholder="Internal use only…"></textarea>
|
placeholder="Internal use only…"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.DTOs.PurchaseOrder
|
@using PowderCoating.Application.DTOs.PurchaseOrder
|
||||||
@model UpdatePurchaseOrderDto
|
@model UpdatePurchaseOrderDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info d-flex align-items-center gap-2 mb-3">
|
<div class="alert alert-info alert-permanent d-flex align-items-center gap-2 mb-3">
|
||||||
<i class="bi bi-info-circle-fill"></i>
|
<i class="bi bi-info-circle-fill"></i>
|
||||||
<span>Only <strong>Draft</strong> purchase orders can be edited.</span>
|
<span>Only <strong>Draft</strong> purchase orders can be edited.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!ViewData.ModelState.IsValid)
|
@if (!ViewData.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<label asp-for="VendorId" class="form-label fw-semibold">Vendor <span class="text-danger">*</span></label>
|
<label asp-for="VendorId" class="form-label fw-semibold">Vendor <span class="text-danger">*</span></label>
|
||||||
<select asp-for="VendorId" asp-items="@ViewBag.Vendors" class="form-select"
|
<select asp-for="VendorId" asp-items="@ViewBag.Vendors" class="form-select"
|
||||||
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
|
||||||
<option value="__new__">+ Add New Vendor…</option>
|
<option value="__new__">+ Add New Vendor…</option>
|
||||||
</select>
|
</select>
|
||||||
<span asp-validation-for="VendorId" class="text-danger small"></span>
|
<span asp-validation-for="VendorId" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,12 +114,12 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
|
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
|
||||||
<textarea asp-for="Notes" class="form-control" rows="3"
|
<textarea asp-for="Notes" class="form-control" rows="3"
|
||||||
placeholder="Visible on PO…"></textarea>
|
placeholder="Visible on PO…"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
|
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
|
||||||
<textarea asp-for="InternalNotes" class="form-control" rows="3"
|
<textarea asp-for="InternalNotes" class="form-control" rows="3"
|
||||||
placeholder="Internal use only…"></textarea>
|
placeholder="Internal use only…"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@using PowderCoating.Application.DTOs.PurchaseOrder
|
@using PowderCoating.Application.DTOs.PurchaseOrder
|
||||||
@model ReceivePurchaseOrderDto
|
@model ReceivePurchaseOrderDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Receive Goods — {ViewBag.PoNumber}";
|
ViewData["Title"] = $"Receive Goods — {ViewBag.PoNumber}";
|
||||||
int poId = (int)ViewBag.PoId;
|
int poId = (int)ViewBag.PoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Receive Goods"
|
data-bs-title="Receive Goods"
|
||||||
data-bs-content="Enter the quantity actually received for each line item. Use Receive All to fill in the full remaining quantity for every item. You can receive partial quantities — the PO becomes Partially Received and you can come back to record the rest later. Saving automatically adds the received quantities to inventory on hand and records purchase transactions.">
|
data-bs-content="Enter the quantity actually received for each line item. Use Receive All to fill in the full remaining quantity for every item. You can receive partial quantities — the PO becomes Partially Received and you can come back to record the rest later. Saving automatically adds the received quantities to inventory on hand and records purchase transactions.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">@ViewBag.PoNumber · @ViewBag.VendorName · Ordered @((DateTime)ViewBag.OrderDate).ToString("MM/dd/yyyy")</small>
|
<small class="text-muted">@ViewBag.PoNumber · @ViewBag.VendorName · Ordered @((DateTime)ViewBag.OrderDate).ToString("MM/dd/yyyy")</small>
|
||||||
</div>
|
</div>
|
||||||
<a asp-action="Details" asp-route-id="@poId" class="btn btn-sm btn-outline-secondary">
|
<a asp-action="Details" asp-route-id="@poId" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="bi bi-arrow-left"></i> Back
|
<i class="bi bi-arrow-left"></i> Back
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||||
data-bs-title="Items to Receive"
|
data-bs-title="Items to Receive"
|
||||||
data-bs-content="Remaining = Ordered minus Previously Received. Enter how many units arrived in this shipment — you can enter less than Remaining for a partial delivery. Rows already fully received are shown in green and cannot be edited. Only inventory catalog items will have their stock quantities updated on save.">
|
data-bs-content="Remaining = Ordered minus Previously Received. Enter how many units arrived in this shipment — you can enter less than Remaining for a partial delivery. Rows already fully received are shown in green and cannot be edited. Only inventory catalog items will have their stock quantities updated on save.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<input type="hidden" name="Items[@i].QuantityToReceive" value="0" />
|
<input type="hidden" name="Items[@i].QuantityToReceive" value="0" />
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -132,12 +132,12 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
|
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
|
||||||
<textarea asp-for="Notes" class="form-control" rows="3"
|
<textarea asp-for="Notes" class="form-control" rows="3"
|
||||||
placeholder="Any notes about this receipt…"></textarea>
|
placeholder="Any notes about this receipt…"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info small">
|
<div class="alert alert-info alert-permanent small">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
Receiving goods will automatically update inventory quantities and record purchase transactions.
|
Receiving goods will automatically update inventory quantities and record purchase transactions.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
|
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
|
||||||
@{
|
@{
|
||||||
Layout = "_QuoteApprovalLayout";
|
Layout = "_QuoteApprovalLayout";
|
||||||
ViewData["Title"] = $"Quote {Model.QuoteNumber}";
|
ViewData["Title"] = $"Quote {Model.QuoteNumber}";
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
@if (soonExpiry)
|
@if (soonExpiry)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning mt-3 mb-0 py-2">
|
<div class="alert alert-warning alert-permanent mt-3 mb-0 py-2">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-1"></i>
|
<i class="bi bi-exclamation-triangle-fill me-1"></i>
|
||||||
This quote expires in @((int)daysUntilExpiry!.Value) day(s). Please respond promptly.
|
This quote expires in @((int)daysUntilExpiry!.Value) day(s). Please respond promptly.
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
<!-- Decline error alert -->
|
<!-- Decline error alert -->
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.DeclineError))
|
@if (!string.IsNullOrWhiteSpace(Model.DeclineError))
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger alert-permanent">
|
||||||
<i class="bi bi-exclamation-circle me-1"></i>@Model.DeclineError
|
<i class="bi bi-exclamation-circle me-1"></i>@Model.DeclineError
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
|
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
|
||||||
@{
|
@{
|
||||||
Layout = "_QuoteApprovalLayout";
|
Layout = "_QuoteApprovalLayout";
|
||||||
ViewData["Title"] = "One Last Step";
|
ViewData["Title"] = "One Last Step";
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.DeclineError))
|
@if (!string.IsNullOrEmpty(Model.DeclineError))
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">@Model.DeclineError</div>
|
<div class="alert alert-danger alert-permanent">@Model.DeclineError</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Quote.ConvertQuoteToCustomerDto
|
@model PowderCoating.Application.DTOs.Quote.ConvertQuoteToCustomerDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Convert Prospect/Walk-In to Customer";
|
ViewData["Title"] = "Convert Prospect/Walk-In to Customer";
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<p><strong>Phone:</strong> @(Model.Phone ?? "-")</p>
|
<p><strong>Phone:</strong> @(Model.Phone ?? "-")</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-warning mb-0">
|
<div class="alert alert-warning alert-permanent mb-0">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong>Note:</strong> Converting this prospect will create a new customer record
|
<strong>Note:</strong> Converting this prospect will create a new customer record
|
||||||
and link this quote to that customer. The quote status will change to "Converted".
|
and link this quote to that customer. The quote status will change to "Converted".
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Quote.CreateQuoteDto
|
@model PowderCoating.Application.DTOs.Quote.CreateQuoteDto
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Customer vs Prospect/Walk-In"
|
data-bs-title="Customer vs Prospect/Walk-In"
|
||||||
data-bs-content="Choose <strong>Existing Customer</strong> if this person is already in your system. Choose <strong>New Prospect/Walk-In</strong> if they haven't committed yet — their details stay on the quote. When they approve, you can convert them to a full customer record with one click.<br><br><a href='/Help/Quotes#prospect-conversion' target='_blank'>Learn more →</a>">
|
data-bs-content="Choose <strong>Existing Customer</strong> if this person is already in your system. Choose <strong>New Prospect/Walk-In</strong> if they haven't committed yet — their details stay on the quote. When they approve, you can convert them to a full customer record with one click.<br><br><a href='/Help/Quotes#prospect-conversion' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Quote Information"
|
data-bs-title="Quote Information"
|
||||||
data-bs-content="Set the quote date, expiration, and any internal notes. The <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
data-bs-content="Set the quote date, expiration, and any internal notes. The <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Oven & Batch Pricing"
|
data-bs-title="Oven & Batch Pricing"
|
||||||
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
|
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Quote Item Types"
|
data-bs-title="Quote Item Types"
|
||||||
data-bs-content="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more →</a>">
|
data-bs-content="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
|
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
|
||||||
<label class="form-check-label small" for="hideDiscountFromCustomer">
|
<label class="form-check-label small" for="hideDiscountFromCustomer">
|
||||||
Hide discount from customer — PDFs and approval portal show final price only
|
Hide discount from customer — PDFs and approval portal show final price only
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
<a tabindex="0" class="help-icon text-white" role="button"
|
<a tabindex="0" class="help-icon text-white" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="left"
|
data-bs-toggle="popover" data-bs-placement="left"
|
||||||
data-bs-title="Pricing Summary"
|
data-bs-title="Pricing Summary"
|
||||||
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more →</a>">
|
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
|
<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="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
|
||||||
<p class="mb-1 d-none" id="ovenBatchCostRow">
|
<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):
|
<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>
|
<strong id="ovenBatchCostDisplay">$0.00</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
<div class="row g-3" id="stagedPhotoGrid"></div>
|
<div class="row g-3" id="stagedPhotoGrid"></div>
|
||||||
<div id="stagedPhotoUploadProgress" class="d-none mt-2">
|
<div id="stagedPhotoUploadProgress" class="d-none mt-2">
|
||||||
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
|
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
|
||||||
<small class="text-muted">Uploading…</small>
|
<small class="text-muted">Uploading…</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
|
<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>
|
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="cylinderInputs" style="display:none">
|
<div id="cylinderInputs" style="display:none">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@@ -461,7 +461,7 @@
|
|||||||
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
@@ -681,7 +681,7 @@
|
|||||||
<script src="~/lib/tom-select/js/tom-select.complete.min.js"></script>
|
<script src="~/lib/tom-select/js/tom-select.complete.min.js"></script>
|
||||||
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
|
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Quick / Full quote mode toggle ──────────────────────────────────
|
// ── Quick / Full quote mode toggle ──────────────────────────────────
|
||||||
(function () {
|
(function () {
|
||||||
const STORAGE_KEY = 'pcl_quote_mode';
|
const STORAGE_KEY = 'pcl_quote_mode';
|
||||||
const form = document.getElementById('quoteForm');
|
const form = document.getElementById('quoteForm');
|
||||||
@@ -690,7 +690,7 @@
|
|||||||
function applyMode(mode) {
|
function applyMode(mode) {
|
||||||
if (mode === 'simple') {
|
if (mode === 'simple') {
|
||||||
form.classList.add('quote-simple-mode');
|
form.classList.add('quote-simple-mode');
|
||||||
hint.textContent = 'Advanced fields are hidden — switch to Full Quote to see them.';
|
hint.textContent = 'Advanced fields are hidden — switch to Full Quote to see them.';
|
||||||
} else {
|
} else {
|
||||||
form.classList.remove('quote-simple-mode');
|
form.classList.remove('quote-simple-mode');
|
||||||
hint.textContent = '';
|
hint.textContent = '';
|
||||||
@@ -758,8 +758,8 @@
|
|||||||
smsNote.style.display = 'inline';
|
smsNote.style.display = 'inline';
|
||||||
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
|
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
|
||||||
smsNote.innerHTML = hasSms
|
smsNote.innerHTML = hasSms
|
||||||
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
|
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
|
||||||
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
|
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
|
||||||
} else {
|
} else {
|
||||||
smsNote.style.display = 'none';
|
smsNote.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
|
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Quote Information"
|
data-bs-title="Quote Information"
|
||||||
data-bs-content="Set the quote date, expiration, and any internal notes. The <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
data-bs-content="Set the quote date, expiration, and any internal notes. The <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Oven & Batch Pricing"
|
data-bs-title="Oven & Batch Pricing"
|
||||||
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
|
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
<a tabindex="0" class="help-icon" role="button"
|
<a tabindex="0" class="help-icon" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="right"
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
data-bs-title="Quote Item Types"
|
data-bs-title="Quote Item Types"
|
||||||
data-bs-content="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more →</a>">
|
data-bs-content="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
|
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
|
||||||
<label class="form-check-label small" for="hideDiscountFromCustomer">
|
<label class="form-check-label small" for="hideDiscountFromCustomer">
|
||||||
Hide discount from customer — PDFs and approval portal show final price only
|
Hide discount from customer — PDFs and approval portal show final price only
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
<a tabindex="0" class="help-icon text-white" role="button"
|
<a tabindex="0" class="help-icon text-white" role="button"
|
||||||
data-bs-toggle="popover" data-bs-placement="left"
|
data-bs-toggle="popover" data-bs-placement="left"
|
||||||
data-bs-title="Pricing Summary"
|
data-bs-title="Pricing Summary"
|
||||||
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more →</a>">
|
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more →</a>">
|
||||||
<i class="bi bi-question-circle"></i>
|
<i class="bi bi-question-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -303,7 +303,7 @@
|
|||||||
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
|
<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="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
|
||||||
<p class="mb-1 d-none" id="ovenBatchCostRow">
|
<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):
|
<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>
|
<strong id="ovenBatchCostDisplay">$0.00</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
|
||||||
@@ -407,7 +407,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="editPhotoUploadProgress" class="d-none mt-2">
|
<div id="editPhotoUploadProgress" class="d-none mt-2">
|
||||||
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
|
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
|
||||||
<small class="text-muted">Uploading…</small>
|
<small class="text-muted">Uploading…</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -435,11 +435,11 @@
|
|||||||
<span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")">
|
<span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")">
|
||||||
@if (editHasSms)
|
@if (editHasSms)
|
||||||
{
|
{
|
||||||
<i class="bi bi-phone me-1"></i><text>No email — send via SMS from quote details</text>
|
<i class="bi bi-phone me-1"></i><text>No email — send via SMS from quote details</text>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<i class="bi bi-phone-slash me-1"></i><text>No email — SMS consent required</text>
|
<i class="bi bi-phone-slash me-1"></i><text>No email — SMS consent required</text>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -483,7 +483,7 @@
|
|||||||
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
|
<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>
|
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="cylinderInputs" style="display:none">
|
<div id="cylinderInputs" style="display:none">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@@ -498,7 +498,7 @@
|
|||||||
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
@@ -579,7 +579,7 @@
|
|||||||
@Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>()))
|
@Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>()))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Existing items — always populated on Edit -->
|
<!-- Existing items — always populated on Edit -->
|
||||||
<script id="existingItemsData" type="application/json">
|
<script id="existingItemsData" type="application/json">
|
||||||
@Html.Raw(System.Text.Json.JsonSerializer.Serialize((Model.QuoteItems ?? new List<PowderCoating.Application.DTOs.Quote.CreateQuoteItemDto>()).Select((item, i) => new {
|
@Html.Raw(System.Text.Json.JsonSerializer.Serialize((Model.QuoteItems ?? new List<PowderCoating.Application.DTOs.Quote.CreateQuoteItemDto>()).Select((item, i) => new {
|
||||||
description = item.Description,
|
description = item.Description,
|
||||||
@@ -740,8 +740,8 @@
|
|||||||
smsNote.style.display = 'inline';
|
smsNote.style.display = 'inline';
|
||||||
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
|
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
|
||||||
smsNote.innerHTML = hasSms
|
smsNote.innerHTML = hasSms
|
||||||
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
|
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
|
||||||
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
|
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
|
||||||
} else {
|
} else {
|
||||||
smsNote.style.display = 'none';
|
smsNote.style.display = 'none';
|
||||||
}
|
}
|
||||||
@@ -811,7 +811,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quote photo direct upload (Edit page — quoteId is known)
|
// Quote photo direct upload (Edit page — quoteId is known)
|
||||||
(function () {
|
(function () {
|
||||||
const quoteId = @Model.Id;
|
const quoteId = @Model.Id;
|
||||||
const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")';
|
const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Registration.RegisterCompanyDto
|
@model PowderCoating.Application.DTOs.Registration.RegisterCompanyDto
|
||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
|
|
||||||
@if (!ViewData.ModelState.IsValid && ViewData.ModelState.ErrorCount > 0)
|
@if (!ViewData.ModelState.IsValid && ViewData.ModelState.ErrorCount > 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger mb-3">
|
<div class="alert alert-danger alert-permanent mb-3">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>Please fix the errors below:</strong>
|
<strong>Please fix the errors below:</strong>
|
||||||
<div asp-validation-summary="All" class="mt-1 mb-0"></div>
|
<div asp-validation-summary="All" class="mt-1 mb-0"></div>
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning mt-2" role="alert">
|
<div class="alert alert-warning alert-permanent mt-2" role="alert">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
No subscription plans are currently available. Please contact a system administrator.
|
No subscription plans are currently available. Please contact a system administrator.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Web.Controllers
|
@using PowderCoating.Web.Controllers
|
||||||
@model List<Vendor1099Row>
|
@model List<Vendor1099Row>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card border-0 shadow-sm text-center">
|
<div class="card border-0 shadow-sm text-center">
|
||||||
<div class="card-body py-3">
|
<div class="card-body py-3">
|
||||||
<div class="text-muted small">Need 1099-NEC (≥ $600)</div>
|
<div class="text-muted small">Need 1099-NEC (≥ $600)</div>
|
||||||
<div class="fs-3 fw-bold text-danger">@vendorsOver600</div>
|
<div class="fs-3 fw-bold text-danger">@vendorsOver600</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info py-2 mb-4">
|
<div class="alert alert-info alert-permanent py-2 mb-4">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
This report shows payments to vendors marked as <strong>1099 Vendor</strong> during <strong>@reportYear</strong>.
|
This report shows payments to vendors marked as <strong>1099 Vendor</strong> during <strong>@reportYear</strong>.
|
||||||
IRS rules require a 1099-NEC for non-incorporated contractors, attorneys, and service providers paid <strong>$600 or more</strong> in the calendar year.
|
IRS rules require a 1099-NEC for non-incorporated contractors, attorneys, and service providers paid <strong>$600 or more</strong> in the calendar year.
|
||||||
@@ -81,7 +81,7 @@ else
|
|||||||
{
|
{
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header bg-white border-0 py-3">
|
<div class="card-header bg-white border-0 py-3">
|
||||||
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>1099-NEC Summary — @reportYear</h5>
|
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>1099-NEC Summary — @reportYear</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -117,14 +117,14 @@ else
|
|||||||
<span class="text-danger small"><i class="bi bi-exclamation-triangle me-1"></i>Missing</span>
|
<span class="text-danger small"><i class="bi bi-exclamation-triangle me-1"></i>Missing</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="small">@(row.Address ?? "—")</td>
|
<td class="small">@(row.Address ?? "—")</td>
|
||||||
<td class="text-end">@row.BillsPaid.ToString("C")</td>
|
<td class="text-end">@row.BillsPaid.ToString("C")</td>
|
||||||
<td class="text-end">@row.ExpensesPaid.ToString("C")</td>
|
<td class="text-end">@row.ExpensesPaid.ToString("C")</td>
|
||||||
<td class="text-end fw-bold @(row.NeedsForm ? "text-danger" : "")">@row.TotalPaid.ToString("C")</td>
|
<td class="text-end fw-bold @(row.NeedsForm ? "text-danger" : "")">@row.TotalPaid.ToString("C")</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@if (row.NeedsForm)
|
@if (row.NeedsForm)
|
||||||
{
|
{
|
||||||
<span class="badge bg-danger">Yes — File Required</span>
|
<span class="badge bg-danger">Yes — File Required</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model List<PowderCoating.Core.Entities.Company>
|
@model List<PowderCoating.Core.Entities.Company>
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Seed Data Management";
|
ViewData["Title"] = "Seed Data Management";
|
||||||
ViewData["PageIcon"] = "bi-database-fill-gear";
|
ViewData["PageIcon"] = "bi-database-fill-gear";
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<li><strong>Default Company:</strong> Demo Company (DEMO)</li>
|
<li><strong>Default Company:</strong> Demo Company (DEMO)</li>
|
||||||
<li><strong>SuperAdmin User:</strong> artemis@("@")powdercoatinglogix.com</li>
|
<li><strong>SuperAdmin User:</strong> artemis@("@")powdercoatinglogix.com</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle-fill me-2"></i>
|
<i class="bi bi-info-circle-fill me-2"></i>
|
||||||
<strong>Note:</strong> This operation is idempotent. If data already exists, it will be skipped.
|
<strong>Note:</strong> This operation is idempotent. If data already exists, it will be skipped.
|
||||||
</div>
|
</div>
|
||||||
@@ -125,14 +125,14 @@
|
|||||||
<li><strong>Operating Costs:</strong> Default labor, equipment, and overhead rates</li>
|
<li><strong>Operating Costs:</strong> Default labor, equipment, and overhead rates</li>
|
||||||
<li><strong>Demo Users:</strong> Company Admin and Manager (only for Demo Company)</li>
|
<li><strong>Demo Users:</strong> Company Admin and Manager (only for Demo Company)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="alert alert-info mb-3">
|
<div class="alert alert-info alert-permanent mb-3">
|
||||||
<i class="bi bi-info-circle-fill me-2"></i>
|
<i class="bi bi-info-circle-fill me-2"></i>
|
||||||
<strong>Note:</strong> Select a company below to seed demo data. Existing data will not be overwritten.
|
<strong>Note:</strong> Select a company below to seed demo data. Existing data will not be overwritten.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (Model == null || !Model.Any())
|
@if (Model == null || !Model.Any())
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning alert-permanent">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
No companies found. Please seed system data first to create the default company.
|
No companies found. Please seed system data first to create the default company.
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile card view — shown on screens < 992px -->
|
<!-- Mobile card view — shown on screens < 992px -->
|
||||||
<div class="mobile-card-view d-lg-none">
|
<div class="mobile-card-view d-lg-none">
|
||||||
<div class="mobile-card-list">
|
<div class="mobile-card-list">
|
||||||
@foreach (var company in Model)
|
@foreach (var company in Model)
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning mt-3 mb-0">
|
<div class="alert alert-warning alert-permanent mt-3 mb-0">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<strong>This action permanently deletes records and cannot be undone.</strong>
|
<strong>This action permanently deletes records and cannot be undone.</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.DTOs.Wizard
|
@using PowderCoating.Application.DTOs.Wizard
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Setup Complete!";
|
ViewData["Title"] = "Setup Complete!";
|
||||||
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
|
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>You're all set!</h1>
|
<h1>You're all set!</h1>
|
||||||
<p style="color:rgba(255,255,255,0.8);font-size:1.05rem;max-width:500px;margin:0 auto 1.5rem;">
|
<p style="color:rgba(255,255,255,0.8);font-size:1.05rem;max-width:500px;margin:0 auto 1.5rem;">
|
||||||
Your setup is complete. @progress.DoneSteps.Count of @WizardProgressDto.TotalSteps steps were configured — your shop is ready to roll.
|
Your setup is complete. @progress.DoneSteps.Count of @WizardProgressDto.TotalSteps steps were configured — your shop is ready to roll.
|
||||||
</p>
|
</p>
|
||||||
@if (showGuidedActivationCta)
|
@if (showGuidedActivationCta)
|
||||||
{
|
{
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
|
|
||||||
@if (progress.SkippedSteps.Any(s => !progress.IsStepDone(s)))
|
@if (progress.SkippedSteps.Any(s => !progress.IsStepDone(s)))
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex gap-2 mb-4">
|
<div class="alert alert-warning alert-permanent d-flex gap-2 mb-4">
|
||||||
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
|
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
|
||||||
<div>
|
<div>
|
||||||
You skipped @progress.SkippedSteps.Count(s => !progress.IsStepDone(s)) step(s). You can always re-run the setup wizard from
|
You skipped @progress.SkippedSteps.Count(s => !progress.IsStepDone(s)) step(s). You can always re-run the setup wizard from
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Storage Migration";
|
ViewData["Title"] = "Storage Migration";
|
||||||
ViewData["PageIcon"] = "bi-cloud-upload";
|
ViewData["PageIcon"] = "bi-cloud-upload";
|
||||||
bool mediaExists = ViewBag.MediaExists;
|
bool mediaExists = ViewBag.MediaExists;
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<small class="text-muted">@ViewBag.MediaPath</small>
|
<small class="text-muted">@ViewBag.MediaPath</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info alert-permanent mb-0">
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
These files will be uploaded to Azure Blob Storage. Files already present in Azure will be skipped automatically.
|
These files will be uploaded to Azure Blob Storage. Files already present in Azure will be skipped automatically.
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Mobile card view — shown on screens < 992px -->
|
<!-- Mobile card view — shown on screens < 992px -->
|
||||||
<div class="mobile-card-view d-lg-none p-3">
|
<div class="mobile-card-view d-lg-none p-3">
|
||||||
<div class="mobile-card-list">
|
<div class="mobile-card-list">
|
||||||
<div class="mobile-data-card">
|
<div class="mobile-data-card">
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text text-warning">
|
<div class="form-text text-warning">
|
||||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
Leave unchecked on the first run. Files already in Azure are always skipped — re-running is safe.
|
Leave unchecked on the first run. Files already in Azure are always skipped — re-running is safe.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
<i class="bi bi-cloud-upload me-2"></i>Start Migration
|
<i class="bi bi-cloud-upload me-2"></i>Start Migration
|
||||||
</button>
|
</button>
|
||||||
<span class="ms-3 text-muted d-none" id="migrationSpinner">
|
<span class="ms-3 text-muted d-none" id="migrationSpinner">
|
||||||
<span class="spinner-border spinner-border-sm me-1"></span>Migrating @localFileCount files, please wait…
|
<span class="spinner-border spinner-border-sm me-1"></span>Migrating @localFileCount files, please wait…
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using PowderCoating.Application.Interfaces
|
@using PowderCoating.Application.Interfaces
|
||||||
@model StorageMigrationResult
|
@model StorageMigrationResult
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
@if (Model.HasErrors)
|
@if (Model.HasErrors)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger mb-4">
|
<div class="alert alert-danger alert-permanent mb-4">
|
||||||
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-1"></i>Errors (@Model.Errors.Count)</h6>
|
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-1"></i>Errors (@Model.Errors.Count)</h6>
|
||||||
<ul class="mb-0 small">
|
<ul class="mb-0 small">
|
||||||
@foreach (var error in Model.Errors)
|
@foreach (var error in Model.Errors)
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
@if (Model.Total == 0)
|
@if (Model.Total == 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info alert-permanent">
|
||||||
<i class="bi bi-info-circle me-1"></i>No files were found in the local media directory.
|
<i class="bi bi-info-circle me-1"></i>No files were found in the local media directory.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@using PowderCoating.Core.Entities
|
@using PowderCoating.Core.Entities
|
||||||
@using PowderCoating.Core.Enums
|
@using PowderCoating.Core.Enums
|
||||||
@model Company
|
@model Company
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = $"Manage – {Model.CompanyName}";
|
ViewData["Title"] = $"Manage – {Model.CompanyName}";
|
||||||
var planConfigs = (dynamic)ViewBag.PlanConfigs;
|
var planConfigs = (dynamic)ViewBag.PlanConfigs;
|
||||||
|
|
||||||
string PlanName(int plan)
|
string PlanName(int plan)
|
||||||
@@ -66,9 +66,9 @@
|
|||||||
<dt class="col-7 text-muted">Users</dt>
|
<dt class="col-7 text-muted">Users</dt>
|
||||||
<dd class="col-5 fw-semibold">@ViewBag.UserCount</dd>
|
<dd class="col-5 fw-semibold">@ViewBag.UserCount</dd>
|
||||||
<dt class="col-7 text-muted">Stripe Customer</dt>
|
<dt class="col-7 text-muted">Stripe Customer</dt>
|
||||||
<dd class="col-5"><code class="small">@(Model.StripeCustomerId ?? "—")</code></dd>
|
<dd class="col-5"><code class="small">@(Model.StripeCustomerId ?? "—")</code></dd>
|
||||||
<dt class="col-7 text-muted">Stripe Sub</dt>
|
<dt class="col-7 text-muted">Stripe Sub</dt>
|
||||||
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "—")</code></dd>
|
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "—")</code></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<form method="post" asp-action="UpdateSubscription" asp-route-id="@Model.Id">
|
<form method="post" asp-action="UpdateSubscription" asp-route-id="@Model.Id">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
@* Comped / Internal card — prominent *@
|
@* Comped / Internal card — prominent *@
|
||||||
<div class="card border-0 shadow-sm mb-3 @(Model.IsComped ? "border-success border-2" : "")">
|
<div class="card border-0 shadow-sm mb-3 @(Model.IsComped ? "border-success border-2" : "")">
|
||||||
<div class="card-header border-0 py-3 @(Model.IsComped ? "bg-success bg-opacity-10" : "bg-white")">
|
<div class="card-header border-0 py-3 @(Model.IsComped ? "bg-success bg-opacity-10" : "bg-white")">
|
||||||
<h6 class="mb-0 fw-semibold">
|
<h6 class="mb-0 fw-semibold">
|
||||||
@@ -345,17 +345,17 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-warning small mb-3">
|
<div class="alert alert-warning alert-permanent small mb-3">
|
||||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
This will immediately issue a refund via Stripe. This action cannot be undone.
|
This will immediately issue a refund via Stripe. This action cannot be undone.
|
||||||
</div>
|
</div>
|
||||||
<dl class="row small mb-3">
|
<dl class="row small mb-3">
|
||||||
<dt class="col-5 text-muted">Invoice</dt>
|
<dt class="col-5 text-muted">Invoice</dt>
|
||||||
<dd class="col-7 font-monospace" id="refund-invoice-number">—</dd>
|
<dd class="col-7 font-monospace" id="refund-invoice-number">—</dd>
|
||||||
<dt class="col-5 text-muted">Amount Paid</dt>
|
<dt class="col-5 text-muted">Amount Paid</dt>
|
||||||
<dd class="col-7 fw-semibold" id="refund-amount-paid">—</dd>
|
<dd class="col-7 fw-semibold" id="refund-amount-paid">—</dd>
|
||||||
<dt class="col-5 text-muted">Max Refundable</dt>
|
<dt class="col-5 text-muted">Max Refundable</dt>
|
||||||
<dd class="col-7 fw-semibold text-success" id="refund-max-amount">—</dd>
|
<dd class="col-7 fw-semibold text-success" id="refund-max-amount">—</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-medium">Refund Amount</label>
|
<label class="form-label fw-medium">Refund Amount</label>
|
||||||
@@ -402,7 +402,7 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
// ── State for the refund modal ────────────────────────────────────────────────
|
// ââ€â‚¬Ã¢â€â‚¬ State for the refund modal ââ€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬
|
||||||
let _refundPaymentIntentId = null;
|
let _refundPaymentIntentId = null;
|
||||||
let _refundMaxCents = 0;
|
let _refundMaxCents = 0;
|
||||||
let _refundAmountPaid = '';
|
let _refundAmountPaid = '';
|
||||||
@@ -442,7 +442,7 @@ async function loadPaymentHistory() {
|
|||||||
: '';
|
: '';
|
||||||
const refundedCell = ch.amountRefunded
|
const refundedCell = ch.amountRefunded
|
||||||
? `<span class="text-danger small">${ch.amountRefunded}</span>`
|
? `<span class="text-danger small">${ch.amountRefunded}</span>`
|
||||||
: '<span class="text-muted small">—</span>';
|
: '<span class="text-muted small">—</span>';
|
||||||
const desc = ch.description ? `<span class="text-muted">${ch.description}</span>` : `<code class="small">${ch.id}</code>`;
|
const desc = ch.description ? `<span class="text-muted">${ch.description}</span>` : `<code class="small">${ch.id}</code>`;
|
||||||
|
|
||||||
// Show Refund button only for succeeded charges that still have something refundable
|
// Show Refund button only for succeeded charges that still have something refundable
|
||||||
@@ -469,7 +469,7 @@ async function loadPaymentHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openRefundModal(chargeId, refundableCents, amountPaid, displayLabel) {
|
function openRefundModal(chargeId, refundableCents, amountPaid, displayLabel) {
|
||||||
_refundPaymentIntentId = chargeId; // reusing variable — now holds charge ID
|
_refundPaymentIntentId = chargeId; // reusing variable — now holds charge ID
|
||||||
_refundMaxCents = refundableCents;
|
_refundMaxCents = refundableCents;
|
||||||
_refundAmountPaid = amountPaid;
|
_refundAmountPaid = amountPaid;
|
||||||
_refundInvoiceNumber = displayLabel;
|
_refundInvoiceNumber = displayLabel;
|
||||||
@@ -509,7 +509,7 @@ async function submitRefund() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Processing…';
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Processing…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Core.Entities.TaxRate
|
@model PowderCoating.Core.Entities.TaxRate
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Add Tax Rate";
|
ViewData["Title"] = "Add Tax Rate";
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-action="Create" method="post">
|
<form asp-action="Create" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Core.Entities.TaxRate
|
@model PowderCoating.Core.Entities.TaxRate
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Edit Tax Rate";
|
ViewData["Title"] = "Edit Tax Rate";
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<form asp-action="Edit" asp-route-id="@Model.Id" method="post">
|
<form asp-action="Edit" asp-route-id="@Model.Id" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" asp-for="Id" />
|
<input type="hidden" asp-for="Id" />
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Set Up Two-Factor Authentication";
|
ViewData["Title"] = "Set Up Two-Factor Authentication";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ol class="mb-0 ps-3">
|
<ol class="mb-0 ps-3">
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<strong>Install an authenticator app</strong> on your phone if you haven't already —
|
<strong>Install an authenticator app</strong> on your phone if you haven't already —
|
||||||
<em>Google Authenticator</em>, <em>Microsoft Authenticator</em>, or <em>Authy</em> all work.
|
<em>Google Authenticator</em>, <em>Microsoft Authenticator</em>, or <em>Authy</em> all work.
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
class="border rounded p-1"
|
class="border rounded p-1"
|
||||||
style="width:200px;height:200px" />
|
style="width:200px;height:200px" />
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-light py-2 small">
|
<div class="alert alert-light alert-permanent py-2 small">
|
||||||
<strong>Manual entry key:</strong><br>
|
<strong>Manual entry key:</strong><br>
|
||||||
<code class="user-select-all">@ViewBag.SharedKey</code>
|
<code class="user-select-all">@ViewBag.SharedKey</code>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model PowderCoating.Application.DTOs.Vendor.VendorDto
|
@model PowderCoating.Application.DTOs.Vendor.VendorDto
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Delete Vendor";
|
ViewData["Title"] = "Delete Vendor";
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warning Banner -->
|
<!-- Warning Banner -->
|
||||||
<div class="alert alert-danger d-flex align-items-start mb-4">
|
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="alert-heading mb-2">Are you sure you want to delete this vendor?</h5>
|
<h5 class="alert-heading mb-2">Are you sure you want to delete this vendor?</h5>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
@if (hasInventoryItems)
|
@if (hasInventoryItems)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-start mb-4">
|
<div class="alert alert-warning alert-permanent d-flex align-items-start mb-4">
|
||||||
<i class="bi bi-exclamation-circle-fill me-3" style="font-size: 1.5rem;"></i>
|
<i class="bi bi-exclamation-circle-fill me-3" style="font-size: 1.5rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="alert-heading mb-2">Warning: This vendor has inventory items</h5>
|
<h5 class="alert-heading mb-2">Warning: This vendor has inventory items</h5>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
@if (hasInventoryItems)
|
@if (hasInventoryItems)
|
||||||
{
|
{
|
||||||
<div class="alert alert-info d-flex align-items-center">
|
<div class="alert alert-info alert-permanent d-flex align-items-center">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>Inventory Items:</strong> This vendor has @ViewBag.InventoryItemCount inventory item(s) on record.
|
<strong>Inventory Items:</strong> This vendor has @ViewBag.InventoryItemCount inventory item(s) on record.
|
||||||
|
|||||||
@@ -979,3 +979,45 @@ a.tag-index-badge:hover {
|
|||||||
filter: brightness(0.88);
|
filter: brightness(0.88);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =============================================================
|
||||||
|
Design system utilities — audit fixes 2026-05
|
||||||
|
============================================================= */
|
||||||
|
|
||||||
|
/* ── Card consistency ──────────────────────────────────────────
|
||||||
|
Standard is `card border-0 shadow-sm`. Any shadow card that
|
||||||
|
drifted to just `shadow-sm` gets border stripped here. */
|
||||||
|
.card.shadow-sm {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card header dark-mode fix ─────────────────────────────────
|
||||||
|
bg-white hardcodes light mode. Rebind to theme surface so
|
||||||
|
card headers follow the user's dark/light preference. */
|
||||||
|
.card-header.bg-white {
|
||||||
|
background-color: var(--bs-body-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Typography scale utilities ────────────────────────────────
|
||||||
|
Fills the gap between Bootstrap's .small and the custom
|
||||||
|
.nav-section-title / .pcl-metric-label sizes. */
|
||||||
|
.text-2xs { font-size: 0.68rem; line-height: 1.4; }
|
||||||
|
.text-xs { font-size: 0.73rem; line-height: 1.4; }
|
||||||
|
|
||||||
|
/* ── Token color utilities ─────────────────────────────────────
|
||||||
|
Use .text-ember for the amber brand accent.
|
||||||
|
Use .text-primary only for near-black ink (Bootstrap default).
|
||||||
|
Never use text-primary when you want the orange accent. */
|
||||||
|
.text-ember { color: var(--pcl-ember) !important; }
|
||||||
|
.text-ok { color: var(--pcl-ok) !important; }
|
||||||
|
.text-bad { color: var(--pcl-bad) !important; }
|
||||||
|
.text-warn { color: var(--pcl-warn) !important; }
|
||||||
|
.text-cool { color: var(--pcl-cool) !important; }
|
||||||
|
.bg-paper-2 { background-color: var(--pcl-paper-2) !important; }
|
||||||
|
|
||||||
|
/* ── Max-width layout constraints ──────────────────────────────
|
||||||
|
Use these instead of inline style="max-width:Npx" */
|
||||||
|
.mw-xs { max-width: 280px; }
|
||||||
|
.mw-sm { max-width: 360px; }
|
||||||
|
.mw-md { max-width: 480px; }
|
||||||
|
.mw-lg { max-width: 640px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user