|
1 | 1 | @page "/reporting/settlement-summary" |
2 | 2 | @rendermode InteractiveServer |
| 3 | +@using MediatR |
| 4 | +@using Microsoft.AspNetCore.Components.Forms |
| 5 | +@using EstateManagementUI.BlazorServer.Requests |
| 6 | +@using EstateManagementUI.BlazorServer.Models |
| 7 | +@using static EstateManagementUI.BlazorServer.Requests.Queries |
| 8 | +@inject IMediator Mediator |
| 9 | +@inject NavigationManager Navigation |
| 10 | +@inject ILogger<SettlementSummary> Logger |
3 | 11 |
|
4 | 12 | <PageTitle>Settlement Summary Report</PageTitle> |
5 | 13 |
|
|
8 | 16 | <div class="flex items-center justify-between"> |
9 | 17 | <div> |
10 | 18 | <h1 class="text-2xl font-bold text-gray-900">Settlement Summary Report</h1> |
11 | | - <p class="text-gray-600 mt-1">View settlement summary information</p> |
| 19 | + <p class="text-gray-600 mt-1">View settlement outcomes per merchant with net settlement calculations</p> |
12 | 20 | </div> |
13 | 21 | <a href="/reporting" class="btn btn-secondary"> |
14 | 22 | <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
18 | 26 | </a> |
19 | 27 | </div> |
20 | 28 |
|
21 | | - <!-- Report Content --> |
22 | | - <div class="card"> |
23 | | - <div class="card-body"> |
24 | | - <p class="text-gray-600">Settlement summary report functionality will be implemented here.</p> |
| 29 | + @if (isLoading) |
| 30 | + { |
| 31 | + <!-- Loading State --> |
| 32 | + <div class="flex justify-center items-center py-12"> |
| 33 | + <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-admin-primary"></div> |
25 | 34 | </div> |
26 | | - </div> |
| 35 | + } |
| 36 | + else if (!string.IsNullOrEmpty(errorMessage)) |
| 37 | + { |
| 38 | + <!-- Error State --> |
| 39 | + <div class="card bg-red-50 border border-red-200"> |
| 40 | + <div class="card-body"> |
| 41 | + <div class="flex items-center"> |
| 42 | + <svg class="w-6 h-6 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 43 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
| 44 | + </svg> |
| 45 | + <div> |
| 46 | + <h3 class="text-red-800 font-semibold">Error Loading Data</h3> |
| 47 | + <p class="text-red-700 text-sm mt-1">@errorMessage</p> |
| 48 | + </div> |
| 49 | + </div> |
| 50 | + </div> |
| 51 | + </div> |
| 52 | + } |
| 53 | + else |
| 54 | + { |
| 55 | + <!-- Filters Card --> |
| 56 | + <div class="card"> |
| 57 | + <div class="card-header"> |
| 58 | + <h3 class="card-title">Filters</h3> |
| 59 | + </div> |
| 60 | + <div class="card-body"> |
| 61 | + <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| 62 | + <!-- Date Range --> |
| 63 | + <div> |
| 64 | + <label class="block text-sm font-medium text-gray-700 mb-2">Start Date</label> |
| 65 | + <input type="date" @bind="_startDate" @bind:format="yyyy-MM-dd" class="form-control" /> |
| 66 | + </div> |
| 67 | + <div> |
| 68 | + <label class="block text-sm font-medium text-gray-700 mb-2">End Date</label> |
| 69 | + <input type="date" @bind="_endDate" @bind:format="yyyy-MM-dd" class="form-control" /> |
| 70 | + </div> |
| 71 | + |
| 72 | + <!-- Merchant Filter --> |
| 73 | + <div> |
| 74 | + <label class="block text-sm font-medium text-gray-700 mb-2">Merchant</label> |
| 75 | + <select @bind="_selectedMerchantId" class="form-control"> |
| 76 | + <option value="">All Merchants</option> |
| 77 | + @if (merchants != null) |
| 78 | + { |
| 79 | + @foreach (var merchant in merchants) |
| 80 | + { |
| 81 | + <option value="@merchant.MerchantId">@merchant.MerchantName</option> |
| 82 | + } |
| 83 | + } |
| 84 | + </select> |
| 85 | + </div> |
| 86 | + </div> |
| 87 | + <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4"> |
| 88 | + <!-- Settlement Status Filter --> |
| 89 | + <div> |
| 90 | + <label class="block text-sm font-medium text-gray-700 mb-2">Settlement Status</label> |
| 91 | + <select @bind="_selectedStatus" class="form-control"> |
| 92 | + <option value="">All Statuses</option> |
| 93 | + <option value="settled">Settled</option> |
| 94 | + <option value="pending">Pending</option> |
| 95 | + <option value="failed">Failed</option> |
| 96 | + </select> |
| 97 | + </div> |
| 98 | + </div> |
| 99 | + <div class="mt-4"> |
| 100 | + <button @onclick="ApplyFilters" class="btn btn-primary"> |
| 101 | + <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 102 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path> |
| 103 | + </svg> |
| 104 | + Apply Filters |
| 105 | + </button> |
| 106 | + <button @onclick="ClearFilters" class="btn btn-secondary ml-2"> |
| 107 | + Clear Filters |
| 108 | + </button> |
| 109 | + </div> |
| 110 | + </div> |
| 111 | + </div> |
| 112 | + |
| 113 | + <!-- Summary KPIs --> |
| 114 | + <div class="grid grid-cols-1 md:grid-cols-4 gap-4"> |
| 115 | + <div class="info-box"> |
| 116 | + <div class="info-box-icon bg-admin-primary"> |
| 117 | + <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 118 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path> |
| 119 | + </svg> |
| 120 | + </div> |
| 121 | + <div class="info-box-content"> |
| 122 | + <span class="info-box-text">Total Merchants</span> |
| 123 | + <span class="info-box-number">@totalMerchants</span> |
| 124 | + </div> |
| 125 | + </div> |
| 126 | + |
| 127 | + <div class="info-box"> |
| 128 | + <div class="info-box-icon bg-admin-success"> |
| 129 | + <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 130 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
| 131 | + </svg> |
| 132 | + </div> |
| 133 | + <div class="info-box-content"> |
| 134 | + <span class="info-box-text">Gross Value</span> |
| 135 | + <span class="info-box-number">@totalGrossValue.ToString("C")</span> |
| 136 | + </div> |
| 137 | + </div> |
| 138 | + |
| 139 | + <div class="info-box"> |
| 140 | + <div class="info-box-icon bg-admin-warning"> |
| 141 | + <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 142 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"></path> |
| 143 | + </svg> |
| 144 | + </div> |
| 145 | + <div class="info-box-content"> |
| 146 | + <span class="info-box-text">Total Fees</span> |
| 147 | + <span class="info-box-number">@totalFees.ToString("C")</span> |
| 148 | + </div> |
| 149 | + </div> |
| 150 | + |
| 151 | + <div class="info-box"> |
| 152 | + <div class="info-box-icon bg-admin-info"> |
| 153 | + <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 154 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
| 155 | + </svg> |
| 156 | + </div> |
| 157 | + <div class="info-box-content"> |
| 158 | + <span class="info-box-text">Net Settlement</span> |
| 159 | + <span class="info-box-number">@totalNetSettlement.ToString("C")</span> |
| 160 | + </div> |
| 161 | + </div> |
| 162 | + </div> |
| 163 | + |
| 164 | + <!-- Data Grid --> |
| 165 | + <div class="card"> |
| 166 | + <div class="card-header"> |
| 167 | + <h3 class="card-title">Settlement Summary</h3> |
| 168 | + </div> |
| 169 | + <div class="card-body"> |
| 170 | + @if (summaryData != null && summaryData.Any()) |
| 171 | + { |
| 172 | + <div class="overflow-x-auto"> |
| 173 | + <table class="table"> |
| 174 | + <thead> |
| 175 | + <tr> |
| 176 | + <th>Merchant Name</th> |
| 177 | + <th>Settlement Period</th> |
| 178 | + <th class="text-right">Gross Value</th> |
| 179 | + <th class="text-right">Total Fees</th> |
| 180 | + <th class="text-right">Net Settlement</th> |
| 181 | + <th class="text-center">Status</th> |
| 182 | + </tr> |
| 183 | + </thead> |
| 184 | + <tbody> |
| 185 | + @foreach (var item in summaryData) |
| 186 | + { |
| 187 | + <tr> |
| 188 | + <td class="font-medium">@item.MerchantName</td> |
| 189 | + <td> |
| 190 | + <div class="text-sm"> |
| 191 | + <div>@item.SettlementPeriodStart.ToString("yyyy-MM-dd")</div> |
| 192 | + <div class="text-gray-500">to @item.SettlementPeriodEnd.ToString("yyyy-MM-dd")</div> |
| 193 | + </div> |
| 194 | + </td> |
| 195 | + <td class="text-right">@item.GrossTransactionValue.ToString("C")</td> |
| 196 | + <td class="text-right text-red-600">-@item.TotalFees.ToString("C")</td> |
| 197 | + <td class="text-right font-semibold">@item.NetSettlementAmount.ToString("C")</td> |
| 198 | + <td class="text-center"> |
| 199 | + @{ |
| 200 | + var statusClass = item.SettlementStatus?.ToLower() switch |
| 201 | + { |
| 202 | + "settled" => "badge-success", |
| 203 | + "pending" => "badge-warning", |
| 204 | + "failed" => "badge-danger", |
| 205 | + _ => "badge-secondary" |
| 206 | + }; |
| 207 | + } |
| 208 | + <span class="badge @statusClass"> |
| 209 | + @(item.SettlementStatus?.ToUpper() ?? "UNKNOWN") |
| 210 | + </span> |
| 211 | + </td> |
| 212 | + </tr> |
| 213 | + } |
| 214 | + </tbody> |
| 215 | + </table> |
| 216 | + </div> |
| 217 | + <div class="mt-4 p-4 bg-gray-50 rounded-lg"> |
| 218 | + <p class="text-sm text-gray-600"> |
| 219 | + <strong>Note:</strong> Net Settlement Amount is calculated as: Gross Transaction Value - Total Fees. |
| 220 | + Settlement status reflects the current state from the settlement engine. |
| 221 | + </p> |
| 222 | + </div> |
| 223 | + } |
| 224 | + else |
| 225 | + { |
| 226 | + <div class="text-center py-8 text-gray-500"> |
| 227 | + <svg class="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 228 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path> |
| 229 | + </svg> |
| 230 | + <p class="text-lg">No settlement data available for the selected period</p> |
| 231 | + </div> |
| 232 | + } |
| 233 | + </div> |
| 234 | + </div> |
| 235 | + } |
27 | 236 | </div> |
| 237 | + |
| 238 | +@code { |
| 239 | + private bool isLoading = true; |
| 240 | + private string? errorMessage; |
| 241 | + |
| 242 | + // Filter states |
| 243 | + private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); |
| 244 | + private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now); |
| 245 | + private string _selectedMerchantId = ""; |
| 246 | + private string _selectedStatus = ""; |
| 247 | + |
| 248 | + // Data |
| 249 | + private List<SettlementSummaryModel>? summaryData; |
| 250 | + private List<MerchantModel>? merchants; |
| 251 | + |
| 252 | + // KPIs |
| 253 | + private int totalMerchants = 0; |
| 254 | + private decimal totalGrossValue = 0; |
| 255 | + private decimal totalFees = 0; |
| 256 | + private decimal totalNetSettlement = 0; |
| 257 | + |
| 258 | + protected override async Task OnInitializedAsync() |
| 259 | + { |
| 260 | + await LoadData(); |
| 261 | + } |
| 262 | + |
| 263 | + private async Task LoadData() |
| 264 | + { |
| 265 | + try |
| 266 | + { |
| 267 | + isLoading = true; |
| 268 | + errorMessage = null; |
| 269 | + StateHasChanged(); |
| 270 | + |
| 271 | + var correlationId = new CorrelationId(Guid.NewGuid()); |
| 272 | + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); |
| 273 | + var accessToken = "stubbed-token"; |
| 274 | + |
| 275 | + // Load filter options |
| 276 | + var merchantsResult = await Mediator.Send(new GetMerchantsQuery(correlationId, accessToken, estateId)); |
| 277 | + |
| 278 | + if (merchantsResult.IsSuccess) |
| 279 | + merchants = merchantsResult.Data; |
| 280 | + |
| 281 | + // Load summary data |
| 282 | + await LoadSummaryData(); |
| 283 | + } |
| 284 | + catch (Exception ex) |
| 285 | + { |
| 286 | + errorMessage = $"Failed to load data: {ex.Message}"; |
| 287 | + Logger.LogError(ex, "Error loading settlement summary data"); |
| 288 | + } |
| 289 | + finally |
| 290 | + { |
| 291 | + isLoading = false; |
| 292 | + StateHasChanged(); |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + private async Task LoadSummaryData() |
| 297 | + { |
| 298 | + try |
| 299 | + { |
| 300 | + var correlationId = new CorrelationId(Guid.NewGuid()); |
| 301 | + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); |
| 302 | + var accessToken = "stubbed-token"; |
| 303 | + |
| 304 | + var startDate = _startDate.ToDateTime(TimeOnly.MinValue); |
| 305 | + var endDate = _endDate.ToDateTime(TimeOnly.MaxValue); |
| 306 | + |
| 307 | + Guid? merchantId = string.IsNullOrEmpty(_selectedMerchantId) ? null : Guid.Parse(_selectedMerchantId); |
| 308 | + string? status = string.IsNullOrEmpty(_selectedStatus) ? null : _selectedStatus; |
| 309 | + |
| 310 | + var result = await Mediator.Send(new GetSettlementSummaryQuery( |
| 311 | + correlationId, |
| 312 | + accessToken, |
| 313 | + estateId, |
| 314 | + startDate, |
| 315 | + endDate, |
| 316 | + merchantId, |
| 317 | + status |
| 318 | + )); |
| 319 | + |
| 320 | + if (result.IsSuccess && result.Data != null) |
| 321 | + { |
| 322 | + summaryData = result.Data; |
| 323 | + CalculateKPIs(); |
| 324 | + } |
| 325 | + else |
| 326 | + { |
| 327 | + errorMessage = result.Message ?? "Failed to load settlement summary data"; |
| 328 | + } |
| 329 | + } |
| 330 | + catch (Exception ex) |
| 331 | + { |
| 332 | + errorMessage = $"Failed to load settlement summary data: {ex.Message}"; |
| 333 | + Logger.LogError(ex, "Error loading settlement summary data"); |
| 334 | + } |
| 335 | + } |
| 336 | + |
| 337 | + private void CalculateKPIs() |
| 338 | + { |
| 339 | + if (summaryData == null || !summaryData.Any()) |
| 340 | + { |
| 341 | + totalMerchants = 0; |
| 342 | + totalGrossValue = 0; |
| 343 | + totalFees = 0; |
| 344 | + totalNetSettlement = 0; |
| 345 | + return; |
| 346 | + } |
| 347 | + |
| 348 | + totalMerchants = summaryData.Count; |
| 349 | + totalGrossValue = summaryData.Sum(s => s.GrossTransactionValue); |
| 350 | + totalFees = summaryData.Sum(s => s.TotalFees); |
| 351 | + totalNetSettlement = summaryData.Sum(s => s.NetSettlementAmount); |
| 352 | + } |
| 353 | + |
| 354 | + private async Task ApplyFilters() |
| 355 | + { |
| 356 | + await LoadSummaryData(); |
| 357 | + } |
| 358 | + |
| 359 | + private async Task ClearFilters() |
| 360 | + { |
| 361 | + _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); |
| 362 | + _endDate = DateOnly.FromDateTime(DateTime.Now); |
| 363 | + _selectedMerchantId = ""; |
| 364 | + _selectedStatus = ""; |
| 365 | + await LoadSummaryData(); |
| 366 | + } |
| 367 | +} |
| 368 | + |
0 commit comments