Initial commit
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
@using PowderCoating.Core.Entities
|
||||
@model StripeWebhookEvent
|
||||
@{
|
||||
ViewData["Title"] = $"Webhook Event – {Model.EventId}";
|
||||
var statusClass = Model.Status switch
|
||||
{
|
||||
StripeWebhookEventStatus.Processed => "success",
|
||||
StripeWebhookEventStatus.Failed => "danger",
|
||||
StripeWebhookEventStatus.Ignored => "secondary",
|
||||
_ => "warning"
|
||||
};
|
||||
}
|
||||
|
||||
<div class="container-fluid py-3" style="max-width:960px">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-stripe me-2 text-primary"></i>Webhook Event
|
||||
</h4>
|
||||
<span class="badge bg-@statusClass fs-6">@Model.Status</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold py-2">Event Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 small">
|
||||
<dt class="col-5 text-muted">Event ID</dt>
|
||||
<dd class="col-7 font-monospace">@Model.EventId</dd>
|
||||
|
||||
<dt class="col-5 text-muted">Type</dt>
|
||||
<dd class="col-7"><span class="badge bg-light text-dark border">@Model.EventType</span></dd>
|
||||
|
||||
<dt class="col-5 text-muted">Company ID</dt>
|
||||
<dd class="col-7">@(Model.CompanyId.HasValue ? Model.CompanyId.ToString() : "—")</dd>
|
||||
|
||||
<dt class="col-5 text-muted">Status</dt>
|
||||
<dd class="col-7"><span class="badge bg-@statusClass">@Model.Status</span></dd>
|
||||
|
||||
<dt class="col-5 text-muted">Received At</dt>
|
||||
<dd class="col-7">@Model.ReceivedAt.ToString("MM/dd/yyyy HH:mm:ss") UTC</dd>
|
||||
|
||||
<dt class="col-5 text-muted">Processed At</dt>
|
||||
<dd class="col-7">@(Model.ProcessedAt.HasValue ? Model.ProcessedAt.Value.ToString("MM/dd/yyyy HH:mm:ss") + " UTC" : "—")</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm h-100 border-danger">
|
||||
<div class="card-header fw-semibold py-2 text-danger">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>Error
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="small mb-0 text-danger">@Model.ErrorMessage</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<span class="fw-semibold">Raw Payload</span>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="copyJson()">
|
||||
<i class="bi bi-clipboard me-1"></i>Copy
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<pre id="json-payload" class="mb-0 p-3 small" style="background:#1e1e1e;color:#d4d4d4;max-height:600px;overflow-y:auto;border-radius:0 0 .375rem .375rem">@ViewBag.FormattedJson</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function copyJson() {
|
||||
const text = document.getElementById('json-payload').textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const btn = event.currentTarget;
|
||||
btn.innerHTML = '<i class="bi bi-check me-1"></i>Copied';
|
||||
setTimeout(() => btn.innerHTML = '<i class="bi bi-clipboard me-1"></i>Copy', 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
@using PowderCoating.Core.Entities
|
||||
@model List<StripeWebhookEvent>
|
||||
@{
|
||||
ViewData["Title"] = "Stripe Webhook Events";
|
||||
int page = (int)ViewBag.Page;
|
||||
int totalPages = (int)ViewBag.TotalPages;
|
||||
int filteredCount = (int)ViewBag.FilteredCount;
|
||||
int pageSize = (int)ViewBag.PageSize;
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
[data-bs-theme="dark"] a.text-dark { color: var(--bs-body-color) !important; }
|
||||
[data-bs-theme="dark"] .badge.bg-light { background-color: var(--bs-secondary-bg) !important; color: var(--bs-body-color) !important; }
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-stripe me-2 text-primary"></i>Stripe Webhook Events
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@* Summary cards *@
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-primary">@ViewBag.TotalCount.ToString("N0")</div>
|
||||
<div class="small text-muted">Total Received</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-success">@ViewBag.ProcessedCount.ToString("N0")</div>
|
||||
<div class="small text-muted">Processed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-danger">@ViewBag.FailedCount.ToString("N0")</div>
|
||||
<div class="small text-muted">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm">
|
||||
<div class="card-body py-2">
|
||||
<div class="fs-4 fw-bold text-info">@ViewBag.Last24hCount.ToString("N0")</div>
|
||||
<div class="small text-muted">Last 24 Hours</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Filters *@
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label small mb-1">Event Type</label>
|
||||
<select name="eventType" class="form-select form-select-sm">
|
||||
<option value="">All Types</option>
|
||||
@foreach (var t in (List<string>)ViewBag.EventTypes)
|
||||
{
|
||||
<option value="@t" selected="@(ViewBag.EventTypeFilter == t)">@t</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label small mb-1">Status</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="">All Statuses</option>
|
||||
@foreach (var s in Enum.GetValues<StripeWebhookEventStatus>())
|
||||
{
|
||||
<option value="@s" selected="@(ViewBag.StatusFilter == s.ToString())">@s</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label small mb-1">From</label>
|
||||
<input type="date" name="from" class="form-control form-control-sm" value="@ViewBag.From" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label small mb-1">To</label>
|
||||
<input type="date" name="to" class="form-control form-control-sm" value="@ViewBag.To" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label small mb-1">Search ID / Error</label>
|
||||
<input type="text" name="search" class="form-control form-control-sm" value="@ViewBag.Search" placeholder="evt_..." />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Filter</button>
|
||||
<a href="@Url.Action("Index")" class="btn btn-outline-secondary btn-sm ms-1">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Results *@
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<span class="small fw-semibold">@filteredCount.ToString("N0") event(s)</span>
|
||||
<select name="pageSize" class="form-select form-select-sm w-auto" onchange="changePageSize(this.value)">
|
||||
<option value="25" selected="@(pageSize == 25)">25 per page</option>
|
||||
<option value="50" selected="@(pageSize == 50)">50 per page</option>
|
||||
<option value="100" selected="@(pageSize == 100)">100 per page</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Received</th>
|
||||
<th>Event ID</th>
|
||||
<th>Type</th>
|
||||
<th>Company</th>
|
||||
<th>Status</th>
|
||||
<th>Processed</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">No webhook events found.</td></tr>
|
||||
}
|
||||
@foreach (var evt in Model)
|
||||
{
|
||||
var statusClass = evt.Status switch
|
||||
{
|
||||
StripeWebhookEventStatus.Processed => "success",
|
||||
StripeWebhookEventStatus.Failed => "danger",
|
||||
StripeWebhookEventStatus.Ignored => "secondary",
|
||||
_ => "warning"
|
||||
};
|
||||
<tr>
|
||||
<td class="small">@evt.ReceivedAt.ToString("MM/dd/yy HH:mm:ss")</td>
|
||||
<td class="small font-monospace text-truncate" style="max-width:160px">@evt.EventId</td>
|
||||
<td class="small">
|
||||
<span class="badge bg-secondary-subtle text-body border">@evt.EventType</span>
|
||||
</td>
|
||||
<td class="small">@(evt.CompanyId.HasValue ? $"#{evt.CompanyId}" : "—")</td>
|
||||
<td>
|
||||
<span class="badge bg-@statusClass">@evt.Status</span>
|
||||
</td>
|
||||
<td class="small">
|
||||
@(evt.ProcessedAt.HasValue ? evt.ProcessedAt.Value.ToString("HH:mm:ss") : "—")
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="Details" asp-route-id="@evt.Id" class="btn btn-outline-secondary btn-sm py-0">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Mobile card view — shown on screens < 992px -->
|
||||
<div class="mobile-card-view">
|
||||
<div class="mobile-card-list">
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<p class="text-center text-muted py-4">No webhook events found.</p>
|
||||
}
|
||||
@foreach (var evt in Model)
|
||||
{
|
||||
var statusClass = evt.Status switch
|
||||
{
|
||||
StripeWebhookEventStatus.Processed => "success",
|
||||
StripeWebhookEventStatus.Failed => "danger",
|
||||
StripeWebhookEventStatus.Ignored => "secondary",
|
||||
_ => "warning"
|
||||
};
|
||||
<a asp-action="Details" asp-route-id="@evt.Id" class="mobile-data-card text-decoration-none">
|
||||
<div class="mobile-card-header">
|
||||
<div class="mobile-card-icon bg-primary"><i class="bi bi-lightning-charge"></i></div>
|
||||
<div class="mobile-card-title">
|
||||
<h6>@evt.EventType</h6>
|
||||
<small>@evt.ReceivedAt.ToString("MM/dd/yy HH:mm:ss")</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Status</span>
|
||||
<span class="mobile-card-value"><span class="badge bg-@statusClass">@evt.Status</span></span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Company</span>
|
||||
<span class="mobile-card-value">@(evt.CompanyId.HasValue ? $"#{evt.CompanyId}" : "—")</span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Event ID</span>
|
||||
<span class="mobile-card-value text-muted small font-monospace">@(evt.EventId?.Length > 20 ? evt.EventId.Substring(0, 20) + "…" : evt.EventId)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<span class="btn btn-sm btn-outline-secondary">View →</span>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (totalPages > 1)
|
||||
{
|
||||
<div class="card-footer d-flex justify-content-between align-items-center py-2">
|
||||
<span class="small text-muted">Page @(page) of @(totalPages)</span>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
@if (page > 1)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="@Url.Action("Index", new { page = page - 1, pageSize, eventType = ViewBag.EventTypeFilter, status = ViewBag.StatusFilter, search = ViewBag.Search, from = ViewBag.From, to = ViewBag.To })">‹</a>
|
||||
</li>
|
||||
}
|
||||
@if (page < totalPages)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="@Url.Action("Index", new { page = page + 1, pageSize, eventType = ViewBag.EventTypeFilter, status = ViewBag.StatusFilter, search = ViewBag.Search, from = ViewBag.From, to = ViewBag.To })">›</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function changePageSize(size) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('pageSize', size);
|
||||
url.searchParams.set('page', '1');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user