Restore all zeroed views + add bulk gift certificate creation
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).
Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
@{
|
||||
ViewData["Title"] = "Anomaly Detection";
|
||||
ViewData["PageIcon"] = "bi-shield-exclamation";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a asp-controller="Reports" asp-action="Landing" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</a>
|
||||
<p class="text-muted mb-0 small">AI scans your recent bills and expense trends for duplicates, unusual amounts, and accounts running over budget.</p>
|
||||
</div>
|
||||
<button id="btnRunAnalysis" class="btn btn-warning text-dark">
|
||||
<i class="bi bi-magic me-1"></i>Run Analysis
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Idle state -->
|
||||
<div id="idleState" class="text-center py-5">
|
||||
<div class="mb-3" style="font-size:3rem;color:var(--bs-border-color);">
|
||||
<i class="bi bi-shield-exclamation"></i>
|
||||
</div>
|
||||
<h5 class="text-muted">No analysis run yet</h5>
|
||||
<p class="text-muted small mb-3">Click <strong>Run Analysis</strong> to scan your bills and expense accounts for anomalies.</p>
|
||||
<button class="btn btn-warning btn-sm text-dark" onclick="runAnalysis()">
|
||||
<i class="bi bi-magic me-1"></i>Run Analysis
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div id="loadingState" class="text-center py-5 d-none">
|
||||
<div class="spinner-border text-warning mb-3" role="status"></div>
|
||||
<p class="text-muted">Scanning bills and expense accounts for anomalies…<br><small>This usually takes 5–10 seconds.</small></p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div id="errorState" class="d-none">
|
||||
<div class="alert alert-danger alert-permanent d-flex align-items-center gap-2">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
<span id="errorMessage">An error occurred.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="analysisResults" class="d-none">
|
||||
|
||||
<!-- Summary badges -->
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<div id="badgeCritical" class="d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#fef2f2;">
|
||||
<i class="bi bi-exclamation-octagon-fill text-danger fs-5"></i>
|
||||
<div>
|
||||
<span id="cntCritical" class="fw-bold fs-5 text-danger">0</span>
|
||||
<span class="text-danger small ms-1">Critical</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#fffbeb;">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning fs-5"></i>
|
||||
<div>
|
||||
<span id="cntWarning" class="fw-bold fs-5 text-warning">0</span>
|
||||
<span class="text-warning small ms-1">Warnings</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#eff6ff;">
|
||||
<i class="bi bi-info-circle-fill text-info fs-5"></i>
|
||||
<div>
|
||||
<span id="cntInfo" class="fw-bold fs-5 text-info">0</span>
|
||||
<span class="text-info small ms-1">Info</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="allClearBadge" class="d-none d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#f0fdf4;">
|
||||
<i class="bi bi-shield-check text-success fs-5"></i>
|
||||
<span class="text-success fw-semibold">All Clear — no anomalies detected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flags list -->
|
||||
<div id="flagsList"></div>
|
||||
|
||||
<div class="text-muted small text-end mt-3">
|
||||
<i class="bi bi-robot me-1"></i>Generated by AI · <span id="analysisTimestamp"></span>
|
||||
· <a href="#" onclick="runAnalysis(); return false;">Re-run</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
document.getElementById('btnRunAnalysis').addEventListener('click', runAnalysis);
|
||||
|
||||
const typeLabels = {
|
||||
duplicate: { label: 'Duplicate Bill', icon: 'bi-files', color: 'danger' },
|
||||
amount_spike: { label: 'Amount Spike', icon: 'bi-graph-up-arrow', color: 'warning' },
|
||||
unusual_vendor: { label: 'Unusual Vendor', icon: 'bi-person-exclamation', color: 'warning' },
|
||||
account_overrun: { label: 'Account Over Budget', icon: 'bi-bar-chart-fill', color: 'info' }
|
||||
};
|
||||
|
||||
const severityConfig = {
|
||||
critical: { cls: 'border-danger', headerCls: 'bg-danger text-white', badgeCls: 'bg-danger' },
|
||||
warning: { cls: 'border-warning', headerCls: 'bg-warning text-dark', badgeCls: 'bg-warning text-dark' },
|
||||
info: { cls: 'border-info', headerCls: 'bg-info text-white', badgeCls: 'bg-info' }
|
||||
};
|
||||
|
||||
async function runAnalysis() {
|
||||
document.getElementById('idleState').classList.add('d-none');
|
||||
document.getElementById('loadingState').classList.remove('d-none');
|
||||
document.getElementById('errorState').classList.add('d-none');
|
||||
document.getElementById('analysisResults').classList.add('d-none');
|
||||
|
||||
try {
|
||||
const resp = await fetch('/Reports/RunAnomalyDetection', {
|
||||
method: 'POST',
|
||||
headers: { 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '' }
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
document.getElementById('loadingState').classList.add('d-none');
|
||||
|
||||
if (!data.success) {
|
||||
document.getElementById('errorMessage').textContent = data.errorMessage || 'An error occurred.';
|
||||
document.getElementById('errorState').classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('cntCritical').textContent = data.criticalCount;
|
||||
document.getElementById('cntWarning').textContent = data.warningCount;
|
||||
document.getElementById('cntInfo').textContent = data.infoCount;
|
||||
|
||||
const totalFlags = (data.flags || []).length;
|
||||
document.getElementById('allClearBadge').classList.toggle('d-none', totalFlags > 0);
|
||||
document.getElementById('badgeCritical').parentElement.querySelectorAll(':scope > div:not(#allClearBadge)')
|
||||
.forEach(el => el.classList.toggle('d-none', totalFlags === 0));
|
||||
|
||||
const container = document.getElementById('flagsList');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Sort: critical first, then warning, then info
|
||||
const sorted = [...(data.flags || [])].sort((a, b) => {
|
||||
const order = { critical: 0, warning: 1, info: 2 };
|
||||
return (order[a.severity] ?? 3) - (order[b.severity] ?? 3);
|
||||
});
|
||||
|
||||
sorted.forEach(flag => {
|
||||
const typeInfo = typeLabels[flag.type] || { label: flag.type, icon: 'bi-exclamation-circle', color: 'secondary' };
|
||||
const sev = severityConfig[flag.severity] || severityConfig.warning;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = `card mb-3 border ${sev.cls}`;
|
||||
card.innerHTML = `
|
||||
<div class="card-header d-flex align-items-center gap-2 ${sev.headerCls} py-2">
|
||||
<i class="bi ${typeInfo.icon}"></i>
|
||||
<span class="fw-semibold">${flag.title}</span>
|
||||
<span class="badge ${sev.badgeCls} ms-auto text-uppercase" style="font-size:0.65rem;">${flag.severity}</span>
|
||||
<span class="badge bg-light text-dark" style="font-size:0.65rem;">${typeInfo.label}</span>
|
||||
${flag.billNumber ? `<span class="badge bg-secondary ms-1" style="font-size:0.65rem;">${flag.billNumber}</span>` : ''}
|
||||
</div>
|
||||
<div class="card-body py-2">
|
||||
<p class="mb-1">${flag.description}</p>
|
||||
${flag.recommendedAction ? `
|
||||
<div class="d-flex align-items-start gap-2 mt-2 p-2 rounded" style="background:var(--bs-tertiary-bg);">
|
||||
<i class="bi bi-arrow-right-circle-fill text-primary mt-1 flex-shrink-0"></i>
|
||||
<span class="small"><strong>Recommended:</strong> ${flag.recommendedAction}</span>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
document.getElementById('analysisTimestamp').textContent = new Date().toLocaleString();
|
||||
document.getElementById('analysisResults').classList.remove('d-none');
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('loadingState').classList.add('d-none');
|
||||
document.getElementById('errorMessage').textContent = 'Network error — please try again.';
|
||||
document.getElementById('errorState').classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@Html.AntiForgeryToken()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.ApAgingReportDto
|
||||
@{
|
||||
ViewData["Title"] = "AP Aging";
|
||||
ViewData["PageIcon"] = "bi-hourglass-split";
|
||||
var today = DateTime.Today;
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
|
||||
body { font-size: 11px; }
|
||||
.table { font-size: 11px; }
|
||||
}
|
||||
.aging-current { color: #198754; }
|
||||
.aging-1-30 { color: #fd7e14; }
|
||||
.aging-31-60 { color: #dc6c02; }
|
||||
.aging-61-90 { color: #dc3545; }
|
||||
.aging-over90 { color: #842029; font-weight: 700; }
|
||||
.vendor-row { background: #f8f9fa; font-weight: 600; }
|
||||
</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">As of @Model.AsOf.ToString("MMMM d, yyyy") · @Model.Vendors.Sum(v => v.Bills.Count) open bills</p>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a href="@Url.Action("ApAgingPdf", new { asOf = Model.AsOf.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("ApAgingPdf", new { asOf = Model.AsOf.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">As of Date</label>
|
||||
<input type="date" name="asOf" class="form-control form-control-sm" value="@Model.AsOf.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("ApAging", new { asOf = today.ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Today</a>
|
||||
<a href="@Url.Action("ApAging", new { asOf = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">End of Last Month</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>Accounts Payable Aging</h5>
|
||||
<p class="text-muted">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
|
||||
</div>
|
||||
|
||||
<!-- Aging summary cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 text-success mb-1">@Model.TotalCurrent.ToString("C0")</div>
|
||||
<div class="text-muted small">Current</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-1-30 mb-1">@Model.Total1to30.ToString("C0")</div>
|
||||
<div class="text-muted small">1–30 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-31-60 mb-1">@Model.Total31to60.ToString("C0")</div>
|
||||
<div class="text-muted small">31–60 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-61-90 mb-1">@Model.Total61to90.ToString("C0")</div>
|
||||
<div class="text-muted small">61–90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-over90 mb-1">@Model.TotalOver90.ToString("C0")</div>
|
||||
<div class="text-muted small">Over 90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100 border-danger border-opacity-25">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 text-danger fw-bold mb-1">@Model.TotalOutstanding.ToString("C0")</div>
|
||||
<div class="text-muted small">Total Owed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!Model.Vendors.Any())
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-check-circle text-success fs-1 d-block mb-2"></i>
|
||||
<p class="mb-0 fw-semibold">All bills are paid!</p>
|
||||
<p class="small mb-0">No outstanding balances as of @Model.AsOf.ToString("MMMM d, yyyy").</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Summary table -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-table me-1"></i>Aging Summary by Vendor
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Vendor</th>
|
||||
<th class="text-end">Current</th>
|
||||
<th class="text-end">1–30 Days</th>
|
||||
<th class="text-end">31–60 Days</th>
|
||||
<th class="text-end">61–90 Days</th>
|
||||
<th class="text-end">Over 90</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var vend in Model.Vendors)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-controller="Vendors" asp-action="Details" asp-route-id="@vend.VendorId" class="text-decoration-none fw-medium">
|
||||
@vend.VendorName
|
||||
</a>
|
||||
<span class="badge bg-secondary ms-1">@vend.Bills.Count bill@(vend.Bills.Count == 1 ? "" : "s")</span>
|
||||
</td>
|
||||
<td class="text-end aging-current">@(vend.TotalCurrent > 0 ? vend.TotalCurrent.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-1-30">@(vend.Total1to30 > 0 ? vend.Total1to30.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-31-60">@(vend.Total31to60 > 0 ? vend.Total31to60.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-61-90">@(vend.Total61to90 > 0 ? vend.Total61to90.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-over90">@(vend.TotalOver90 > 0 ? vend.TotalOver90.ToString("C") : "—")</td>
|
||||
<td class="text-end fw-semibold">@vend.TotalBalance.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-bold">
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td class="text-end aging-current">@Model.TotalCurrent.ToString("C")</td>
|
||||
<td class="text-end aging-1-30">@Model.Total1to30.ToString("C")</td>
|
||||
<td class="text-end aging-31-60">@Model.Total31to60.ToString("C")</td>
|
||||
<td class="text-end aging-61-90">@Model.Total61to90.ToString("C")</td>
|
||||
<td class="text-end aging-over90">@Model.TotalOver90.ToString("C")</td>
|
||||
<td class="text-end">@Model.TotalOutstanding.ToString("C")</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail by vendor -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-list-ul me-1"></i>Bill Detail
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Bill #</th>
|
||||
<th>Bill Date</th>
|
||||
<th>Due Date</th>
|
||||
<th class="text-end">Balance Due</th>
|
||||
<th class="text-end">Age</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var vend in Model.Vendors)
|
||||
{
|
||||
<tr class="vendor-row">
|
||||
<td colspan="6" class="py-2">@vend.VendorName</td>
|
||||
</tr>
|
||||
@foreach (var bill in vend.Bills.OrderBy(b => b.DaysOverdue))
|
||||
{
|
||||
string ageBadge = bill.DaysOverdue <= 0 ? "bg-success-subtle text-success"
|
||||
: bill.DaysOverdue <= 30 ? "bg-warning-subtle text-warning"
|
||||
: bill.DaysOverdue <= 60 ? "bg-orange-subtle text-warning"
|
||||
: bill.DaysOverdue <= 90 ? "bg-danger-subtle text-danger"
|
||||
: "bg-danger text-white";
|
||||
string ageLabel = bill.DaysOverdue <= 0 ? "Current" : $"{bill.DaysOverdue}d overdue";
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<a asp-controller="Bills" asp-action="Details" asp-route-id="@bill.BillId" class="text-decoration-none fw-medium">
|
||||
@bill.BillNumber
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-muted small">@bill.BillDate.ToString("MM/dd/yyyy")</td>
|
||||
<td class="text-muted small">@(bill.DueDate?.ToString("MM/dd/yyyy") ?? "—")</td>
|
||||
<td class="text-end fw-semibold @(bill.DaysOverdue > 30 ? "text-danger" : "")">@bill.BalanceDue.ToString("C")</td>
|
||||
<td class="text-end"><span class="badge @ageBadge">@ageLabel</span></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="table-light">
|
||||
<td colspan="3" class="ps-4 fw-semibold text-end small">@vend.VendorName subtotal</td>
|
||||
<td class="text-end fw-semibold">@vend.TotalBalance.ToString("C")</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</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") · Includes all open bills (excluding Draft and Voided). Age calculated from due date.
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.ArAgingReportDto
|
||||
@{
|
||||
ViewData["Title"] = "AR Aging";
|
||||
ViewData["PageIcon"] = "bi-hourglass-split";
|
||||
var today = DateTime.Today;
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
|
||||
body { font-size: 11px; }
|
||||
.table { font-size: 11px; }
|
||||
}
|
||||
.aging-current { color: #198754; }
|
||||
.aging-1-30 { color: #fd7e14; }
|
||||
.aging-31-60 { color: #dc6c02; }
|
||||
.aging-61-90 { color: #dc3545; }
|
||||
.aging-over90 { color: #842029; font-weight: 700; }
|
||||
.customer-row { background: #f8f9fa; font-weight: 600; }
|
||||
</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">As of @Model.AsOf.ToString("MMMM d, yyyy") · @Model.Customers.Sum(c => c.Invoices.Count) open invoices</p>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a href="@Url.Action("ArAgingPdf", new { asOf = Model.AsOf.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("ArAgingPdf", new { asOf = Model.AsOf.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">As of Date</label>
|
||||
<input type="date" name="asOf" class="form-control form-control-sm" value="@Model.AsOf.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("ArAging", new { asOf = today.ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Today</a>
|
||||
<a href="@Url.Action("ArAging", new { asOf = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">End of Last Month</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>Accounts Receivable Aging</h5>
|
||||
<p class="text-muted">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
|
||||
</div>
|
||||
|
||||
<!-- Aging summary cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 text-success mb-1">@Model.TotalCurrent.ToString("C0")</div>
|
||||
<div class="text-muted small">Current</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-1-30 mb-1">@Model.Total1to30.ToString("C0")</div>
|
||||
<div class="text-muted small">1–30 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-31-60 mb-1">@Model.Total31to60.ToString("C0")</div>
|
||||
<div class="text-muted small">31–60 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-61-90 mb-1">@Model.Total61to90.ToString("C0")</div>
|
||||
<div class="text-muted small">61–90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 aging-over90 mb-1">@Model.TotalOver90.ToString("C0")</div>
|
||||
<div class="text-muted small">Over 90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="card shadow-sm text-center h-100 border-primary border-opacity-25">
|
||||
<div class="card-body py-3">
|
||||
<div class="h6 text-primary fw-bold mb-1">@Model.TotalOutstanding.ToString("C0")</div>
|
||||
<div class="text-muted small">Total Outstanding</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!Model.Customers.Any())
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-check-circle text-success fs-1 d-block mb-2"></i>
|
||||
<p class="mb-0 fw-semibold">All invoices are paid!</p>
|
||||
<p class="small mb-0">No outstanding balances as of @Model.AsOf.ToString("MMMM d, yyyy").</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Summary table -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-table me-1"></i>Aging Summary 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-end">Current</th>
|
||||
<th class="text-end">1–30 Days</th>
|
||||
<th class="text-end">31–60 Days</th>
|
||||
<th class="text-end">61–90 Days</th>
|
||||
<th class="text-end">Over 90</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var cust in Model.Customers)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-controller="Customers" asp-action="Details" asp-route-id="@cust.CustomerId" class="text-decoration-none fw-medium">
|
||||
@cust.CustomerName
|
||||
</a>
|
||||
<span class="badge bg-secondary ms-1">@cust.Invoices.Count inv.</span>
|
||||
</td>
|
||||
<td class="text-end aging-current">@(cust.TotalCurrent > 0 ? cust.TotalCurrent.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-1-30">@(cust.Total1to30 > 0 ? cust.Total1to30.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-31-60">@(cust.Total31to60 > 0 ? cust.Total31to60.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-61-90">@(cust.Total61to90 > 0 ? cust.Total61to90.ToString("C") : "—")</td>
|
||||
<td class="text-end aging-over90">@(cust.TotalOver90 > 0 ? cust.TotalOver90.ToString("C") : "—")</td>
|
||||
<td class="text-end fw-semibold">@cust.TotalBalance.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-bold">
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td class="text-end aging-current">@Model.TotalCurrent.ToString("C")</td>
|
||||
<td class="text-end aging-1-30">@Model.Total1to30.ToString("C")</td>
|
||||
<td class="text-end aging-31-60">@Model.Total31to60.ToString("C")</td>
|
||||
<td class="text-end aging-61-90">@Model.Total61to90.ToString("C")</td>
|
||||
<td class="text-end aging-over90">@Model.TotalOver90.ToString("C")</td>
|
||||
<td class="text-end">@Model.TotalOutstanding.ToString("C")</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail by customer -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-list-ul me-1"></i>Invoice Detail
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Invoice</th>
|
||||
<th>Date</th>
|
||||
<th>Due Date</th>
|
||||
<th class="text-end">Balance Due</th>
|
||||
<th class="text-end">Age</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var cust in Model.Customers)
|
||||
{
|
||||
<tr class="customer-row">
|
||||
<td colspan="6" class="py-2">@cust.CustomerName</td>
|
||||
</tr>
|
||||
@foreach (var inv in cust.Invoices.OrderBy(i => i.DaysOverdue))
|
||||
{
|
||||
string ageBadge = inv.DaysOverdue <= 0 ? "bg-success-subtle text-success"
|
||||
: inv.DaysOverdue <= 30 ? "bg-warning-subtle text-warning"
|
||||
: inv.DaysOverdue <= 60 ? "bg-orange-subtle text-warning"
|
||||
: inv.DaysOverdue <= 90 ? "bg-danger-subtle text-danger"
|
||||
: "bg-danger text-white";
|
||||
string ageLabel = inv.DaysOverdue <= 0 ? "Current" : $"{inv.DaysOverdue}d overdue";
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<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.InvoiceDate.ToString("MM/dd/yyyy")</td>
|
||||
<td class="text-muted small">@(inv.DueDate?.ToString("MM/dd/yyyy") ?? "—")</td>
|
||||
<td class="text-end fw-semibold @(inv.DaysOverdue > 30 ? "text-danger" : "")">@inv.BalanceDue.ToString("C")</td>
|
||||
<td class="text-end"><span class="badge @ageBadge">@ageLabel</span></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="table-light">
|
||||
<td colspan="3" class="ps-4 fw-semibold text-end small">@cust.CustomerName subtotal</td>
|
||||
<td class="text-end fw-semibold">@cust.TotalBalance.ToString("C")</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</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") · Includes all open invoices (excluding Draft and Voided). Age calculated from due date.
|
||||
</div>
|
||||
|
||||
@if (Model.Customers.Any())
|
||||
{
|
||||
<!-- AI Late Payment Prediction -->
|
||||
<div class="card shadow-sm mt-4 border-0 no-print" id="aiRiskCard">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<i class="bi bi-robot text-primary"></i>
|
||||
<span class="fw-semibold">AI Payment Risk Prediction</span>
|
||||
<button id="aiRiskBtn" class="btn btn-sm btn-outline-primary ms-auto">
|
||||
<i class="bi bi-magic me-1"></i>Predict Payment Risk
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body d-none" id="aiRiskBody">
|
||||
<div id="aiRiskSpinner" class="text-center py-3 d-none">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="text-muted mt-2 small">Claude is analyzing payment behavior…</p>
|
||||
</div>
|
||||
<div id="aiRiskError" class="alert alert-danger alert-permanent d-none"></div>
|
||||
<div id="aiRiskInsights" class="text-muted small mb-3"></div>
|
||||
<div id="aiRiskTable" class="table-responsive d-none">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Risk</th>
|
||||
<th>Est. Days to Payment</th>
|
||||
<th>Reasoning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="aiRiskRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script src="/js/ar-aging-ai.js"></script>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.BalanceSheetDto
|
||||
@{
|
||||
ViewData["Title"] = "Balance Sheet";
|
||||
ViewData["PageIcon"] = "bi-scale";
|
||||
var today = DateTime.Today;
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
|
||||
body { font-size: 12px; }
|
||||
}
|
||||
.report-section-header { background: #f8f9fa; font-weight: 600; }
|
||||
.report-subtotal-row { border-top: 1px solid #dee2e6; font-weight: 600; }
|
||||
.report-total-row { border-top: 2px solid #343a40; font-weight: 700; background: #f1f3f5; }
|
||||
.bs-balanced { color: #198754; }
|
||||
.bs-unbalanced { color: #dc3545; }
|
||||
</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">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
|
||||
@if (Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Cash)
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Cash Basis</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-info text-dark">Accrual Basis</span>
|
||||
}
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a href="@Url.Action("BalanceSheetPdf", new { asOf = Model.AsOf.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("BalanceSheetPdf", new { asOf = Model.AsOf.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">As of Date</label>
|
||||
<input type="date" name="asOf" class="form-control form-control-sm" value="@Model.AsOf.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("BalanceSheet", new { asOf = today.ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Today</a>
|
||||
<a href="@Url.Action("BalanceSheet", new { asOf = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">End of Last Month</a>
|
||||
<a href="@Url.Action("BalanceSheet", new { asOf = new DateTime(today.Year, 12, 31).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Year End</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>Balance Sheet</h5>
|
||||
<p class="text-muted">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
|
||||
</div>
|
||||
|
||||
<!-- KPI row -->
|
||||
<div class="row g-3 mb-4 no-print">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm text-center h-100 border-primary border-opacity-25">
|
||||
<div class="card-body py-3">
|
||||
<div class="h5 text-primary fw-bold mb-1">@Model.TotalAssets.ToString("C")</div>
|
||||
<div class="text-muted small">Total Assets</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm text-center h-100 border-danger border-opacity-25">
|
||||
<div class="card-body py-3">
|
||||
<div class="h5 text-danger fw-bold mb-1">@Model.TotalLiabilities.ToString("C")</div>
|
||||
<div class="text-muted small">Total Liabilities</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm text-center h-100 border-success border-opacity-25">
|
||||
<div class="card-body py-3">
|
||||
<div class="h5 text-success fw-bold mb-1">@Model.TotalEquity.ToString("C")</div>
|
||||
<div class="text-muted small">Total Equity</div>
|
||||
@if (!Model.IsBalanced)
|
||||
{
|
||||
<small class="text-danger"><i class="bi bi-exclamation-triangle me-1"></i>Sheet does not balance</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- ASSETS column -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-safe text-primary me-2"></i>Assets
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<tbody>
|
||||
@{
|
||||
void RenderSection(string title, List<PowderCoating.Application.DTOs.Accounting.FinancialReportLine> lines, string subtotalLabel)
|
||||
{
|
||||
if (!lines.Any()) return;
|
||||
<tr class="report-section-header">
|
||||
<td colspan="2" class="py-2 small text-uppercase text-muted">@title</td>
|
||||
</tr>
|
||||
foreach (var line in lines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
|
||||
<td class="text-end">@line.Amount.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="report-subtotal-row">
|
||||
<td class="ps-3 fw-semibold">@subtotalLabel</td>
|
||||
<td class="text-end fw-semibold">@lines.Sum(l => l.Amount).ToString("C")</td>
|
||||
</tr>
|
||||
<tr><td colspan="2" class="py-1"></td></tr>
|
||||
}
|
||||
}
|
||||
@{ RenderSection("Current Assets", Model.CurrentAssets, "Total Current Assets"); }
|
||||
@{ RenderSection("Fixed Assets", Model.FixedAssets, "Total Fixed Assets"); }
|
||||
@{ RenderSection("Other Assets", Model.OtherAssets, "Total Other Assets"); }
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="report-total-row">
|
||||
<td>Total Assets</td>
|
||||
<td class="text-end text-primary">@Model.TotalAssets.ToString("C")</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIABILITIES + EQUITY column -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Liabilities -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-credit-card text-danger me-2"></i>Liabilities
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<tbody>
|
||||
@{
|
||||
void RenderLiabSection(string title, List<PowderCoating.Application.DTOs.Accounting.FinancialReportLine> lines, string subtotalLabel)
|
||||
{
|
||||
if (!lines.Any()) return;
|
||||
<tr class="report-section-header">
|
||||
<td colspan="2" class="py-2 small text-uppercase text-muted">@title</td>
|
||||
</tr>
|
||||
foreach (var line in lines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
|
||||
<td class="text-end">@line.Amount.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="report-subtotal-row">
|
||||
<td class="ps-3 fw-semibold">@subtotalLabel</td>
|
||||
<td class="text-end fw-semibold">@lines.Sum(l => l.Amount).ToString("C")</td>
|
||||
</tr>
|
||||
<tr><td colspan="2" class="py-1"></td></tr>
|
||||
}
|
||||
}
|
||||
@{ RenderLiabSection("Current Liabilities", Model.CurrentLiabilities, "Total Current Liabilities"); }
|
||||
@{ RenderLiabSection("Long-Term Liabilities", Model.LongTermLiabilities, "Total Long-Term Liabilities"); }
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="report-total-row">
|
||||
<td>Total Liabilities</td>
|
||||
<td class="text-end text-danger">@Model.TotalLiabilities.ToString("C")</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equity -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-bar-chart-line text-success me-2"></i>Equity
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<tbody>
|
||||
@if (Model.EquityLines.Any())
|
||||
{
|
||||
<tr class="report-section-header">
|
||||
<td colspan="2" class="py-2 small text-uppercase text-muted">Owner's Equity</td>
|
||||
</tr>
|
||||
@foreach (var line in Model.EquityLines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
|
||||
<td class="text-end">@line.Amount.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
<tr class="@(Model.RetainedEarnings < 0 ? "text-danger" : "")">
|
||||
<td class="ps-3"><span class="text-muted">Retained Earnings (Net Income)</span></td>
|
||||
<td class="text-end">@Model.RetainedEarnings.ToString("C")</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="report-total-row">
|
||||
<td>Total Equity</td>
|
||||
<td class="text-end text-success">@Model.TotalEquity.ToString("C")</td>
|
||||
</tr>
|
||||
<tr class="report-total-row" style="border-top:3px double #343a40;">
|
||||
<td>Total Liabilities & Equity</td>
|
||||
<td class="text-end @(Model.IsBalanced ? "bs-balanced" : "bs-unbalanced")">
|
||||
@Model.TotalLiabilitiesAndEquity.ToString("C")
|
||||
@if (!Model.IsBalanced)
|
||||
{
|
||||
<i class="bi bi-exclamation-triangle ms-1" title="Sheet does not balance — check account setup"></i>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mt-3 no-print">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Balances include opening balances plus all recorded transactions through @Model.AsOf.ToString("MMM d, yyyy").
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
@using PowderCoating.Web.Controllers
|
||||
@using PowderCoating.Core.Enums
|
||||
@model List<BudgetVsActualRow>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Budget vs. Actual";
|
||||
ViewData["PageIcon"] = "bi-bar-chart-line";
|
||||
var reportYear = (int)ViewBag.ReportYear;
|
||||
var budget = ViewBag.Budget as PowderCoating.Core.Entities.Budget;
|
||||
var allBudgets = ViewBag.AllBudgets as List<PowderCoating.Core.Entities.Budget> ?? new();
|
||||
var noBudget = ViewBag.NoBudget == true;
|
||||
var availYears = ViewBag.AvailableYears as List<int> ?? new();
|
||||
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<form method="get" asp-action="BudgetVsActual" class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<select name="year" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
|
||||
@foreach (var y in availYears)
|
||||
{
|
||||
<option value="@y" selected="@(y == reportYear ? "selected" : null)">@y</option>
|
||||
}
|
||||
</select>
|
||||
@if (allBudgets.Count > 1)
|
||||
{
|
||||
<select name="budgetId" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
|
||||
@foreach (var b in allBudgets)
|
||||
{
|
||||
<option value="@b.Id" selected="@(budget?.Id == b.Id ? "selected" : null)">
|
||||
@b.Name@(b.IsDefault ? " (default)" : "")
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</form>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-controller="Budgets" asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-pie-chart me-1"></i>Manage Budgets
|
||||
</a>
|
||||
<a asp-action="Landing" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Reports
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (noBudget || budget == null)
|
||||
{
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center text-muted py-5">
|
||||
<i class="bi bi-bar-chart-line display-4 d-block mb-3 opacity-25"></i>
|
||||
<p>No budget found for <strong>@reportYear</strong>.</p>
|
||||
<a asp-controller="Budgets" asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Create Budget for @reportYear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
var revRows = Model.Where(r => r.AccountType == AccountType.Revenue).ToList();
|
||||
var expRows = Model.Where(r => r.AccountType != AccountType.Revenue).ToList();
|
||||
var totalBudgetRev = revRows.Sum(r => r.BudgetAnnual);
|
||||
var totalActualRev = revRows.Sum(r => r.ActualAnnual);
|
||||
var totalBudgetExp = expRows.Sum(r => r.BudgetAnnual);
|
||||
var totalActualExp = expRows.Sum(r => r.ActualAnnual);
|
||||
var budgetNetIncome = totalBudgetRev - totalBudgetExp;
|
||||
var actualNetIncome = totalActualRev - totalActualExp;
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Budget Revenue</div>
|
||||
<div class="fs-4 fw-bold text-success">@totalBudgetRev.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Actual Revenue</div>
|
||||
<div class="fs-4 fw-bold @(totalActualRev >= totalBudgetRev ? "text-success" : "text-warning")">@totalActualRev.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Budget Net Income</div>
|
||||
<div class="fs-4 fw-bold">@budgetNetIncome.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Actual Net Income</div>
|
||||
<div class="fs-4 fw-bold @(actualNetIncome >= budgetNetIncome ? "text-success" : "text-danger")">@actualNetIncome.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-bar-chart-line me-2 text-primary"></i>@budget.Name — @reportYear
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="min-width:200px">Account</th>
|
||||
<th class="text-end">Budget Annual</th>
|
||||
<th class="text-end">Actual Annual</th>
|
||||
<th class="text-end">Variance</th>
|
||||
@foreach (var m in months)
|
||||
{
|
||||
<th class="text-end small" style="min-width:60px">@m</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (revRows.Any())
|
||||
{
|
||||
<tr class="table-success">
|
||||
<td colspan="@(4 + 12)" class="fw-bold py-1 ps-3 small">REVENUE</td>
|
||||
</tr>
|
||||
@foreach (var row in revRows)
|
||||
{
|
||||
var varA = row.VarianceAnnual;
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-semibold">@row.AccountNumber</span>
|
||||
<span class="text-muted ms-1">@row.AccountName</span>
|
||||
</td>
|
||||
<td class="text-end">@row.BudgetAnnual.ToString("C")</td>
|
||||
<td class="text-end">@row.ActualAnnual.ToString("C")</td>
|
||||
<td class="text-end fw-semibold @(varA >= 0 ? "text-success" : "text-danger")">
|
||||
@(varA >= 0 ? "+" : "")@varA.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
var varM = row.ActualMonths[m] - row.BudgetMonths[m];
|
||||
<td class="text-end small @(varM < 0 ? "text-danger" : "")">
|
||||
@row.ActualMonths[m].ToString("N0")
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<tr class="fw-bold table-success">
|
||||
<td>Total Revenue</td>
|
||||
<td class="text-end">@totalBudgetRev.ToString("C")</td>
|
||||
<td class="text-end">@totalActualRev.ToString("C")</td>
|
||||
<td class="text-end @((totalActualRev - totalBudgetRev) >= 0 ? "text-success" : "text-danger")">
|
||||
@{var revVar = totalActualRev - totalBudgetRev;}
|
||||
@(revVar >= 0 ? "+" : "")@revVar.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
<td class="text-end small">@revRows.Sum(r => r.ActualMonths[m]).ToString("N0")</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
@if (expRows.Any())
|
||||
{
|
||||
<tr class="table-danger">
|
||||
<td colspan="@(4 + 12)" class="fw-bold py-1 ps-3 small">EXPENSE</td>
|
||||
</tr>
|
||||
@foreach (var row in expRows)
|
||||
{
|
||||
var varA = row.BudgetAnnual - row.ActualAnnual; // favorable if under budget
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-semibold">@row.AccountNumber</span>
|
||||
<span class="text-muted ms-1">@row.AccountName</span>
|
||||
</td>
|
||||
<td class="text-end">@row.BudgetAnnual.ToString("C")</td>
|
||||
<td class="text-end">@row.ActualAnnual.ToString("C")</td>
|
||||
<td class="text-end fw-semibold @(varA >= 0 ? "text-success" : "text-danger")">
|
||||
@(varA >= 0 ? "+" : "")@varA.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
var varM = row.BudgetMonths[m] - row.ActualMonths[m];
|
||||
<td class="text-end small @(varM < 0 ? "text-danger" : "")">
|
||||
@row.ActualMonths[m].ToString("N0")
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<tr class="fw-bold table-danger">
|
||||
<td>Total Expenses</td>
|
||||
<td class="text-end">@totalBudgetExp.ToString("C")</td>
|
||||
<td class="text-end">@totalActualExp.ToString("C")</td>
|
||||
<td class="text-end @((totalBudgetExp - totalActualExp) >= 0 ? "text-success" : "text-danger")">
|
||||
@{var expVar = totalBudgetExp - totalActualExp;}
|
||||
@(expVar >= 0 ? "+" : "")@expVar.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
<td class="text-end small">@expRows.Sum(r => r.ActualMonths[m]).ToString("N0")</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<!-- Net Income row -->
|
||||
<tr class="fw-bold border-top" style="border-top-width:2px!important">
|
||||
<td>Net Income</td>
|
||||
<td class="text-end">@budgetNetIncome.ToString("C")</td>
|
||||
<td class="text-end @(actualNetIncome >= 0 ? "text-success" : "text-danger")">@actualNetIncome.ToString("C")</td>
|
||||
<td class="text-end @((actualNetIncome - budgetNetIncome) >= 0 ? "text-success" : "text-danger")">
|
||||
@{var netVar = actualNetIncome - budgetNetIncome;}
|
||||
@(netVar >= 0 ? "+" : "")@netVar.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
var mActualNet = revRows.Sum(r => r.ActualMonths[m]) - expRows.Sum(r => r.ActualMonths[m]);
|
||||
<td class="text-end small @(mActualNet < 0 ? "text-danger" : "")">@mActualNet.ToString("N0")</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-muted small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Variance is shown as <strong>favorable</strong> (positive) when revenue exceeds budget or expenses are under budget.
|
||||
Actual figures reflect the P&L for each calendar month.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
@{
|
||||
ViewData["Title"] = "Cash Flow Forecast";
|
||||
ViewData["PageIcon"] = "bi-cash-stack";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a asp-controller="Reports" asp-action="Landing" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</a>
|
||||
<p class="text-muted mb-0 small">AI-projected 30/60/90-day cash position based on open invoices, outstanding bills, and your job pipeline.</p>
|
||||
</div>
|
||||
<button id="btnRunForecast" class="btn btn-success">
|
||||
<i class="bi bi-magic me-1"></i>Generate Forecast
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Idle state -->
|
||||
<div id="idleState" class="text-center py-5">
|
||||
<div class="mb-3" style="font-size:3rem;color:var(--bs-border-color);">
|
||||
<i class="bi bi-cash-stack"></i>
|
||||
</div>
|
||||
<h5 class="text-muted">No forecast generated yet</h5>
|
||||
<p class="text-muted small mb-3">Click <strong>Generate Forecast</strong> to analyse your open invoices, outstanding bills, and active job pipeline.</p>
|
||||
<button class="btn btn-success btn-sm" onclick="runForecast()">
|
||||
<i class="bi bi-magic me-1"></i>Generate Forecast
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div id="loadingState" class="text-center py-5 d-none">
|
||||
<div class="spinner-border text-success mb-3" role="status"></div>
|
||||
<p class="text-muted">Analysing your AR, AP, and job pipeline…<br><small>This usually takes 5–10 seconds.</small></p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div id="errorState" class="d-none">
|
||||
<div class="alert alert-danger alert-permanent d-flex align-items-center gap-2">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
<span id="errorMessage">An error occurred.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="forecastResults" class="d-none">
|
||||
|
||||
<!-- Outlook banner -->
|
||||
<div id="outlookBanner" class="alert alert-permanent d-flex align-items-center gap-3 mb-4">
|
||||
<div id="outlookIcon" style="font-size:1.8rem;"></div>
|
||||
<div>
|
||||
<div id="outlookTitle" class="fw-bold fs-5"></div>
|
||||
<div id="outlookSub" class="small"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Period cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
||||
<i class="bi bi-calendar-event text-primary"></i> Next 30 Days
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center g-2 mb-3">
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Inflows</div>
|
||||
<div id="in30" class="fw-bold text-success fs-6"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Outflows</div>
|
||||
<div id="out30" class="fw-bold text-danger fs-6"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Net</div>
|
||||
<div id="net30" class="fw-bold fs-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="items30" class="list-unstyled mb-0" style="font-size:0.8rem;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
||||
<i class="bi bi-calendar2-event text-primary"></i> Next 60 Days
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center g-2 mb-3">
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Inflows</div>
|
||||
<div id="in60" class="fw-bold text-success fs-6"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Outflows</div>
|
||||
<div id="out60" class="fw-bold text-danger fs-6"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Net</div>
|
||||
<div id="net60" class="fw-bold fs-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="items60" class="list-unstyled mb-0" style="font-size:0.8rem;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
||||
<i class="bi bi-calendar3-event text-primary"></i> Next 90 Days
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center g-2 mb-3">
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Inflows</div>
|
||||
<div id="in90" class="fw-bold text-success fs-6"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Outflows</div>
|
||||
<div id="out90" class="fw-bold text-danger fs-6"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Net</div>
|
||||
<div id="net90" class="fw-bold fs-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="items90" class="list-unstyled mb-0" style="font-size:0.8rem;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Insights -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
||||
<i class="bi bi-lightbulb text-warning"></i> AI Insights
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul id="insightsList" class="mb-0" style="line-height:1.8;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small text-end">
|
||||
<i class="bi bi-robot me-1"></i>Generated by AI · <span id="forecastTimestamp"></span>
|
||||
· <a href="#" onclick="runForecast(); return false;">Refresh</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
document.getElementById('btnRunForecast').addEventListener('click', runForecast);
|
||||
|
||||
function fmt(n) {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n);
|
||||
}
|
||||
|
||||
function setNetColor(el, val) {
|
||||
el.textContent = fmt(val);
|
||||
el.className = 'fw-bold fs-6 ' + (val >= 0 ? 'text-success' : 'text-danger');
|
||||
}
|
||||
|
||||
function fillPeriod(suffix, period) {
|
||||
document.getElementById('in' + suffix).textContent = fmt(period.expectedInflows);
|
||||
document.getElementById('out' + suffix).textContent = fmt(period.expectedOutflows);
|
||||
setNetColor(document.getElementById('net' + suffix), period.netCashFlow);
|
||||
const ul = document.getElementById('items' + suffix);
|
||||
ul.innerHTML = '';
|
||||
(period.keyItems || []).forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'text-muted mb-1';
|
||||
li.innerHTML = '<i class="bi bi-dot"></i>' + item;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function runForecast() {
|
||||
document.getElementById('idleState').classList.add('d-none');
|
||||
document.getElementById('loadingState').classList.remove('d-none');
|
||||
document.getElementById('errorState').classList.add('d-none');
|
||||
document.getElementById('forecastResults').classList.add('d-none');
|
||||
|
||||
try {
|
||||
const resp = await fetch('/Reports/GenerateCashFlowForecast', {
|
||||
method: 'POST',
|
||||
headers: { 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '' }
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
document.getElementById('loadingState').classList.add('d-none');
|
||||
|
||||
if (!data.success) {
|
||||
document.getElementById('errorMessage').textContent = data.errorMessage || 'An error occurred.';
|
||||
document.getElementById('errorState').classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Outlook banner
|
||||
const outlookMap = {
|
||||
strong: { cls: 'alert-success', icon: '💪', title: 'Strong Cash Position', sub: 'Your projected cash flow looks healthy across all three periods.' },
|
||||
moderate: { cls: 'alert-info', icon: '👍', title: 'Moderate Cash Position', sub: 'Cash flow looks manageable — a few items to watch.' },
|
||||
tight: { cls: 'alert-warning', icon: '⚠️', title: 'Tight Cash Position', sub: 'Cash flow may be constrained — consider following up on open invoices.' },
|
||||
concerning: { cls: 'alert-danger', icon: '🚨', title: 'Concerning Cash Position', sub: 'Projected cash flow is under pressure — immediate action may be needed.' }
|
||||
};
|
||||
const outlook = outlookMap[data.outlook] || outlookMap.moderate;
|
||||
const banner = document.getElementById('outlookBanner');
|
||||
banner.className = 'alert alert-permanent d-flex align-items-center gap-3 mb-4 ' + outlook.cls;
|
||||
document.getElementById('outlookIcon').textContent = outlook.icon;
|
||||
document.getElementById('outlookTitle').textContent = outlook.title;
|
||||
document.getElementById('outlookSub').textContent = outlook.sub;
|
||||
|
||||
fillPeriod('30', data.next30Days);
|
||||
fillPeriod('60', data.next60Days);
|
||||
fillPeriod('90', data.next90Days);
|
||||
|
||||
const ul = document.getElementById('insightsList');
|
||||
ul.innerHTML = '';
|
||||
(data.insights || []).forEach(ins => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'mb-2';
|
||||
li.innerHTML = '<i class="bi bi-check-circle-fill text-success me-2"></i>' + ins;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
document.getElementById('forecastTimestamp').textContent = new Date().toLocaleString();
|
||||
document.getElementById('forecastResults').classList.remove('d-none');
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('loadingState').classList.add('d-none');
|
||||
document.getElementById('errorMessage').textContent = 'Network error — please try again.';
|
||||
document.getElementById('errorState').classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@Html.AntiForgeryToken()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.CashFlowStatementDto
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
ViewData["Title"] = "Cash Flow Statement";
|
||||
|
||||
string AmountClass(decimal v) => v < 0 ? "text-danger" : "text-body";
|
||||
string Fmt(decimal v) => v.ToString("C");
|
||||
}
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-water me-2 text-info"></i>Cash Flow Statement</h4>
|
||||
<p class="text-muted small mb-0">
|
||||
@Model.From.ToString("MMMM d, yyyy") – @Model.To.ToString("MMMM d, yyyy")
|
||||
· Direct Method (Cash Basis)
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-action="CashFlowStatementPdf"
|
||||
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
|
||||
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-download me-1"></i>PDF
|
||||
</a>
|
||||
<a asp-action="CashFlowStatementPdf"
|
||||
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
|
||||
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
|
||||
asp-route-inline="true"
|
||||
target="_blank"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-eye me-1"></i>Preview
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date range filter -->
|
||||
<form method="get" asp-action="CashFlowStatement" class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label fw-semibold small">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 fw-semibold small">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-sm btn-primary">Update</button>
|
||||
</div>
|
||||
<!-- Quick date presets -->
|
||||
@{
|
||||
var y = DateTime.Today.Year;
|
||||
var presets = new[]
|
||||
{
|
||||
("YTD", new DateTime(y, 1, 1).ToString("yyyy-MM-dd"), DateTime.Today.ToString("yyyy-MM-dd")),
|
||||
("This Qtr", new DateTime(y, ((DateTime.Today.Month - 1) / 3) * 3 + 1, 1).ToString("yyyy-MM-dd"), DateTime.Today.ToString("yyyy-MM-dd")),
|
||||
("Last Year", new DateTime(y-1, 1, 1).ToString("yyyy-MM-dd"), new DateTime(y-1, 12, 31).ToString("yyyy-MM-dd")),
|
||||
};
|
||||
}
|
||||
@foreach (var (label, f, t) in presets)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a asp-action="CashFlowStatement" asp-route-from="@f" asp-route-to="@t"
|
||||
class="btn btn-sm btn-outline-secondary">@label</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Main statement -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<!-- Operating Activities -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold"><i class="bi bi-gear me-2 text-info"></i>Operating Activities</span>
|
||||
<span class="badge @(Model.NetOperating >= 0 ? "bg-success" : "bg-danger")">@Fmt(Model.NetOperating)</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-3 text-body-secondary">Cash received from customers</td>
|
||||
<td class="text-end pe-3 text-success fw-semibold">@Fmt(Model.CashFromCustomers)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 text-body-secondary">Cash paid to vendors (bills)</td>
|
||||
<td class="text-end pe-3 @AmountClass(-Model.CashToVendors)">(@Fmt(Model.CashToVendors))</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 text-body-secondary">Cash paid for direct expenses</td>
|
||||
<td class="text-end pe-3 @AmountClass(-Model.CashForExpenses)">(@Fmt(Model.CashForExpenses))</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<td class="ps-3 fw-semibold">Net Cash from Operating Activities</td>
|
||||
<td class="text-end pe-3 fw-bold @AmountClass(Model.NetOperating)">@Fmt(Model.NetOperating)</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Investing Activities -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold"><i class="bi bi-building me-2 text-primary"></i>Investing Activities</span>
|
||||
<span class="badge @(Model.NetInvesting >= 0 ? "bg-success" : "bg-danger")">@Fmt(Model.NetInvesting)</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@if (!Model.InvestingLines.Any())
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3 text-muted" colspan="2">
|
||||
<i class="bi bi-dash-circle me-1"></i>No investing activities recorded in this period.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var line in Model.InvestingLines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3 text-body-secondary">@line.Label</td>
|
||||
<td class="text-end pe-3 @AmountClass(line.Amount)">@Fmt(line.Amount)</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<td class="ps-3 fw-semibold">Net Cash from Investing Activities</td>
|
||||
<td class="text-end pe-3 fw-bold @AmountClass(Model.NetInvesting)">@Fmt(Model.NetInvesting)</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Financing Activities -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold"><i class="bi bi-bank me-2 text-secondary"></i>Financing Activities</span>
|
||||
<span class="badge @(Model.NetFinancing >= 0 ? "bg-success" : "bg-danger")">@Fmt(Model.NetFinancing)</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@if (!Model.FinancingLines.Any())
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3 text-muted" colspan="2">
|
||||
<i class="bi bi-dash-circle me-1"></i>No financing activities recorded in this period.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var line in Model.FinancingLines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3 text-body-secondary">@line.Label</td>
|
||||
<td class="text-end pe-3 @AmountClass(line.Amount)">@Fmt(line.Amount)</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<td class="ps-3 fw-semibold">Net Cash from Financing Activities</td>
|
||||
<td class="text-end pe-3 fw-bold @AmountClass(Model.NetFinancing)">@Fmt(Model.NetFinancing)</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header fw-semibold"><i class="bi bi-calculator me-2 text-info"></i>Cash Summary</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8 text-muted small fw-normal">Beginning Cash</dt>
|
||||
<dd class="col-4 text-end fw-semibold mb-2">@Fmt(Model.BeginningCash)</dd>
|
||||
|
||||
<dt class="col-8 text-muted small fw-normal">Operating</dt>
|
||||
<dd class="col-4 text-end fw-semibold mb-1 @AmountClass(Model.NetOperating)">@Fmt(Model.NetOperating)</dd>
|
||||
|
||||
<dt class="col-8 text-muted small fw-normal">Investing</dt>
|
||||
<dd class="col-4 text-end fw-semibold mb-1 @AmountClass(Model.NetInvesting)">@Fmt(Model.NetInvesting)</dd>
|
||||
|
||||
<dt class="col-8 text-muted small fw-normal">Financing</dt>
|
||||
<dd class="col-4 text-end fw-semibold mb-2 @AmountClass(Model.NetFinancing)">@Fmt(Model.NetFinancing)</dd>
|
||||
|
||||
<dt class="col-8 text-muted small fw-normal">Net Change in Cash</dt>
|
||||
<dd class="col-4 text-end fw-semibold mb-3 @AmountClass(Model.NetChangeInCash)">@Fmt(Model.NetChangeInCash)</dd>
|
||||
|
||||
<dt class="col-8 fw-bold">Ending Cash Balance</dt>
|
||||
<dd class="col-4 text-end fw-bold fs-5 @AmountClass(Model.EndingCash)">@Fmt(Model.EndingCash)</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold small"><i class="bi bi-info-circle me-2"></i>Methodology</div>
|
||||
<div class="card-body small text-muted">
|
||||
<p class="mb-2">This statement uses the <strong>direct (cash basis)</strong> method for Operating Activities:</p>
|
||||
<ul class="mb-2 ps-3">
|
||||
<li>Inflows = customer invoice payments received</li>
|
||||
<li>Outflows = vendor bill payments + direct expense payments</li>
|
||||
</ul>
|
||||
<p class="mb-0">Beginning Cash is approximated from all cash inflows and outflows recorded prior to the start date plus account opening balances. For the most accurate beginning balance, reconcile your bank accounts first.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.CustomerRetentionViewModel
|
||||
@{ ViewData["Title"] = "Customer Retention"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6 col-md">
|
||||
<div class="card text-center border-success">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-success">@Model.ActiveCount</div>
|
||||
<div class="small text-muted">Active</div>
|
||||
<div class="small text-muted">(≤ 30 days)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md">
|
||||
<div class="card text-center border-warning">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-warning">@Model.AtRiskCount</div>
|
||||
<div class="small text-muted">At Risk</div>
|
||||
<div class="small text-muted">(31–60 days)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md">
|
||||
<div class="card text-center border-orange" style="border-color:#fd7e14!important">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold" style="color:#fd7e14">@Model.LapsingCount</div>
|
||||
<div class="small text-muted">Lapsing</div>
|
||||
<div class="small text-muted">(61–90 days)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md">
|
||||
<div class="card text-center border-danger">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-danger">@Model.ChurnedCount</div>
|
||||
<div class="small text-muted">Churned</div>
|
||||
<div class="small text-muted">(> 90 days)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md">
|
||||
<div class="card text-center border-secondary">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-secondary">@Model.NeverOrderedCount</div>
|
||||
<div class="small text-muted">Never Ordered</div>
|
||||
<div class="small text-muted"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var statusOrder = new[] { "Churned", "Lapsing", "At Risk", "Active", "Never Ordered" };
|
||||
var grouped = Model.Items.GroupBy(i => i.RetentionStatus).OrderBy(g => Array.IndexOf(statusOrder, g.Key));
|
||||
}
|
||||
|
||||
@foreach (var group in grouped)
|
||||
{
|
||||
var badgeClass = group.Key switch {
|
||||
"Active" => "bg-success",
|
||||
"At Risk" => "bg-warning text-dark",
|
||||
"Lapsing" => "bg-orange",
|
||||
"Churned" => "bg-danger",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
var headerClass = group.Key switch {
|
||||
"Active" => "table-success",
|
||||
"At Risk" => "table-warning",
|
||||
"Lapsing" => "table-orange",
|
||||
"Churned" => "table-danger",
|
||||
_ => "table-secondary"
|
||||
};
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center @headerClass">
|
||||
<span class="fw-semibold">@group.Key</span>
|
||||
<span class="badge @badgeClass">@group.Count()</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th class="text-end">Jobs</th>
|
||||
<th class="text-end">Lifetime Revenue</th>
|
||||
<th>Last Job</th>
|
||||
<th class="text-end">Days Since</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in group.OrderByDescending(i => i.LifetimeRevenue))
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-controller="Customers" asp-action="Details" asp-route-id="@item.CustomerId">
|
||||
@item.CustomerName
|
||||
</a>
|
||||
</td>
|
||||
<td class="small">@(item.Email ?? "—")</td>
|
||||
<td class="small">@(item.Phone ?? "—")</td>
|
||||
<td class="text-end">@item.TotalJobs</td>
|
||||
<td class="text-end">@item.LifetimeRevenue.ToString("C")</td>
|
||||
<td>@(item.LastJobDate?.ToString("MMM d, yyyy") ?? "—")</td>
|
||||
<td class="text-end">
|
||||
@if (item.DaysSinceLastJob < 0)
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@item.DaysSinceLastJob</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
@using System.Text.Json
|
||||
@{
|
||||
ViewData["Title"] = "Ask Your Financials";
|
||||
ViewData["PageIcon"] = "bi-chat-dots";
|
||||
var context = ViewBag.Context as PowderCoating.Application.DTOs.AI.FinancialQueryContext;
|
||||
var contextJson = JsonSerializer.Serialize(context);
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-semibold mb-1"><i class="bi bi-chat-dots text-primary me-2"></i>Ask Your Financials</h4>
|
||||
<p class="text-muted small mb-0">Ask Claude a plain-English question about your business finances. Data as of @DateTime.Today.ToString("MMMM d, yyyy").</p>
|
||||
</div>
|
||||
<a asp-action="Landing" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Reports
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<!-- Query input -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<label class="form-label fw-semibold">Your question</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="queryInput" class="form-control form-control-lg"
|
||||
placeholder="e.g. What did we spend on powder last quarter?"
|
||||
autocomplete="off" />
|
||||
<button id="queryBtn" class="btn btn-primary px-4" type="button">
|
||||
<i class="bi bi-send me-1"></i>Ask
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 d-flex flex-wrap gap-2" id="suggestionChips">
|
||||
<span class="text-muted small me-1">Try:</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Answer area -->
|
||||
<div id="answerArea" class="d-none">
|
||||
<div class="card shadow-sm border-primary border-opacity-25">
|
||||
<div class="card-header bg-primary-subtle text-primary-emphasis fw-semibold">
|
||||
<i class="bi bi-robot me-1"></i>Claude's Answer
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="answerSpinner" class="text-center py-3 d-none">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="text-muted mt-2 small">Analyzing your financials…</p>
|
||||
</div>
|
||||
<div id="answerError" class="alert alert-danger alert-permanent d-none"></div>
|
||||
<p id="answerText" class="mb-3 fs-6 d-none"></p>
|
||||
<div id="factsArea" class="d-none">
|
||||
<p class="small fw-semibold text-muted mb-1">Supporting data:</p>
|
||||
<ul id="factsList" class="list-unstyled small text-muted mb-0"></ul>
|
||||
</div>
|
||||
<div id="followUpArea" class="mt-3 pt-3 border-top d-none">
|
||||
<span class="text-muted small me-2">Follow-up suggestion:</span>
|
||||
<button id="followUpBtn" class="btn btn-sm btn-outline-primary"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History -->
|
||||
<div id="historyArea" class="mt-3 d-none">
|
||||
<p class="text-muted small fw-semibold mb-2"><i class="bi bi-clock-history me-1"></i>Earlier questions this session</p>
|
||||
<div id="historyList" class="d-flex flex-column gap-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Snapshot -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header fw-semibold text-muted small">
|
||||
<i class="bi bi-graph-up me-1"></i>YTD Snapshot
|
||||
</div>
|
||||
<div class="card-body pb-2">
|
||||
<div class="d-flex justify-content-between small mb-2">
|
||||
<span class="text-muted">Revenue</span>
|
||||
<span class="fw-medium">@context?.TotalRevenueYtd.ToString("C0")</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small mb-2">
|
||||
<span class="text-muted">Expenses</span>
|
||||
<span class="fw-medium">@context?.TotalExpensesYtd.ToString("C0")</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small mb-2 border-top pt-2">
|
||||
<span class="fw-semibold">Net Income</span>
|
||||
<span class="fw-bold @(context?.NetIncomeYtd >= 0 ? "text-success" : "text-danger")">
|
||||
@context?.NetIncomeYtd.ToString("C0")
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small mb-2 border-top pt-2">
|
||||
<span class="text-muted">AR Outstanding</span>
|
||||
<span class="fw-medium text-warning">@context?.ArOutstanding.ToString("C0")</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small mb-2">
|
||||
<span class="text-muted">AP Outstanding</span>
|
||||
<span class="fw-medium text-danger">@context?.ApOutstanding.ToString("C0")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tips -->
|
||||
<div class="card shadow-sm border-0 bg-light">
|
||||
<div class="card-body small">
|
||||
<p class="fw-semibold text-muted mb-2"><i class="bi bi-lightbulb me-1 text-warning"></i>Tips</p>
|
||||
<ul class="list-unstyled text-muted mb-0 small">
|
||||
<li class="mb-1">Ask about specific time periods: "last month", "Q1", "this year"</li>
|
||||
<li class="mb-1">Compare periods: "compared to last quarter"</li>
|
||||
<li class="mb-1">Ask about vendors, categories, or customers</li>
|
||||
<li>Claude only uses data it was given — it won't invent figures</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="contextJson" value="@Html.Raw(System.Text.Encodings.Web.HtmlEncoder.Default.Encode(contextJson))" />
|
||||
|
||||
@section Scripts {
|
||||
<script src="/js/financial-query.js"></script>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.InventoryTurnoverViewModel
|
||||
@{ ViewData["Title"] = "Inventory Turnover"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Stock Consumption Rates</span>
|
||||
<div class="d-flex gap-2 small">
|
||||
<span class="badge bg-danger">Critical ≤ 7 days</span>
|
||||
<span class="badge bg-warning text-dark">Low ≤ 30 days</span>
|
||||
<span class="badge bg-success">Normal</span>
|
||||
<span class="badge bg-info">Overstocked</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Item / SKU</th>
|
||||
<th>Color</th>
|
||||
<th class="text-end">Current Stock (lbs)</th>
|
||||
<th class="text-end">Consumed (lbs)</th>
|
||||
<th class="text-end">Purchased (lbs)</th>
|
||||
<th class="text-end">Daily Use (lbs)</th>
|
||||
<th class="text-end">Days to Stockout</th>
|
||||
<th class="text-end">Turnover Rate</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
var statusBadge = item.StockStatus switch {
|
||||
"Critical" => "bg-danger",
|
||||
"Low" => "bg-warning text-dark",
|
||||
"Normal" => "bg-success",
|
||||
"Overstocked" => "bg-info",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
var daysClass = item.StockStatus switch {
|
||||
"Critical" => "text-danger fw-bold",
|
||||
"Low" => "text-warning fw-semibold",
|
||||
_ => ""
|
||||
};
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">@item.ItemName</div>
|
||||
@if (!string.IsNullOrEmpty(item.SKU))
|
||||
{
|
||||
<div class="small text-muted">@item.SKU</div>
|
||||
}
|
||||
</td>
|
||||
<td>@(item.ColorName ?? "—")</td>
|
||||
<td class="text-end">@item.CurrentStockLbs.ToString("N1")</td>
|
||||
<td class="text-end">@item.TotalConsumedLbs.ToString("N1")</td>
|
||||
<td class="text-end">@item.TotalPurchasedLbs.ToString("N1")</td>
|
||||
<td class="text-end">@item.DailyConsumptionLbs.ToString("N3")</td>
|
||||
<td class="text-end @daysClass">
|
||||
@(item.DaysToStockout >= 9999 ? "∞" : item.DaysToStockout.ToString("N0"))
|
||||
</td>
|
||||
<td class="text-end">@item.TurnoverRate.ToString("N2")x</td>
|
||||
<td><span class="badge @statusBadge">@item.StockStatus</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.InvoiceAgingDetailViewModel
|
||||
@{ ViewData["Title"] = "Invoice Aging Detail"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<div class="card text-bg-danger">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Outstanding</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalBalance.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body py-2 text-center">
|
||||
<div class="small text-muted">Open Invoices</div>
|
||||
<div class="fs-5 fw-bold">@Model.Items.Count</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body py-2 text-center">
|
||||
<div class="small text-muted">Overdue (1–30 days)</div>
|
||||
<div class="fs-5 fw-bold text-warning">
|
||||
@Model.Items.Where(i => i.AgingBucket == "1–30 Days").Sum(i => i.BalanceDue).ToString("C")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body py-2 text-center">
|
||||
<div class="small text-muted">Overdue (90+ days)</div>
|
||||
<div class="fs-5 fw-bold text-danger">
|
||||
@Model.Items.Where(i => i.AgingBucket == "90+ Days").Sum(i => i.BalanceDue).ToString("C")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Customer</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Due Date</th>
|
||||
<th class="text-end">Total</th>
|
||||
<th class="text-end">Paid</th>
|
||||
<th class="text-end">Balance Due</th>
|
||||
<th class="text-end">Days Overdue</th>
|
||||
<th>Aging Bucket</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
var bucketClass = item.AgingBucket switch {
|
||||
"Current" => "text-success",
|
||||
"1–30 Days" => "text-warning",
|
||||
"31–60 Days" => "text-orange",
|
||||
"61–90 Days" => "text-danger",
|
||||
"90+ Days" => "fw-bold text-danger",
|
||||
_ => ""
|
||||
};
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-controller="Invoices" asp-action="Details" asp-route-id="@item.InvoiceId">
|
||||
@item.InvoiceNumber
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@item.CustomerName
|
||||
@if (!string.IsNullOrEmpty(item.CustomerEmail))
|
||||
{
|
||||
<div class="small text-muted">@item.CustomerEmail</div>
|
||||
}
|
||||
</td>
|
||||
<td>@item.InvoiceDate.ToString("MMM d, yyyy")</td>
|
||||
<td>@(item.DueDate?.ToString("MMM d, yyyy") ?? "—")</td>
|
||||
<td class="text-end">@item.Total.ToString("C")</td>
|
||||
<td class="text-end text-success">@item.AmountPaid.ToString("C")</td>
|
||||
<td class="text-end fw-semibold">@item.BalanceDue.ToString("C")</td>
|
||||
<td class="text-end @bucketClass">
|
||||
@(item.DaysOverdue > 0 ? item.DaysOverdue.ToString() : "—")
|
||||
</td>
|
||||
<td><span class="badge @bucketClass bg-opacity-10 border">@item.AgingBucket</span></td>
|
||||
<td><span class="badge bg-secondary-subtle text-secondary">@item.StatusDisplay</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-bold">
|
||||
<tr>
|
||||
<td colspan="6">Total Outstanding</td>
|
||||
<td class="text-end">@Model.TotalBalance.ToString("C")</td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.JobStatusAgingViewModel
|
||||
@{ ViewData["Title"] = "Job Status Aging"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Active Jobs by Days in Current Status</span>
|
||||
<span class="text-muted small">@Model.Items.Count active jobs</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Job #</th>
|
||||
<th>Customer</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th class="text-end">Days in Status</th>
|
||||
<th>Due Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
var rowClass = item.IsOverdue ? "table-danger" : item.DaysInCurrentStatus > 7 ? "table-warning" : "";
|
||||
<tr class="@rowClass">
|
||||
<td>
|
||||
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@item.JobId">
|
||||
@item.JobNumber
|
||||
</a>
|
||||
</td>
|
||||
<td>@item.CustomerName</td>
|
||||
<td><span class="badge bg-secondary">@item.StatusName</span></td>
|
||||
<td>
|
||||
@{
|
||||
var pBadge = item.PriorityCode switch {
|
||||
"RUSH" => "bg-danger",
|
||||
"URGENT" => "bg-warning text-dark",
|
||||
"HIGH" => "bg-orange text-white",
|
||||
"LOW" => "bg-secondary",
|
||||
_ => "bg-primary"
|
||||
};
|
||||
}
|
||||
<span class="badge @pBadge">@item.PriorityName</span>
|
||||
</td>
|
||||
<td class="text-end fw-semibold">
|
||||
@item.DaysInCurrentStatus day@(item.DaysInCurrentStatus != 1 ? "s" : "")
|
||||
</td>
|
||||
<td>
|
||||
@if (item.DueDate.HasValue)
|
||||
{
|
||||
if (item.IsOverdue)
|
||||
{
|
||||
<span class="text-danger fw-semibold">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>@item.DueDate.Value.ToString("MMM d, yyyy")
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@item.DueDate.Value.ToString("MMM d, yyyy")</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
@{
|
||||
ViewData["Title"] = "Reports";
|
||||
ViewData["PageIcon"] = "bi-bar-chart-line";
|
||||
var allowAccounting = Context.Items["AllowAccounting"] as bool? ?? false;
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.report-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.875rem;
|
||||
padding: 1.25rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
border-color: var(--bs-primary);
|
||||
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.12);
|
||||
transform: translateY(-2px);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.report-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.report-card h5 {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.report-card p {
|
||||
font-size: 0.82rem;
|
||||
color: var(--bs-secondary-color);
|
||||
margin: 0;
|
||||
line-height: 1.45;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.report-card .report-arrow {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--bs-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bs-secondary-color);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
|
||||
<!-- ══ SECTION 1: CHARTS & DASHBOARDS ═══════════════════════════════════ -->
|
||||
<div class="mb-2">
|
||||
<h5 class="fw-bold mb-1"><i class="bi bi-bar-chart-line me-2 text-primary"></i>Charts & Dashboards</h5>
|
||||
<p class="text-muted small mb-3">Interactive visuals, trend charts, and at-a-glance summaries.</p>
|
||||
</div>
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="mb-4">
|
||||
<div class="section-heading">Overview</div>
|
||||
<div class="report-grid">
|
||||
<a asp-controller="Reports" asp-action="KpiDashboard" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ede9fe;color:#6d28d9;">
|
||||
<i class="bi bi-speedometer2"></i>
|
||||
</div>
|
||||
<h5>KPI Dashboard</h5>
|
||||
<p>High-level KPIs — revenue, active jobs, customers, and job counts with monthly trends and equipment status.</p>
|
||||
<div class="report-arrow">View dashboard <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="Analytics" class="report-card">
|
||||
<div class="report-card-icon" style="background:#eff6ff;color:#2563eb;">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
</div>
|
||||
<h5>Charts & Analytics</h5>
|
||||
<p>Tabbed visual dashboard covering revenue trends, operations, customers, financials, and powder usage with interactive charts.</p>
|
||||
<div class="report-arrow">View charts <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="CashFlowForecast" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0fdf4;color:#16a34a;">
|
||||
<i class="bi bi-cash-stack"></i>
|
||||
</div>
|
||||
<h5>Cash Flow Forecast</h5>
|
||||
<p>AI-projected 30/60/90-day cash position based on open invoices, outstanding bills, and active job pipeline.</p>
|
||||
<div class="report-arrow">View forecast <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="AnomalyDetection" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fffbeb;color:#b45309;">
|
||||
<i class="bi bi-shield-exclamation"></i>
|
||||
</div>
|
||||
<h5>Anomaly Detection</h5>
|
||||
<p>AI scans recent bills and expense trends for duplicate entries, unusual amounts, and accounts running over their historical average.</p>
|
||||
<div class="report-arrow">Run analysis <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="FinancialQuery" class="report-card">
|
||||
<div class="report-card-icon" style="background:#eff6ff;color:#2563eb;">
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
</div>
|
||||
<h5>Ask Your Financials</h5>
|
||||
<p>Ask Claude plain-English questions about your revenue, expenses, and AR. Answers are grounded in your actual financial data.</p>
|
||||
<div class="report-arrow">Ask a question <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="ArAging" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fef3c7;color:#d97706;">
|
||||
<i class="bi bi-hourglass-split"></i>
|
||||
</div>
|
||||
<h5>AR Aging + Risk Prediction</h5>
|
||||
<p>View outstanding invoices by aging bucket, then run AI payment risk scoring to prioritize your follow-up calls.</p>
|
||||
<div class="report-arrow">View aging <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<!-- ══ SECTION 2: DETAILED REPORTS ══════════════════════════════════════ -->
|
||||
<div class="mb-2">
|
||||
<h5 class="fw-bold mb-1"><i class="bi bi-file-earmark-text me-2 text-secondary"></i>Detailed Reports</h5>
|
||||
<p class="text-muted small mb-3">Printable data reports for accounting, operations, and inventory.</p>
|
||||
</div>
|
||||
|
||||
@if (allowAccounting)
|
||||
{
|
||||
<!-- Finance -->
|
||||
<div class="mb-4">
|
||||
<div class="section-heading">Finance</div>
|
||||
<div class="report-grid">
|
||||
<a asp-controller="Reports" asp-action="FinancialSummary" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ecfdf5;color:#059669;">
|
||||
<i class="bi bi-wallet2"></i>
|
||||
</div>
|
||||
<h5>Financial Summary</h5>
|
||||
<p>Invoiced vs collected, outstanding balances, AR aging buckets, and recent payment activity.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="ProfitAndLoss" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0fdf4;color:#16a34a;">
|
||||
<i class="bi bi-file-earmark-bar-graph"></i>
|
||||
</div>
|
||||
<h5>Profit & Loss</h5>
|
||||
<p>Revenue, COGS, gross profit, and operating expenses for any date range. Exportable to PDF.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="ExpensesAp" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fff7ed;color:#c2410c;">
|
||||
<i class="bi bi-receipt"></i>
|
||||
</div>
|
||||
<h5>Expenses & AP</h5>
|
||||
<p>Vendor spend, accounts payable aging, expense breakdown by account, and monthly P&L comparison.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="ArAging" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fef9c3;color:#a16207;">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</div>
|
||||
<h5>AR Aging</h5>
|
||||
<p>Outstanding customer balances by age — current, 30, 60, and 90+ days. Exportable to PDF.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="InvoiceAgingDetail" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fef2f2;color:#dc2626;">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
</div>
|
||||
<h5>Invoice Aging Detail</h5>
|
||||
<p>Every open invoice with aging bucket, days overdue, contact info, and balance due.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="BalanceSheet" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0f9ff;color:#0369a1;">
|
||||
<i class="bi bi-bank"></i>
|
||||
</div>
|
||||
<h5>Balance Sheet</h5>
|
||||
<p>Snapshot of assets, liabilities, and equity as of any date.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="ApAging" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fff1f2;color:#b91c1c;">
|
||||
<i class="bi bi-hourglass-split"></i>
|
||||
</div>
|
||||
<h5>AP Aging</h5>
|
||||
<p>Outstanding vendor bills by age — current, 30, 60, and 90+ days past due. Exportable to PDF.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="TrialBalance" class="report-card">
|
||||
<div class="report-card-icon" style="background:#eef2ff;color:#4338ca;">
|
||||
<i class="bi bi-list-columns-reverse"></i>
|
||||
</div>
|
||||
<h5>Trial Balance</h5>
|
||||
<p>All active accounts with debit and credit balances — validates that your books are in balance.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="CashFlowStatement" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ecfeff;color:#0891b2;">
|
||||
<i class="bi bi-water"></i>
|
||||
</div>
|
||||
<h5>Cash Flow Statement</h5>
|
||||
<p>Track actual cash in/out across operating, investing, and financing activities with beginning and ending cash balance.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="TaxReporting1099" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fdf2f8;color:#86198f;">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</div>
|
||||
<h5>1099-NEC Report</h5>
|
||||
<p>Payments to 1099-eligible vendors by calendar year — flags those exceeding the $600 reporting threshold.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="BudgetVsActual" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0fdf4;color:#15803d;">
|
||||
<i class="bi bi-bar-chart-line"></i>
|
||||
</div>
|
||||
<h5>Budget vs. Actual</h5>
|
||||
<p>Compare monthly budgeted amounts against real P&L activity — revenue, expenses, and net income variance.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Sales & Customers -->
|
||||
<div class="mb-4">
|
||||
<div class="section-heading">Sales & Customers</div>
|
||||
<div class="report-grid">
|
||||
<a asp-controller="Reports" asp-action="SalesByCustomer" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0fdf4;color:#15803d;">
|
||||
<i class="bi bi-currency-dollar"></i>
|
||||
</div>
|
||||
<h5>Sales by Customer</h5>
|
||||
<p>Revenue, collections, outstanding balance, and invoice count ranked per customer.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="SalesAndIncome" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ecfdf5;color:#059669;">
|
||||
<i class="bi bi-bar-chart-steps"></i>
|
||||
</div>
|
||||
<h5>Sales & Income</h5>
|
||||
<p>Detailed breakdown of sales by customer, job type, and period.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="SalesTax" class="report-card">
|
||||
<div class="report-card-icon" style="background:#faf5ff;color:#7c3aed;">
|
||||
<i class="bi bi-percent"></i>
|
||||
</div>
|
||||
<h5>Sales Tax Report</h5>
|
||||
<p>Invoice-basis tax liability: taxable vs non-taxable sales, tax billed by account and month, full invoice detail.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="CustomerOverview" class="report-card">
|
||||
<div class="report-card-icon" style="background:#eff6ff;color:#2563eb;">
|
||||
<i class="bi bi-people"></i>
|
||||
</div>
|
||||
<h5>Customer Overview</h5>
|
||||
<p>New customer acquisition trends, lifetime value rankings, and quote conversion funnel.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="CustomerRetention" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fef9c3;color:#b45309;">
|
||||
<i class="bi bi-person-check"></i>
|
||||
</div>
|
||||
<h5>Customer Retention</h5>
|
||||
<p>Customers segmented by recency — active, at risk, lapsing, churned, or never ordered.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operations -->
|
||||
<div class="mb-4">
|
||||
<div class="section-heading">Operations</div>
|
||||
<div class="report-grid">
|
||||
<a asp-controller="Reports" asp-action="OperationsReport" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f5f3ff;color:#7c3aed;">
|
||||
<i class="bi bi-gear-wide-connected"></i>
|
||||
</div>
|
||||
<h5>Operations Report</h5>
|
||||
<p>Job status breakdown, appointment metrics, worker performance, and equipment health.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="JobStatusAging" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fff7ed;color:#ea580c;">
|
||||
<i class="bi bi-hourglass-split"></i>
|
||||
</div>
|
||||
<h5>Job Status Aging</h5>
|
||||
<p>Active jobs ranked by days spent in their current status. Quickly spot bottlenecks and overdue work.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="JobCycleTime" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0f9ff;color:#0284c7;">
|
||||
<i class="bi bi-stopwatch"></i>
|
||||
</div>
|
||||
<h5>Job Cycle Time</h5>
|
||||
<p>Average, min, and max days spent in each workflow stage for completed jobs.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="RevenueTrends" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ede9fe;color:#7c3aed;">
|
||||
<i class="bi bi-trending-up"></i>
|
||||
</div>
|
||||
<h5>Revenue Trends</h5>
|
||||
<p>Monthly revenue, job count, and average order value trends with breakdowns by customer type and priority.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inventory & Powder -->
|
||||
<div class="mb-4">
|
||||
<div class="section-heading">Inventory & Powder</div>
|
||||
<div class="report-grid">
|
||||
<a asp-controller="Reports" asp-action="PowderUsage" class="report-card">
|
||||
<div class="report-card-icon" style="background:#fdf4ff;color:#a21caf;">
|
||||
<i class="bi bi-droplet-half"></i>
|
||||
</div>
|
||||
<h5>Powder Usage</h5>
|
||||
<p>Powder consumption by color with monthly lbs and cost trends. See which colors drive the most usage.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="PowderConsumption" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f5f3ff;color:#6d28d9;">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
</div>
|
||||
<h5>Powder Consumption vs Purchase</h5>
|
||||
<p>Compare purchased vs consumed powder per SKU. Track waste, variance, and restocking needs.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="InventoryTurnover" class="report-card">
|
||||
<div class="report-card-icon" style="background:#ecfdf5;color:#047857;">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</div>
|
||||
<h5>Inventory Turnover</h5>
|
||||
<p>Daily consumption rates and days-to-stockout per item. Critical and low-stock items highlighted.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ((bool)(ViewBag.SmsEnabled ?? false))
|
||||
{
|
||||
<!-- Compliance -->
|
||||
<div class="mb-4">
|
||||
<div class="section-heading">Compliance</div>
|
||||
<div class="report-grid">
|
||||
<a asp-controller="SmsConsentAudit" asp-action="Index" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0f9ff;color:#0369a1;">
|
||||
<i class="bi bi-phone-vibrate"></i>
|
||||
</div>
|
||||
<h5>SMS Consent Audit</h5>
|
||||
<p>Per-customer TCPA consent status — who opted in, who opted out, and when. Export to CSV for compliance records.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.OperationsReportViewModel
|
||||
@{ ViewData["Title"] = "Operations Report"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-bg-primary">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Active Jobs</div>
|
||||
<div class="fs-5 fw-bold">@Model.ActiveJobsCount</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-bg-info">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Appointments (period)</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalAppointments</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-bg-success">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Appt Completion Rate</div>
|
||||
<div class="fs-5 fw-bold">@Model.AppointmentCompletionRate.ToString("N1")%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-bg-warning">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Low Stock Items</div>
|
||||
<div class="fs-5 fw-bold">@Model.LowStockItems.Count</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Jobs by Status</div>
|
||||
<div class="card-body">
|
||||
<canvas id="statusChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Active Jobs by Priority</div>
|
||||
<div class="card-body">
|
||||
<canvas id="priorityChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Appointments by Day of Week</div>
|
||||
<div class="card-body">
|
||||
<canvas id="apptDayChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.WorkerStats.Any())
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header fw-semibold">Worker Performance</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr><th>Worker</th><th>Role</th><th class="text-end">Jobs Assigned</th><th class="text-end">Jobs Completed</th><th class="text-end">Completion Rate</th><th class="text-end">Appts</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var w in Model.WorkerStats)
|
||||
{
|
||||
<tr>
|
||||
<td>@w.Name</td>
|
||||
<td class="text-muted small">@w.Role</td>
|
||||
<td class="text-end">@w.JobsAssigned</td>
|
||||
<td class="text-end">@w.JobsCompleted</td>
|
||||
<td class="text-end">
|
||||
<div class="d-flex align-items-center justify-content-end gap-2">
|
||||
<div class="progress flex-grow-1" style="height:6px;max-width:80px">
|
||||
<div class="progress-bar bg-success" style="width:@w.CompletionRate%"></div>
|
||||
</div>
|
||||
@w.CompletionRate.ToString("N0")%
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">@w.AppointmentsAssigned</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.LowStockItems.Any())
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold d-flex justify-content-between">
|
||||
<span>Low Stock Alert</span>
|
||||
<a asp-controller="Inventory" asp-action="Index" class="btn btn-sm btn-outline-warning">View Inventory</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr><th>Item</th><th>Color</th><th class="text-end">On Hand</th><th class="text-end">Reorder Point</th><th>Unit</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var i in Model.LowStockItems)
|
||||
{
|
||||
<tr class="@(i.QuantityOnHand == 0 ? "table-danger" : "table-warning")">
|
||||
<td>@i.Name</td>
|
||||
<td>@(i.ColorName ?? "—")</td>
|
||||
<td class="text-end fw-semibold text-danger">@i.QuantityOnHand.ToString("N1")</td>
|
||||
<td class="text-end">@i.ReorderPoint.ToString("N1")</td>
|
||||
<td class="text-muted small">@i.UnitOfMeasure</td>
|
||||
<td>
|
||||
@if (i.QuantityOnHand == 0)
|
||||
{
|
||||
<span class="badge bg-dark">Out of Stock</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Low Stock</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/lib/chartjs/chart.umd.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const palette = ['#0d6efd','#6610f2','#6f42c1','#d63384','#dc3545','#fd7e14','#ffc107','#198754','#20c997','#0dcaf0','#adb5bd','#343a40','#6c757d','#495057','#212529','#ced4da'];
|
||||
|
||||
const statusLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.JobsByStatus.Keys));
|
||||
const statusData = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.JobsByStatus.Values));
|
||||
new Chart(document.getElementById('statusChart'), {
|
||||
type: 'doughnut',
|
||||
data: { labels: statusLabels, datasets: [{ data: statusData, backgroundColor: palette }] },
|
||||
options: { responsive: true, plugins: { legend: { position: 'right', labels: { font: { size: 10 } } } } }
|
||||
});
|
||||
|
||||
const prioLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.ActiveJobsByPriority.Keys));
|
||||
const prioData = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.ActiveJobsByPriority.Values));
|
||||
new Chart(document.getElementById('priorityChart'), {
|
||||
type: 'bar',
|
||||
data: { labels: prioLabels, datasets: [{ label: 'Jobs', data: prioData, backgroundColor: ['#6c757d','#0d6efd','#ffc107','#dc3545','#842029'] }] },
|
||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
||||
});
|
||||
|
||||
const dayLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AppointmentsByDayOfWeek.Keys));
|
||||
const dayData = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AppointmentsByDayOfWeek.Values));
|
||||
new Chart(document.getElementById('apptDayChart'), {
|
||||
type: 'bar',
|
||||
data: { labels: dayLabels, datasets: [{ label: 'Appointments', data: dayData, backgroundColor: 'rgba(13,202,240,0.7)' }] },
|
||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.PowderConsumptionViewModel
|
||||
@{ ViewData["Title"] = "Powder Consumption vs Purchase"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-bg-primary">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Purchased</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalPurchasedLbs.ToString("N1") lbs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-bg-success">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Consumed</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalConsumedLbs.ToString("N1") lbs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card @(Model.TotalPurchasedLbs - Model.TotalConsumedLbs >= 0 ? "text-bg-info" : "text-bg-warning")">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Net Variance</div>
|
||||
<div class="fs-5 fw-bold">@((Model.TotalPurchasedLbs - Model.TotalConsumedLbs).ToString("N1")) lbs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Purchased vs Consumed per SKU</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Item / SKU</th>
|
||||
<th>Color</th>
|
||||
<th>Manufacturer</th>
|
||||
<th class="text-end">Purchased (lbs)</th>
|
||||
<th class="text-end">Consumed (lbs)</th>
|
||||
<th class="text-end">Variance (lbs)</th>
|
||||
<th class="text-end">Purchases</th>
|
||||
<th class="text-end">Jobs Used</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
var varianceClass = item.VarianceLbs < 0 ? "text-danger" : item.VarianceLbs == 0 ? "" : "text-success";
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">@item.ItemName</div>
|
||||
@if (!string.IsNullOrEmpty(item.SKU))
|
||||
{
|
||||
<div class="small text-muted">@item.SKU</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@(item.ColorName ?? "—")
|
||||
@if (!string.IsNullOrEmpty(item.ColorCode))
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary ms-1">@item.ColorCode</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-muted">@(item.Manufacturer ?? "—")</td>
|
||||
<td class="text-end">@item.TotalPurchasedLbs.ToString("N1")</td>
|
||||
<td class="text-end">@item.TotalConsumedLbs.ToString("N1")</td>
|
||||
<td class="text-end fw-semibold @varianceClass">@item.VarianceLbs.ToString("N1")</td>
|
||||
<td class="text-end">@item.PurchaseCount</td>
|
||||
<td class="text-end">@item.UsageJobCount</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-bold">
|
||||
<tr>
|
||||
<td colspan="3">Totals</td>
|
||||
<td class="text-end">@Model.TotalPurchasedLbs.ToString("N1")</td>
|
||||
<td class="text-end">@Model.TotalConsumedLbs.ToString("N1")</td>
|
||||
<td class="text-end">@((Model.TotalPurchasedLbs - Model.TotalConsumedLbs).ToString("N1"))</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.PowderUsageViewModel
|
||||
@{ ViewData["Title"] = "Powder Usage"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-bg-primary">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Powder Used</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalPowderUsedLbs.ToString("N1") lbs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-bg-success">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Powder Cost</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalPowderCost.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body py-2 text-center">
|
||||
<div class="small text-muted">Jobs with Usage</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalJobsWithPowderUsage</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body py-2 text-center">
|
||||
<div class="small text-muted">Colors Used</div>
|
||||
<div class="fs-5 fw-bold">@Model.ColorsUsedCount</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Monthly Powder Usage</div>
|
||||
<div class="card-body">
|
||||
<canvas id="monthlyChart" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Top Colors by Usage</div>
|
||||
<div class="card-body">
|
||||
<canvas id="colorChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Usage by Color</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Color</th>
|
||||
<th>SKU</th>
|
||||
<th>Manufacturer</th>
|
||||
<th class="text-end">Total Used (lbs)</th>
|
||||
<th class="text-end">Total Cost</th>
|
||||
<th class="text-end">Jobs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.TopColorsByUsage)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@item.DisplayLabel
|
||||
</td>
|
||||
<td class="text-muted small">@(item.SKU ?? "—")</td>
|
||||
<td class="text-muted small">@(item.Manufacturer ?? "—")</td>
|
||||
<td class="text-end fw-semibold">@item.TotalLbsUsed.ToString("N1")</td>
|
||||
<td class="text-end">@item.TotalCost.ToString("C")</td>
|
||||
<td class="text-end">@item.JobCount</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/lib/chartjs/chart.umd.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const monthLabels = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels));
|
||||
const lbsData = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlyPowderUsageLbs));
|
||||
const costData = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlyPowderUsageCost));
|
||||
|
||||
new Chart(document.getElementById('monthlyChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: monthLabels,
|
||||
datasets: [
|
||||
{ label: 'Lbs Used', data: lbsData, backgroundColor: 'rgba(13,110,253,0.7)', yAxisID: 'y' },
|
||||
{ label: 'Cost ($)', data: costData, type: 'line', borderColor: 'rgba(220,53,69,0.9)', backgroundColor: 'transparent', yAxisID: 'y2', tension: 0.3 }
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { position: 'top' } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, position: 'left', title: { display: true, text: 'lbs' } },
|
||||
y2: { beginAtZero: true, position: 'right', title: { display: true, text: '$' }, grid: { drawOnChartArea: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const topColors = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TopColorsByUsage.Take(8).Select(c => c.DisplayLabel)));
|
||||
const topLbs = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.TopColorsByUsage.Take(8).Select(c => c.TotalLbsUsed)));
|
||||
const palette = ['#0d6efd','#6610f2','#6f42c1','#d63384','#dc3545','#fd7e14','#ffc107','#198754'];
|
||||
|
||||
new Chart(document.getElementById('colorChart'), {
|
||||
type: 'doughnut',
|
||||
data: { labels: topColors, datasets: [{ data: topLbs, backgroundColor: palette }] },
|
||||
options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { font: { size: 11 } } } } }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.ProfitAndLossDto
|
||||
@{
|
||||
ViewData["Title"] = "Profit & Loss";
|
||||
ViewData["PageIcon"] = "bi-graph-up-arrow";
|
||||
var today = DateTime.Today;
|
||||
var ytdFrom = new DateTime(today.Year, 1, 1).ToString("yyyy-MM-dd");
|
||||
var ytdTo = today.ToString("yyyy-MM-dd");
|
||||
var q1From = new DateTime(today.Year, 1, 1).ToString("yyyy-MM-dd");
|
||||
var q1To = new DateTime(today.Year, 3, 31).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");
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
|
||||
body { font-size: 12px; }
|
||||
}
|
||||
.report-section-header { background: #f8f9fa; font-weight: 600; }
|
||||
.report-total-row { border-top: 2px solid #dee2e6; font-weight: 700; }
|
||||
.report-subtotal-row { border-top: 1px solid #dee2e6; font-weight: 600; }
|
||||
.report-net-row { background: #e8f5e9; font-weight: 700; font-size: 1.05em; }
|
||||
.report-net-negative { background: #fdecea; }
|
||||
</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">Income Statement — @Model.From.ToString("MMM d") – @Model.To.ToString("MMM d, yyyy")</p>
|
||||
@if (Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Cash)
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Cash Basis</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-info text-dark">Accrual Basis</span>
|
||||
}
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a href="@Url.Action("ProfitAndLossPdf", 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("ProfitAndLossPdf", 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("ProfitAndLoss", new { from = thisMonthFrom, to = thisMonthTo })" class="btn btn-outline-secondary">This Month</a>
|
||||
<a href="@Url.Action("ProfitAndLoss", new { from = ytdFrom, to = ytdTo })" class="btn btn-outline-secondary">YTD</a>
|
||||
<a href="@Url.Action("ProfitAndLoss", 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>Profit & Loss</h5>
|
||||
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") – @Model.To.ToString("MMMM d, yyyy")</p>
|
||||
</div>
|
||||
|
||||
<!-- KPI Summary -->
|
||||
<div class="row g-3 mb-4 no-print">
|
||||
<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 text-success mb-1">@Model.TotalRevenue.ToString("C")</div>
|
||||
<div class="text-muted small">Total Revenue</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 text-warning mb-1">@Model.TotalCogs.ToString("C")</div>
|
||||
<div class="text-muted small">Cost of Goods</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 text-danger mb-1">@Model.TotalExpenses.ToString("C")</div>
|
||||
<div class="text-muted small">Operating Expenses</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm text-center h-100 @(Model.NetIncome >= 0 ? "border-success border-opacity-50" : "border-danger border-opacity-50")">
|
||||
<div class="card-body py-3">
|
||||
<div class="h5 fw-bold @(Model.NetIncome >= 0 ? "text-success" : "text-danger") mb-1">@Model.NetIncome.ToString("C")</div>
|
||||
<div class="text-muted small">Net Income</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- P&L Statement -->
|
||||
<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-file-earmark-bar-graph me-1"></i>Income Statement</span>
|
||||
<span class="text-muted small">@Model.From.ToString("MMM d") – @Model.To.ToString("MMM d, yyyy")</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Account</th>
|
||||
<th class="text-end" style="width:160px">Amount</th>
|
||||
<th class="text-end" style="width:120px">% of Revenue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Revenue -->
|
||||
<tr class="report-section-header">
|
||||
<td colspan="3" class="py-2"><i class="bi bi-graph-up-arrow text-success me-2"></i>Revenue</td>
|
||||
</tr>
|
||||
@if (!Model.RevenueLines.Any())
|
||||
{
|
||||
<tr><td colspan="3" class="text-muted ps-4 small">No revenue recorded for this period.</td></tr>
|
||||
}
|
||||
@foreach (var line in Model.RevenueLines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-4">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
|
||||
<td class="text-end">@line.Amount.ToString("C")</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="report-subtotal-row">
|
||||
<td class="ps-4 fw-semibold">Total Revenue</td>
|
||||
<td class="text-end fw-semibold text-success">@Model.TotalRevenue.ToString("C")</td>
|
||||
<td class="text-end text-muted small">100%</td>
|
||||
</tr>
|
||||
|
||||
<!-- COGS -->
|
||||
@if (Model.CogsLines.Any())
|
||||
{
|
||||
<tr><td colspan="3" class="py-1"></td></tr>
|
||||
<tr class="report-section-header">
|
||||
<td colspan="3" class="py-2"><i class="bi bi-box-seam text-warning me-2"></i>Cost of Goods Sold</td>
|
||||
</tr>
|
||||
@foreach (var line in Model.CogsLines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-4">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
|
||||
<td class="text-end">@line.Amount.ToString("C")</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="report-subtotal-row">
|
||||
<td class="ps-4 fw-semibold">Total COGS</td>
|
||||
<td class="text-end fw-semibold text-warning">(@Model.TotalCogs.ToString("C"))</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : (Model.TotalCogs / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
|
||||
</tr>
|
||||
<tr class="report-subtotal-row">
|
||||
<td class="ps-2 fw-semibold">Gross Profit</td>
|
||||
<td class="text-end fw-semibold @(Model.GrossProfit >= 0 ? "text-success" : "text-danger")">@Model.GrossProfit.ToString("C")</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : Model.GrossMarginPercent.ToString("F1") + "%")</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
<!-- Expenses -->
|
||||
<tr><td colspan="3" class="py-1"></td></tr>
|
||||
<tr class="report-section-header">
|
||||
<td colspan="3" class="py-2"><i class="bi bi-receipt-cutoff text-danger me-2"></i>Operating Expenses</td>
|
||||
</tr>
|
||||
@if (!Model.ExpenseLines.Any())
|
||||
{
|
||||
<tr><td colspan="3" class="text-muted ps-4 small">No expenses recorded for this period.</td></tr>
|
||||
}
|
||||
@foreach (var line in Model.ExpenseLines)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-4">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
|
||||
<td class="text-end">@line.Amount.ToString("C")</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="report-subtotal-row">
|
||||
<td class="ps-4 fw-semibold">Total Expenses</td>
|
||||
<td class="text-end fw-semibold text-danger">(@Model.TotalExpenses.ToString("C"))</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : (Model.TotalExpenses / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="report-net-row @(Model.NetIncome < 0 ? "report-net-negative" : "")">
|
||||
<td class="ps-2">Net Income</td>
|
||||
<td class="text-end @(Model.NetIncome >= 0 ? "text-success" : "text-danger")">@Model.NetIncome.ToString("C")</td>
|
||||
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "—" : (Model.NetIncome / Model.TotalRevenue * 100).ToString("F1") + "%")</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") · Accrual basis · Revenue from sent/paid invoices; expenses from bills and direct expenses.
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
@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 & 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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
@model PowderCoating.Web.ViewModels.Reports.SalesByCustomerViewModel
|
||||
@{ ViewData["Title"] = "Sales by Customer"; }
|
||||
|
||||
<partial name="_ReportHeader" model="Model" />
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-bg-primary">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Invoiced</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalInvoiced.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-bg-success">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Total Collected</div>
|
||||
<div class="fs-5 fw-bold">@Model.TotalPaid.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-bg-warning">
|
||||
<div class="card-body py-2">
|
||||
<div class="small">Outstanding Balance</div>
|
||||
<div class="fs-5 fw-bold">@((Model.TotalInvoiced - Model.TotalPaid).ToString("C"))</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Customer Sales Summary</span>
|
||||
<span class="text-muted small">@Model.Items.Count customers</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Type</th>
|
||||
<th class="text-end">Invoices</th>
|
||||
<th class="text-end">Total Invoiced</th>
|
||||
<th class="text-end">Total Paid</th>
|
||||
<th class="text-end">Balance Due</th>
|
||||
<th class="text-end">Avg Invoice</th>
|
||||
<th>Last Invoice</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-controller="Customers" asp-action="Details" asp-route-id="@item.CustomerId">
|
||||
@item.CustomerName
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@if (item.IsCommercial)
|
||||
{
|
||||
<span class="badge bg-info-subtle text-info">Commercial</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary">Individual</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">@item.InvoiceCount</td>
|
||||
<td class="text-end fw-semibold">@item.TotalInvoiced.ToString("C")</td>
|
||||
<td class="text-end text-success">@item.TotalPaid.ToString("C")</td>
|
||||
<td class="text-end @(item.BalanceDue > 0 ? "text-warning fw-semibold" : "")">
|
||||
@item.BalanceDue.ToString("C")
|
||||
</td>
|
||||
<td class="text-end">@item.AvgInvoiceValue.ToString("C")</td>
|
||||
<td>@(item.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "—")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-bold">
|
||||
<tr>
|
||||
<td colspan="3">Totals</td>
|
||||
<td class="text-end">@Model.TotalInvoiced.ToString("C")</td>
|
||||
<td class="text-end text-success">@Model.TotalPaid.ToString("C")</td>
|
||||
<td class="text-end">@((Model.TotalInvoiced - Model.TotalPaid).ToString("C"))</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.SalesTaxReportDto
|
||||
@{
|
||||
ViewData["Title"] = "Sales Tax Report";
|
||||
ViewData["PageIcon"] = "bi-percent";
|
||||
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 monthTaxable = Model.ByMonth.Select(m => m.TaxableSales).ToList();
|
||||
var monthTaxBilled = Model.ByMonth.Select(m => m.TaxBilled).ToList();
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
|
||||
body { font-size: 11px; }
|
||||
}
|
||||
.row-nontaxable td { background-color: #f8f9fa !important; color: #6c757d; }
|
||||
</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.TaxableInvoiceCount + Model.NonTaxableInvoiceCount) invoices</p>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a href="@Url.Action("SalesTaxCsv", new { from = Model.From.ToString("yyyy-MM-dd"), to = Model.To.ToString("yyyy-MM-dd") })"
|
||||
class="btn btn-sm btn-outline-success no-print">
|
||||
<i class="bi bi-filetype-csv me-1"></i>Export CSV
|
||||
</a>
|
||||
<a href="@Url.Action("SalesTaxPdf", 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("SalesTaxPdf", 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("SalesTax", new { from = thisMonthFrom, to = thisMonthTo })" class="btn btn-outline-secondary">This Month</a>
|
||||
<a href="@Url.Action("SalesTax", new { from = lastMonthFrom, to = lastMonthTo })" class="btn btn-outline-secondary">Last Month</a>
|
||||
<a href="@Url.Action("SalesTax", new { from = ytdFrom, to = ytdTo })" class="btn btn-outline-secondary">YTD</a>
|
||||
<a href="@Url.Action("SalesTax", 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 Tax Liability Report</h5>
|
||||
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") – @Model.To.ToString("MMMM d, yyyy") · Invoice Basis</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 border-primary border-top border-3">
|
||||
<div class="card-body py-3">
|
||||
<div class="h5 fw-bold text-primary mb-1">@Model.TotalTaxBilled.ToString("C")</div>
|
||||
<div class="text-muted small">Total Tax Billed</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.TotalTaxableSales.ToString("C")</div>
|
||||
<div class="text-muted small">Taxable Sales</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">@Model.TaxableInvoiceCount invoices</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-secondary mb-1">@Model.TotalNonTaxableSales.ToString("C")</div>
|
||||
<div class="text-muted small">Non-Taxable Sales</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">@Model.NonTaxableInvoiceCount invoices</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.EffectiveTaxRate.ToString("F2")%</div>
|
||||
<div class="text-muted small">Effective Tax Rate</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">on taxable sales</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 Tax Trend
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="taxTrendChart" 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">Taxable Sales</th>
|
||||
<th class="text-end">Tax Billed</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.TaxableSales.ToString("C")</td>
|
||||
<td class="text-end text-primary fw-semibold">@m.TaxBilled.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.TotalTaxableSales.ToString("C")</td>
|
||||
<td class="text-end text-primary">@Model.TotalTaxBilled.ToString("C")</td>
|
||||
<td class="text-center">@Model.TaxableInvoiceCount</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- By Tax Account -->
|
||||
@if (Model.ByAccount.Any())
|
||||
{
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header fw-semibold"><i class="bi bi-bookmark me-1"></i>By Tax Account</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Account</th>
|
||||
<th class="text-end">Taxable Sales</th>
|
||||
<th class="text-end">Tax Billed</th>
|
||||
<th class="text-center">Invoices</th>
|
||||
<th class="text-end no-print">Effective Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var a in Model.ByAccount)
|
||||
{
|
||||
var rate = a.TaxableSales == 0 ? 0m : Math.Round(a.TaxBilled / a.TaxableSales * 100, 2);
|
||||
<tr>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(a.AccountNumber))
|
||||
{
|
||||
<span class="text-muted small me-1">@a.AccountNumber</span>
|
||||
}
|
||||
@a.AccountName
|
||||
</td>
|
||||
<td class="text-end">@a.TaxableSales.ToString("C")</td>
|
||||
<td class="text-end fw-semibold text-primary">@a.TaxBilled.ToString("C")</td>
|
||||
<td class="text-center text-muted small">@a.InvoiceCount</td>
|
||||
<td class="text-end text-muted small no-print">@rate.ToString("F2")%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-semibold">
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td class="text-end">@Model.TotalTaxableSales.ToString("C")</td>
|
||||
<td class="text-end text-primary">@Model.TotalTaxBilled.ToString("C")</td>
|
||||
<td class="text-center">@Model.TaxableInvoiceCount</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>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary">@(Model.TaxableInvoiceCount + Model.NonTaxableInvoiceCount) invoices</span>
|
||||
<span class="badge bg-light text-muted border">
|
||||
<span class="d-inline-block me-1" style="width:10px;height:10px;background:#f8f9fa;border:1px solid #dee2e6"></span>
|
||||
Non-taxable rows shaded
|
||||
</span>
|
||||
</div>
|
||||
</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>Status</th>
|
||||
<th class="text-end">Subtotal</th>
|
||||
<th class="text-end">Tax %</th>
|
||||
<th class="text-end">Tax Amount</th>
|
||||
<th class="text-end">Total</th>
|
||||
<th class="text-end no-print">Paid</th>
|
||||
<th>Tax Account</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var inv in Model.Invoices)
|
||||
{
|
||||
bool isTaxable = inv.TaxAmount > 0;
|
||||
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 class="@(!isTaxable ? "row-nontaxable" : "")">
|
||||
<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="small">@inv.CustomerName</td>
|
||||
<td class="small text-muted">@inv.InvoiceDate.ToString("MM/dd/yyyy")</td>
|
||||
<td><span class="badge @statusBadge">@inv.Status</span></td>
|
||||
<td class="text-end">@inv.SubTotal.ToString("C")</td>
|
||||
<td class="text-end text-muted small">@(isTaxable ? inv.TaxPercent.ToString("F2") + "%" : "—")</td>
|
||||
<td class="text-end @(isTaxable ? "fw-semibold text-primary" : "text-muted")">@(isTaxable ? inv.TaxAmount.ToString("C") : "—")</td>
|
||||
<td class="text-end fw-semibold">@inv.Total.ToString("C")</td>
|
||||
<td class="text-end text-success no-print">@inv.AmountPaid.ToString("C")</td>
|
||||
<td class="small text-muted">@(string.IsNullOrEmpty(inv.TaxAccountName) ? "—" : inv.TaxAccountName)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-light fw-semibold">
|
||||
<tr>
|
||||
<td colspan="4">Totals</td>
|
||||
<td class="text-end">@(Model.TotalTaxableSales + Model.TotalNonTaxableSales).ToString("C")</td>
|
||||
<td></td>
|
||||
<td class="text-end text-primary">@Model.TotalTaxBilled.ToString("C")</td>
|
||||
<td class="text-end">@Model.Invoices.Sum(i => i.Total).ToString("C")</td>
|
||||
<td class="text-end text-success no-print">@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") · Invoice basis — tax liability is recognized when invoiced, not when collected. Excludes Draft and Voided invoices.
|
||||
</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('taxTrendChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(monthLabels)),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Taxable Sales',
|
||||
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(monthTaxable)),
|
||||
backgroundColor: 'rgba(79,70,229,0.5)',
|
||||
borderRadius: 4,
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
label: 'Tax Billed',
|
||||
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(monthTaxBilled)),
|
||||
type: 'line',
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59,130,246,0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 4,
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
order: 1,
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
},
|
||||
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 }, title: { display: true, text: 'Taxable Sales', color: textColor } },
|
||||
y1: { ticks: { color: textColor, callback: v => '$' + v.toLocaleString() }, grid: { display: false }, position: 'right', title: { display: true, text: 'Tax Billed', color: textColor } },
|
||||
x: { ticks: { color: textColor }, grid: { display: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user