diff --git a/ShopAdmin/Components/Pages/Notifications.razor b/ShopAdmin/Components/Pages/Notifications.razor new file mode 100644 index 0000000..bc8f7f3 --- /dev/null +++ b/ShopAdmin/Components/Pages/Notifications.razor @@ -0,0 +1,256 @@ +@page "/notifications" +@using Blazored.Toast +@using Microsoft.AspNetCore.Components.QuickGrid +@rendermode RenderMode.InteractiveServer + +Notifications | Shop Console + +@if (NotificationQueryable == null && IsLoading) +{ +
+
+ +

Initializing Operational Console registers...

+
+
+} + +
+ + + +
+ +
+
+ Operational Inventory + + + + + + + + + + + + + + + + + + + + + + + + + +
Metric ItemTelemetry NodeRegister
Total Inbound VolumeCore-System@TotalCount
Operational FailuresFault-Engine@ErrorCount
Pending DispatchesQueue-Worker@PendingCount
+
+ +
+ Health Efficiency +
+
+ + + + +
@SuccessRate%
+
+
+ + + + +
@((TotalCount > 0) ? Math.Round(((double)PendingCount / TotalCount) * 100) : 0)%
+
+
+
+
+ +
+ Telemetry Filters + +
+
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ Operational Load Density Timeline +
+ + + + + + + + + + +
+ 04/17 + 04/24 + 05/01 + 05/08 + 05/17 +
+
+
+ +
+ + @if (NotificationQueryable == null && IsLoading) + { +
+
+
+
+
+
+

Syncing system log registers...

+
+
+ } + else + { +
+ + @if (IsLoading) + { +
+
+ +

Syncing system log registers...

+
+
+ } + + + + +
+ @context.Platform + @context.Direction +
+
+ + + + @context.Priority + + + + + + +
+ @(string.IsNullOrWhiteSpace(context.SenderName) ? "System Core" : context.SenderName) + @context.SenderAddress +
+
+ + +
+ @context.Subject + @if (!string.IsNullOrWhiteSpace(context.Message)) + { + @context.Message + } +
+
+ + +
+ @context.RecipientName + @context.RecipientAddress +
+
+ + + @if (context.HasError) + { + ✕ Error + } + else if (context.Processed) + { + ✓ Processed + } + else + { + ⚙ Pending + } + + + + @if (context.HasError) + { + + } + else + { + @((MarkupString)" ") + } + + +
+
+ +
+ +
+ } +
+ + \ No newline at end of file diff --git a/ShopAdmin/Components/Pages/Notifications.razor.cs b/ShopAdmin/Components/Pages/Notifications.razor.cs new file mode 100644 index 0000000..edc5503 --- /dev/null +++ b/ShopAdmin/Components/Pages/Notifications.razor.cs @@ -0,0 +1,144 @@ +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Notifications; +using LiteCharms.Features.Shop.Notifications.Models; + +namespace ShopAdmin.Components.Pages; + +public partial class Notifications(NotificationService notificationService, IToastService ToastService) +{ + protected IQueryable NotificationQueryable + { + get + { + var query = NotificationItems.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(SearchFilter)) + { + query = query.Where(n => + (n.Subject != null && n.Subject.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)) || + (n.SenderAddress != null && n.SenderAddress.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)) || + (n.RecipientAddress != null && n.RecipientAddress.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase))); + } + + return query; + } + } + + protected List NotificationItems { get; set; } = []; + + protected PaginationState Pagination { get; set; } = new() { ItemsPerPage = 10 }; + + protected bool IsLoading { get; set; } = true; + + protected string SearchFilter { get; set; } = string.Empty; + + protected int MaxRecords { get; set; } = 100; + + protected DateTime FromDate { get; set; } = DateTime.Today.AddDays(-30); + + protected DateTime ToDate { get; set; } = DateTime.Today.AddDays(1); + + protected string FromDateBind + { + get => FromDate.ToString("yyyy-MM-dd"); + set + { + if (DateTime.TryParse(value, out var parsedDate)) + { + FromDate = parsedDate; + } + } + } + + protected string ToDateBind + { + get => ToDate.ToString("yyyy-MM-dd"); + set + { + if (DateTime.TryParse(value, out var parsedDate)) + { + ToDate = parsedDate; + } + } + } + + protected int TotalCount => NotificationItems.Count; + + protected int ErrorCount => NotificationItems.Count(n => n.HasError); + + protected int PendingCount => NotificationItems.Count(n => !n.Processed && !n.HasError); + + protected double SuccessRate => TotalCount > 0 + ? Math.Round(((double)(TotalCount - ErrorCount) / TotalCount) * 100, 1) + : 100.0; + + protected GridSort SortByOrigin => GridSort.ByAscending(n => n.Platform); + protected GridSort SortByPriority => GridSort.ByDescending(n => n.Priority); + protected GridSort SortByCreatedAt => GridSort.ByDescending(n => n.CreatedAt); + protected GridSort SortBySender => GridSort.ByAscending(n => n.SenderAddress); + protected GridSort SortBySubject => GridSort.ByAscending(n => n.Subject); + protected GridSort SortByRecipient => GridSort.ByAscending(n => n.RecipientAddress); + protected GridSort SortByStatus => GridSort.ByAscending(n => n.HasError); + + protected override async Task OnInitializedAsync() => await LoadNotificationDataAsync(); + + protected async Task HandleRefreshClickAsync() + { + await LoadNotificationDataAsync(); + + ToastService.ShowInfo("Console metrics and telemetry logs refreshed."); + } + + private async Task LoadNotificationDataAsync() + { + IsLoading = true; + + var dateRange = new DateRange() + { + From = DateOnly.FromDateTime(FromDate), + To = DateOnly.FromDateTime(ToDate), + MaxRecords = MaxRecords + }; + + var result = await notificationService.GetNotificationsAsync(dateRange); + + if (result.IsSuccess) NotificationItems = [.. result.Value]; + + if (result.IsFailed) + { + var errorMsg = result.Errors.FirstOrDefault()?.Message ?? "Unable to load telemetry log data."; + + ToastService.ShowError(errorMsg); + + NotificationItems = []; + } + + IsLoading = false; + } + + protected string GetRowClass(Notification item) + { + var errorStatus = item.HasError ? "row-has-error" : "row-success"; + var priorityStatus = $"row-priority-{item.Priority.ToString().ToLower()}"; + + return $"{errorStatus} {priorityStatus}"; + } + + protected async Task HandleRetryRowAsync(Notification item) + { + var updateRequest = new UpdateNotification + { + NotificationId = item.Id, + Processed = false, + HasError = false, + Errors = null + }; + + var result = await notificationService.UpdateNotificationAsync(updateRequest); + + await LoadNotificationDataAsync(); + + if (result.IsSuccess) ToastService.ShowSuccess($"{item.Subject}, has been marked for reprocessing."); + if (result.IsFailed) ToastService.ShowError("Failed to update notification state execution."); + } +} \ No newline at end of file diff --git a/ShopAdmin/Components/Pages/Notifications.razor.css b/ShopAdmin/Components/Pages/Notifications.razor.css new file mode 100644 index 0000000..57a9868 --- /dev/null +++ b/ShopAdmin/Components/Pages/Notifications.razor.css @@ -0,0 +1,688 @@ +/* ========================================================================== + 1. WORKSPACE LAYOUT & LAYOUT ENGINE BOUNDS + ========================================================================== */ + +.console-workspace { + max-width: 1240px !important; + width: 100% !important; + margin: 0 auto; + padding: 2rem 1.5rem; + box-sizing: border-box; +} + +.dashboard-control-deck { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; + width: 100%; +} + +@media (min-width: 1024px) { + .dashboard-control-deck { + /* Splitting into explicit proportional grid columns */ + grid-template-columns: 1.6fr 1fr; + } + + /* Force the timeline block to span both columns underneath them */ + .full-width-row { + grid-column: span 2; + } +} + +.charts-and-gauges-block { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.top-row-panels { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.25rem; +} + +/* Shared structural foundation for dashboard panels */ +.mini-table-card, +.radial-gauge-card, +.trend-spline-card, +.filter-panel { + padding: 1.25rem; + display: flex; + flex-direction: column; +} + +/* ========================================================================== + 2. TELEMETRY FILTERS PANEL & INPUT FORM STRUCTURES + ========================================================================== */ + +.filter-panel { + height: auto; + display: flex; + flex-direction: column; + /* Fixed selector to accurately target nested wrapper structure input elements */ + input[type="text"], + .input-wrapper input { + width: 100%; + height: 38px; + padding: 10px 12px !important; + background: rgba(0, 0, 0, 0.4) !important; + border: 1px solid rgba(255, 255, 255, 0.05) !important; + color: #fff !important; + border-radius: 6px !important; + font-size: 0.85rem !important; + outline: none; + box-sizing: border-box; + } + + input:focus, + .input-wrapper input:focus { + border-color: #ff6b35 !important; + box-shadow: 0 0 10px rgba(255, 107, 53, 0.25); + } +} + +.filter-grid-form { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + align-content: start; /* Prevents vertical item stretching */ +} + +.btn-apply-filters { + background: #ff6b35; + border: none; + color: #fff; + width: 100%; /* Stretches nicely on small screens, or adjust to max-content if preferred */ + height: 38px; + border-radius: 6px; + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + transition: background 0.2s ease; + + &:hover:not(:disabled) { + background: #ff8552; + } +} + +.themed-dropdown-box, +.themed-date-picker-box { + position: relative; + width: 100%; + + select, input[type="date"] { + width: 100%; + height: 38px; + padding: 9px 12px !important; + background: rgba(28, 32, 43, 0.85) !important; + border: 1px solid rgba(255, 107, 53, 0.15) !important; + color: #e2e8f0 !important; + border-radius: 6px !important; + font-size: 0.85rem !important; + outline: none; + appearance: none; + -webkit-appearance: none; + box-sizing: border-box; + cursor: pointer; + transition: all 0.2s ease; + + &:focus { + border-color: #ff6b35 !important; + box-shadow: 0 0 10px rgba(255, 107, 53, 0.25); + } + } +} + + .themed-date-picker-box input[type="date"]::-webkit-calendar-picker-indicator { + filter: invert(53%) sepia(86%) saturate(1637%) hue-rotate(345deg) brightness(101%) contrast(101%); + cursor: pointer; + opacity: 0.85; + } + +.themed-dropdown-box { + select option { + background-color: #141821 !important; + color: #fff !important; + padding: 8px !important; + } + + &::after { + content: "▼"; + font-size: 0.65rem; + color: #ff6b35; + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + } +} + +.action-wrapper { + justify-content: flex-end; +} + +.btn-apply-filters { + background: #ff6b35; + border: none; + color: #fff; + width: 100%; + height: 38px; + border-radius: 6px; + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + transition: background 0.2s ease; + + &:hover:not(:disabled) { + background: #ff8552; + } +} + +/* ========================================================================== + 3. INTERNAL MICRO-TABLE COMPONENTS (OPERATIONAL INVENTORY) + ========================================================================== */ + +.dashboard-mini-table { + width: 100%; + margin-top: 0.75rem; + border-collapse: collapse; + + th { + text-align: left; + font-size: 0.7rem; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.3); + padding: 6px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + + td { + padding: 10px 0; + font-size: 0.85rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + } +} + +/* ========================================================================== + 4. DATA VISUALIZATION COMPONENTS (GAUGES & TIMELINES) + ========================================================================== */ + +.gauge-presentation-box { + display: flex; + justify-content: space-around; + align-items: center; + flex: 1; + margin-top: 0.5rem; +} + +.svg-gauge-container { + position: relative; + width: 75px; + height: 75px; +} + +.gauge-ring-matrix { + width: 100%; + height: 100%; +} + +.gauge-bg-track { + fill: none; + stroke: rgba(255, 255, 255, 0.04); + stroke-width: 2.8; +} + +.gauge-active-fill { + fill: none; + stroke-width: 3.2; + stroke-linecap: round; + transition: stroke-dasharray 0.4s ease; +} + +.gauge-center-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: monospace; + font-weight: 700; + font-size: 0.9rem; +} + +.vector-chart-viewport { + width: 100%; + margin-top: 1rem; + position: relative; +} + +.area-spline-graph { + width: 100%; + height: 85px; + display: block; + overflow: visible; +} + +.chart-axis-labels { + display: flex; + justify-content: space-between; + font-size: 0.68rem; + font-family: monospace; + color: rgba(255, 255, 255, 0.25); + margin-top: 0.5rem; +} + +/* ========================================================================== + 5. BLAZOR QUICKGRID COMPLEX ENHANCEMENTS (SCOPED FIX) + ========================================================================== */ + +.full-width-table { + width: 100% !important; + overflow-x: auto; + margin-top: 1rem; + box-sizing: border-box; + background: rgba(13, 16, 23, 0.65); + border: 1px solid rgba(255, 255, 255, 0.03); + border-radius: 8px; +} + + /* --- QuickGrid Empty Body Row Elimination (Header Safe) --- */ + .full-width-table ::deep tbody tr:has(td:empty) { + display: none !important; + } + + /* Forces the table structure to hug only active data rows */ + .full-width-table ::deep tbody { + display: table-row-group !important; + height: auto !important; + } + + /* Properly combining parent scope with deep combinator to force child table styles */ + .full-width-table ::deep table { + width: 100% !important; + table-layout: fixed !important; /* Forces layout engine to honor assigned column percentages */ + border-collapse: collapse !important; + } + + /* --- High Contrast Header Component Overrides --- */ + .full-width-table ::deep th { + background: #141821 !important; /* Solid contrast block background */ + padding: 14px 10px !important; + border-bottom: 2px solid #ff6b35 !important; /* Solid brand accent line */ + vertical-align: middle !important; + } + + /* Target QuickGrid's native internal header sorting buttons & plain titles */ + .full-width-table ::deep th button, + .full-width-table ::deep th .col-title { + color: #ff6b35 !important; /* Vibrant orange contrast text */ + font-weight: 700 !important; + font-size: 0.75rem !important; /* Slightly optimized down for layout safety */ + text-transform: uppercase !important; + letter-spacing: 1px !important; + background: transparent !important; + border: none !important; + padding: 0 !important; + text-align: left !important; + } + + .full-width-table ::deep th button:hover { + color: #ffffff !important; /* Highlight on hover interaction */ + } + + /* --- Explicit Column Width Definitions (Optimized for Date Visibility) --- */ + /* Origin */ + .full-width-table ::deep th:nth-child(1), .full-width-table ::deep td:nth-child(1) { + width: 9%; + min-width: 85px; + } + /* Priority */ + .full-width-table ::deep th:nth-child(2), .full-width-table ::deep td:nth-child(2) { + width: 8%; + min-width: 75px; + } + /* Created At (Given extra breathing space) */ + .full-width-table ::deep th:nth-child(3), .full-width-table ::deep td:nth-child(3) { + width: 16%; + min-width: 145px; + } + /* Sender */ + .full-width-table ::deep th:nth-child(4), .full-width-table ::deep td:nth-child(4) { + width: 20%; + min-width: 190px; + } + /* Subject / Description */ + .full-width-table ::deep th:nth-child(5), .full-width-table ::deep td:nth-child(5) { + width: 22%; + min-width: 200px; + } + /* Recipient */ + .full-width-table ::deep th:nth-child(6), .full-width-table ::deep td:nth-child(6) { + width: 15%; + min-width: 150px; + } + /* Status */ + .full-width-table ::deep th:nth-child(7), .full-width-table ::deep td:nth-child(7) { + width: 10%; + min-width: 90px; + } + /* Action */ + .full-width-table ::deep th:nth-child(8), .full-width-table ::deep td:nth-child(8) { + width: 5%; + min-width: 65px; + } + + /* --- Body Cells Engine & Text Preservation --- */ + .full-width-table ::deep td { + padding: 12px 10px !important; /* Tighter padding values to maximize text viewport bounds */ + border-bottom: 1px solid rgba(255, 255, 255, 0.04) !important; + vertical-align: middle !important; + font-size: 0.78rem !important; /* Dropped size down slightly from 0.85rem to fix clipping */ + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .full-width-table ::deep tr:hover td { + background: rgba(255, 255, 255, 0.02) !important; + } + +/* ========================================================================== + 6. ATOMIC INTERACTION & GLOBAL STATUS DESIGN + ========================================================================== */ + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.shadow-card { + background: rgba(13, 16, 23, 0.65) !important; + border: 1px solid rgba(255, 255, 255, 0.03) !important; + box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.6); + backdrop-filter: blur(12px); + border-radius: 8px; +} + +.btn-refresh { + background: rgba(255, 107, 53, 0.06); + border: 1px solid rgba(255, 107, 53, 0.25); + color: #ff6b35; + padding: 10px 18px; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + + &:hover:not(:disabled) { + background: rgba(255, 107, 53, 0.12); + border-color: #ff6b35; + color: #fff; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.origin-composite, +.contact-composite, +.subject-cell { + display: flex; + flex-direction: column; + line-height: 1.3; + overflow: hidden; +} + +/* Typography Classes */ +.panel-title-lbl { + font-size: 0.72rem; + color: rgba(255, 255, 255, 0.4); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.75px; +} + +.platform-lbl { + font-weight: 600; + color: #fff; + font-size: 0.85rem; +} + +.direction-lbl { + font-size: 0.75rem; + font-family: monospace; +} + +.node-dim { + color: rgba(255, 255, 255, 0.4); + font-size: 0.78rem; +} + +.text-mono { + font-family: monospace; + font-weight: 700; + font-size: 0.95rem; +} + +.contact-name { + color: #fff; + font-size: 0.85rem; + font-weight: 500; + text-overflow: ellipsis; + overflow: hidden; +} + +.contact-email, .message-subtext { + font-size: 0.75rem; + text-overflow: ellipsis; + overflow: hidden; +} + +.subject-text { + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + text-overflow: ellipsis; + overflow: hidden; +} + +/* Status Color Tokens */ +.text-purple { + color: #bd00ff; + stroke: #bd00ff; +} + +.text-cyan { + color: #00b4d8; + stroke: #00b4d8; +} + +.text-red { + color: #e63946; +} + +.text-yellow { + color: #ffb703; +} + +.text-emerald { + color: #2a9d8f; +} + +.status-indicator { + font-size: 0.85rem; + font-weight: 500; +} + +.success-text { + color: #2a9d8f; +} + +.alert-text { + color: #e63946; +} + +.idle-text { + color: #ffb703; +} + +/* Priority Badge Elements */ +.priority-tag { + font-size: 0.68rem; + font-weight: 700; + font-family: monospace; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + width: max-content; + + &.critical { + background: rgba(230, 57, 70, 0.12); + color: #e63946; + } + + &.high { + background: rgba(255, 183, 3, 0.12); + color: #ffb703; + } + + &.medium { + background: rgba(0, 180, 216, 0.12); + color: #00b4d8; + } + + &.low { + background: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.4); + } +} + +.grid-retry-btn { + background: transparent; + border: 1px solid rgba(230, 57, 70, 0.4); + color: #e63946; + padding: 3px 10px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; +} + +/* Paginator & Loading Elements */ +.grid-paginator-wrapper { + margin-top: 1.25rem; + display: flex; + justify-content: flex-end; +} + +::deep .blazor-quickgrid-paginator button { + background: rgba(255, 255, 255, 0.02) !important; + border: 1px solid rgba(255, 107, 53, 0.1) !important; + color: #fff !important; + border-radius: 4px; + padding: 4px 10px; + cursor: pointer; +} + +.console-loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 6rem 2rem; + color: rgba(255, 255, 255, 0.4); +} + +.spinner { + width: 26px; + height: 26px; + border: 2px solid rgba(255, 107, 53, 0.1); + border-top-color: #ff6b35; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.refresh-icon.spinning { + animation: spin 1s linear infinite; +} + +/* Absolute Fullscreen initial overlay alignment rules for a clean loading UX */ +.initial-page-fullscreen-loader { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: #0b1116; /* Fallback baseline dark match */ + backdrop-filter: blur(20px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + + .loader-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + color: rgba(255, 255, 255, 0.6); + font-size: 0.95rem; + font-weight: 500; + } +} + +/* --- Seamless Grid Sync Loading Overlay --- */ +.relative-grid-container { + position: relative; + min-height: 250px; /* Prevents container collapse when data updates */ +} + +.grid-inline-loading-veil { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(11, 17, 22, 0.7); /* Translucent dark tint matching workspace canvas */ + backdrop-filter: blur(4px); /* Clean frosted separation without changing structural layouts */ + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + animation: fadeInVeil 0.15s ease-out; +} + +.veil-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + font-size: 0.9rem; +} + +@keyframes fadeInVeil { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/ShopAdmin/ShopAdmin.csproj b/ShopAdmin/ShopAdmin.csproj index 543805d..0770f5b 100644 --- a/ShopAdmin/ShopAdmin.csproj +++ b/ShopAdmin/ShopAdmin.csproj @@ -16,7 +16,8 @@ - + + @@ -25,8 +26,8 @@ - - + + @@ -57,6 +58,7 @@ +