Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,237 @@
@model PowderCoating.Web.ViewModels.OnlinePaymentsViewModel
@{
ViewData["Title"] = "Online Payment Reconciliation";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0"><i class="bi bi-credit-card me-2 text-primary"></i>Online Payment Reconciliation</h2>
<small class="text-muted">Stripe Connect payments, refunds, and chargebacks</small>
</div>
</div>
@* Date range filter *@
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<form method="get" class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label mb-1 small fw-semibold">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 mb-1 small fw-semibold">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-search me-1"></i>Filter
</button>
</div>
</form>
</div>
</div>
@* Summary cards *@
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small mb-1">Total Collected Online</div>
<div class="fs-4 fw-bold text-success">@Model.TotalCollected.ToString("C")</div>
<div class="text-muted small">@Model.Invoices.Count invoice(s)</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small mb-1">Total Refunded</div>
<div class="fs-4 fw-bold text-danger">@Model.TotalRefunded.ToString("C")</div>
<div class="text-muted small">@Model.Refunds.Count refund(s)</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small mb-1">Net Revenue</div>
<div class="fs-4 fw-bold @(Model.NetRevenue >= 0 ? "text-success" : "text-danger")">
@Model.NetRevenue.ToString("C")
</div>
<div class="text-muted small">After refunds</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="text-muted small mb-1">Surcharges Collected</div>
<div class="fs-4 fw-bold text-secondary">@Model.SurchargesCollected.ToString("C")</div>
<div class="text-muted small">Convenience fees</div>
</div>
</div>
</div>
</div>
@* Online Payments Table *@
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-semibold"><i class="bi bi-check-circle me-2 text-success"></i>Payments Received</h6>
<span class="badge bg-success-subtle text-success">@Model.Invoices.Count</span>
</div>
<div class="card-body p-0">
@if (!Model.Invoices.Any())
{
<div class="text-center text-muted py-4">No online payments in this period.</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Invoice #</th>
<th>Customer</th>
<th class="text-end">Gross Collected</th>
<th class="text-end">Surcharge</th>
<th class="text-end">Net</th>
<th>Date</th>
<th>Status</th>
<th>Payment Intent</th>
</tr>
</thead>
<tbody>
@foreach (var inv in Model.Invoices)
{
var net = inv.OnlineAmountPaid;
var gross = inv.OnlineAmountPaid + inv.OnlineSurchargeCollected;
var dateDisplay = inv.PaidDate.HasValue ? inv.PaidDate.Value.ToString("MMM d, yyyy") : (inv.UpdatedAt?.ToString("MMM d, yyyy") ?? "—");
var statusClass = inv.OnlinePaymentStatus switch
{
PowderCoating.Core.Enums.OnlinePaymentStatus.Paid => "bg-success-subtle text-success",
PowderCoating.Core.Enums.OnlinePaymentStatus.PartiallyPaid => "bg-warning-subtle text-warning",
PowderCoating.Core.Enums.OnlinePaymentStatus.Refunded => "bg-danger-subtle text-danger",
_ => "bg-secondary-subtle text-secondary"
};
var customerName = inv.Customer != null
? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
: "—";
<tr>
<td>
<a asp-action="Details" asp-route-id="@inv.Id" class="fw-semibold text-decoration-none">
@inv.InvoiceNumber
</a>
</td>
<td>@customerName</td>
<td class="text-end">@gross.ToString("C")</td>
<td class="text-end text-muted">@inv.OnlineSurchargeCollected.ToString("C")</td>
<td class="text-end fw-semibold">@net.ToString("C")</td>
<td>@dateDisplay</td>
<td><span class="badge @statusClass">@inv.OnlinePaymentStatus</span></td>
<td>
@if (!string.IsNullOrEmpty(inv.StripePaymentIntentId))
{
<code class="small" title="@inv.StripePaymentIntentId">
@inv.StripePaymentIntentId[..Math.Min(20, inv.StripePaymentIntentId.Length)]…
</code>
}
</td>
</tr>
}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td colspan="2">Totals</td>
<td class="text-end">@((Model.TotalCollected + Model.SurchargesCollected).ToString("C"))</td>
<td class="text-end">@Model.SurchargesCollected.ToString("C")</td>
<td class="text-end">@Model.TotalCollected.ToString("C")</td>
<td colspan="3"></td>
</tr>
</tfoot>
</table>
</div>
}
</div>
</div>
@* Refunds Table *@
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-semibold"><i class="bi bi-arrow-counterclockwise me-2 text-danger"></i>Refunds & Chargebacks</h6>
<span class="badge bg-danger-subtle text-danger">@Model.Refunds.Count</span>
</div>
<div class="card-body p-0">
@if (!Model.Refunds.Any())
{
<div class="text-center text-muted py-4">No refunds in this period.</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Invoice #</th>
<th>Customer</th>
<th class="text-end">Amount</th>
<th>Date</th>
<th>Status</th>
<th>Stripe Reference</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
@foreach (var r in Model.Refunds)
{
var invNum = r.Invoice?.InvoiceNumber ?? "—";
var invId = r.Invoice?.Id;
var cust = r.Invoice?.Customer;
var custName = cust != null
? (cust.CompanyName ?? $"{cust.ContactFirstName} {cust.ContactLastName}".Trim())
: "—";
var statusClass = r.Status switch
{
PowderCoating.Core.Enums.RefundStatus.Issued => "bg-success-subtle text-success",
PowderCoating.Core.Enums.RefundStatus.Pending => "bg-warning-subtle text-warning",
PowderCoating.Core.Enums.RefundStatus.Cancelled => "bg-secondary-subtle text-secondary",
_ => "bg-secondary-subtle text-secondary"
};
<tr>
<td>
@if (invId.HasValue)
{
<a asp-action="Details" asp-route-id="@invId" class="fw-semibold text-decoration-none">@invNum</a>
}
else
{
@invNum
}
</td>
<td>@custName</td>
<td class="text-end text-danger fw-semibold">@r.Amount.ToString("C")</td>
<td>@r.RefundDate.ToString("MMM d, yyyy")</td>
<td><span class="badge @statusClass">@r.Status</span></td>
<td>
@if (!string.IsNullOrEmpty(r.Reference))
{
<code class="small" title="@r.Reference">@r.Reference[..Math.Min(24, r.Reference.Length)]</code>
}
</td>
<td class="text-muted small">@r.Reason</td>
</tr>
}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td colspan="2">Total Refunded</td>
<td class="text-end text-danger">@Model.TotalRefunded.ToString("C")</td>
<td colspan="4"></td>
</tr>
</tfoot>
</table>
</div>
}
</div>
</div>