Initial commit
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user