Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Reports/SalesAndIncome.cshtml
T
spouliot 4ec55e7290 Restore all zeroed views + add bulk gift certificate creation
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).

Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
  per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
  Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:09:22 -04:00

344 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@model PowderCoating.Application.DTOs.Accounting.SalesIncomeReportDto
@{
ViewData["Title"] = "Sales & Income";
ViewData["PageIcon"] = "bi-currency-dollar";
var today = DateTime.Today;
var ytdFrom = new DateTime(today.Year, 1, 1).ToString("yyyy-MM-dd");
var ytdTo = today.ToString("yyyy-MM-dd");
var lastYrFrom = new DateTime(today.Year - 1, 1, 1).ToString("yyyy-MM-dd");
var lastYrTo = new DateTime(today.Year - 1, 12, 31).ToString("yyyy-MM-dd");
var thisMonthFrom = new DateTime(today.Year, today.Month, 1).ToString("yyyy-MM-dd");
var thisMonthTo = today.ToString("yyyy-MM-dd");
var lastMonthFrom = new DateTime(today.Year, today.Month, 1).AddMonths(-1).ToString("yyyy-MM-dd");
var lastMonthTo = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd");
var monthLabels = Model.ByMonth.Select(m => m.Label).ToList();
var monthInvoiced = Model.ByMonth.Select(m => m.TotalInvoiced).ToList();
var monthCollected = Model.ByMonth.Select(m => m.TotalCollected).ToList();
}
<style>
@@media print {
.no-print { display: none !important; }
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
body { font-size: 11px; }
}
</style>
<!-- Header -->
<div class="d-flex align-items-center gap-2 mb-3 no-print">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<p class="text-muted mb-0">@Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy") · @Model.InvoiceCount invoices · @Model.CustomerCount customers</p>
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("SalesAndIncomePdf", new { from = Model.From.ToString("yyyy-MM-dd"), to = Model.To.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-danger no-print" target="_blank">
<i class="bi bi-file-pdf me-1"></i>Download PDF
</a>
<a href="@Url.Action("SalesAndIncomePdf", new { from = Model.From.ToString("yyyy-MM-dd"), to = Model.To.ToString("yyyy-MM-dd"), inline = true })"
class="btn btn-sm btn-outline-secondary no-print" target="_blank">
<i class="bi bi-printer me-1"></i>Print
</a>
</div>
</div>
<!-- Date filter -->
<div class="card shadow-sm mb-4 no-print">
<div class="card-body py-3">
<form method="get" class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label form-label-sm mb-1">From</label>
<input type="date" name="from" class="form-control form-control-sm" value="@Model.From.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<label class="form-label form-label-sm mb-1">To</label>
<input type="date" name="to" class="form-control form-control-sm" value="@Model.To.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel me-1"></i>Run Report</button>
</div>
<div class="col-auto ms-2">
<div class="btn-group btn-group-sm">
<a href="@Url.Action("SalesAndIncome", new { from = thisMonthFrom, to = thisMonthTo })" class="btn btn-outline-secondary">This Month</a>
<a href="@Url.Action("SalesAndIncome", new { from = lastMonthFrom, to = lastMonthTo })" class="btn btn-outline-secondary">Last Month</a>
<a href="@Url.Action("SalesAndIncome", new { from = ytdFrom, to = ytdTo })" class="btn btn-outline-secondary">YTD</a>
<a href="@Url.Action("SalesAndIncome", new { from = lastYrFrom, to = lastYrTo })" class="btn btn-outline-secondary">Last Year</a>
</div>
</div>
</form>
</div>
</div>
<!-- Print header -->
<div class="text-center mb-4 d-none d-print-block">
<h4 class="fw-bold">@Model.CompanyName</h4>
<h5>Sales &amp; Income Report</h5>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") @Model.To.ToString("MMMM d, yyyy")</p>
</div>
<!-- KPI Cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h5 fw-bold mb-1">@Model.TotalInvoiced.ToString("C")</div>
<div class="text-muted small">Total Invoiced</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h5 fw-bold text-success mb-1">@Model.TotalCollected.ToString("C")</div>
<div class="text-muted small">Collected (period)</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h5 fw-bold mb-1">@Model.AverageInvoiceValue.ToString("C")</div>
<div class="text-muted small">Avg Invoice Value</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h5 fw-bold text-primary mb-1">@Model.CustomerCount</div>
<div class="text-muted small">Active Customers</div>
</div>
</div>
</div>
</div>
@if (!Model.Invoices.Any())
{
<div class="card shadow-sm">
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-receipt fs-1 d-block mb-2"></i>
<p class="mb-0">No invoices found for this period.</p>
</div>
</div>
}
else
{
<div class="row g-4 mb-4">
<!-- Monthly trend chart -->
@if (Model.ByMonth.Count > 1)
{
<div class="col-lg-8 no-print">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">
<i class="bi bi-bar-chart me-1"></i>Monthly Sales Trend
</div>
<div class="card-body">
<canvas id="salesTrendChart" height="120"></canvas>
</div>
</div>
</div>
}
<!-- By month table -->
<div class="col-lg-@(Model.ByMonth.Count > 1 ? "4" : "12")">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold"><i class="bi bi-calendar3 me-1"></i>By Month</div>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Month</th>
<th class="text-end">Invoiced</th>
<th class="text-end">Collected</th>
<th class="text-center">#</th>
</tr>
</thead>
<tbody>
@foreach (var m in Model.ByMonth)
{
<tr>
<td>@m.Label</td>
<td class="text-end">@m.TotalInvoiced.ToString("C")</td>
<td class="text-end text-success">@m.TotalCollected.ToString("C")</td>
<td class="text-center text-muted small">@m.InvoiceCount</td>
</tr>
}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td>Total</td>
<td class="text-end">@Model.TotalInvoiced.ToString("C")</td>
<td class="text-end text-success">@Model.TotalCollected.ToString("C")</td>
<td class="text-center">@Model.InvoiceCount</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<!-- By Customer -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold"><i class="bi bi-people me-1"></i>Sales by Customer</div>
<div class="table-responsive">
<table class="table table-hover table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>Customer</th>
<th class="text-center">Invoices</th>
<th class="text-end">Total Invoiced</th>
<th class="text-end">Paid</th>
<th class="text-end">Balance Due</th>
<th class="text-end no-print">% of Sales</th>
</tr>
</thead>
<tbody>
@foreach (var c in Model.ByCustomer)
{
<tr>
<td>
<a asp-controller="Customers" asp-action="Details" asp-route-id="@c.CustomerId" class="text-decoration-none fw-medium">
@c.CustomerName
</a>
</td>
<td class="text-center text-muted small">@c.InvoiceCount</td>
<td class="text-end fw-semibold">@c.TotalInvoiced.ToString("C")</td>
<td class="text-end text-success">@c.TotalPaid.ToString("C")</td>
<td class="text-end @(c.BalanceDue > 0 ? "text-warning" : "text-muted")">@c.BalanceDue.ToString("C")</td>
<td class="text-end text-muted small no-print">@(Model.TotalInvoiced == 0 ? "—" : (c.TotalInvoiced / Model.TotalInvoiced * 100).ToString("F1") + "%")</td>
</tr>
}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td>Total (@Model.CustomerCount customers)</td>
<td class="text-center">@Model.InvoiceCount</td>
<td class="text-end">@Model.TotalInvoiced.ToString("C")</td>
<td class="text-end text-success">@Model.TotalCollected.ToString("C")</td>
<td class="text-end">@Model.Invoices.Sum(i => i.BalanceDue).ToString("C")</td>
<td class="no-print"></td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Invoice detail -->
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-semibold"><i class="bi bi-receipt me-1"></i>Invoice Detail</span>
<span class="badge bg-secondary">@Model.InvoiceCount invoices</span>
</div>
<div class="table-responsive">
<table class="table table-hover table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>Invoice</th>
<th>Customer</th>
<th>Date</th>
<th>Due</th>
<th class="text-end">Subtotal</th>
<th class="text-end">Tax</th>
<th class="text-end">Total</th>
<th class="text-end">Paid</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach (var inv in Model.Invoices)
{
string statusBadge = inv.Status switch
{
"Paid" => "bg-success-subtle text-success",
"PartiallyPaid" => "bg-warning-subtle text-warning",
"Sent" => "bg-info-subtle text-info",
"Overdue" => "bg-danger-subtle text-danger",
_ => "bg-secondary-subtle text-secondary"
};
<tr>
<td>
<a asp-controller="Invoices" asp-action="Details" asp-route-id="@inv.InvoiceId" class="text-decoration-none fw-medium">
@inv.InvoiceNumber
</a>
</td>
<td class="text-muted small">@inv.CustomerName</td>
<td class="text-muted small">@inv.InvoiceDate.ToString("MM/dd/yyyy")</td>
<td class="text-muted small">@(inv.DueDate?.ToString("MM/dd/yyyy") ?? "—")</td>
<td class="text-end">@inv.SubTotal.ToString("C")</td>
<td class="text-end text-muted small">@(inv.TaxAmount > 0 ? inv.TaxAmount.ToString("C") : "—")</td>
<td class="text-end fw-semibold">@inv.Total.ToString("C")</td>
<td class="text-end text-success">@inv.AmountPaid.ToString("C")</td>
<td><span class="badge @statusBadge">@inv.Status</span></td>
</tr>
}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td colspan="4">Totals</td>
<td class="text-end">@Model.Invoices.Sum(i => i.SubTotal).ToString("C")</td>
<td class="text-end text-muted">@Model.TotalTax.ToString("C")</td>
<td class="text-end">@Model.TotalInvoiced.ToString("C")</td>
<td class="text-end text-success">@Model.Invoices.Sum(i => i.AmountPaid).ToString("C")</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
}
<div class="text-muted small mt-2 no-print">
<i class="bi bi-info-circle me-1"></i>
Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Excludes Draft and Voided invoices. "Collected" reflects payments received within the period, regardless of invoice date.
</div>
@if (Model.ByMonth.Count > 1)
{
@section Scripts {
<script src="~/lib/chartjs/chart.umd.min.js"></script>
<script>
(function() {
const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark';
const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)';
const textColor = isDark ? '#adb5bd' : '#6c757d';
new Chart(document.getElementById('salesTrendChart'), {
type: 'bar',
data: {
labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(monthLabels)),
datasets: [
{
label: 'Invoiced',
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(monthInvoiced)),
backgroundColor: 'rgba(79,70,229,0.7)',
borderRadius: 4,
order: 2
},
{
label: 'Collected',
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(monthCollected)),
type: 'line',
borderColor: '#10b981',
backgroundColor: 'rgba(16,185,129,0.1)',
borderWidth: 2,
pointRadius: 4,
fill: false,
tension: 0.3,
order: 1
}
]
},
options: {
responsive: true,
plugins: { legend: { display: true }, tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.y.toLocaleString('en-US', {minimumFractionDigits:2}) } } },
scales: {
y: { ticks: { color: textColor, callback: v => '$' + v.toLocaleString() }, grid: { color: gridColor } },
x: { ticks: { color: textColor }, grid: { display: false } }
}
}
});
})();
</script>
}
}