diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor index 478669d6..e00128de 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor @@ -1,5 +1,13 @@ @page "/reporting/transaction-summary-operator" @rendermode InteractiveServer +@using MediatR +@using Microsoft.AspNetCore.Components.Forms +@using EstateManagementUI.BlazorServer.Requests +@using EstateManagementUI.BlazorServer.Models +@using static EstateManagementUI.BlazorServer.Requests.Queries +@inject IMediator Mediator +@inject NavigationManager Navigation +@inject ILogger Logger Transaction Summary by Operator @@ -8,7 +16,7 @@

Transaction Summary by Operator

-

View transaction summaries grouped by operator

+

View aggregated transaction performance per operator

@@ -18,10 +26,373 @@
- -
-
-

Transaction summary by operator report functionality will be implemented here.

+ @if (isLoading) + { + +
+
-
+ } + else if (!string.IsNullOrEmpty(errorMessage)) + { + +
+
+
+ + + +
+

Error Loading Data

+

@errorMessage

+
+
+
+
+ } + else + { + +
+
+

Filters

+
+
+
+ +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + + +
+
+ Total Operators + @totalOperators +
+
+ +
+
+ + + +
+
+ Total Transactions + @totalTransactions.ToString("N0") +
+
+ +
+
+ + + +
+
+ Total Value + @totalValue.ToString("C") +
+
+ +
+
+ + + +
+
+ Total Fees Earned + @totalFees.ToString("C") +
+
+
+ + +
+
+

Operator Transaction Summary

+
+
+ @if (summaryData != null && summaryData.Any()) + { +
+ + + + + + + + + + + + + + + @foreach (var item in summaryData) + { + + + + + + + + + + + } + + + + + + + + + + + + + +
Operator NameTransaction CountTransaction ValueFees EarnedAverage ValueSuccessfulFailedFailure Rate
@item.OperatorName@item.TotalTransactionCount.ToString("N0")@item.TotalTransactionValue.ToString("C")@item.TotalFeesEarned.ToString("C")@item.AverageTransactionValue.ToString("C")@item.SuccessfulTransactionCount.ToString("N0")@item.FailedTransactionCount.ToString("N0") + @{ + var failureRate = CalculateFailureRate(item.FailedTransactionCount, item.TotalTransactionCount); + var rateClass = GetFailureRateClass(failureRate); + } + @failureRate.ToString("F1")% +
Total@totalTransactions.ToString("N0")@totalValue.ToString("C")@totalFees.ToString("C")@(totalTransactions > 0 ? (totalValue / totalTransactions).ToString("C") : "$0.00")@totalSuccessful.ToString("N0")@totalFailed.ToString("N0") + @{ + var overallFailureRate = CalculateFailureRate(totalFailed, totalTransactions); + var overallRateClass = GetFailureRateClass(overallFailureRate); + } + @overallFailureRate.ToString("F1")% +
+
+ } + else + { +
+ + + +

No transaction data available for the selected period

+
+ } +
+
+ }
+ +@code { + private bool isLoading = true; + private string? errorMessage; + + // Filter states + private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); + private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now); + private string _selectedMerchantId = ""; + private string _selectedOperatorId = ""; + + // Data + private List? summaryData; + private List? merchants; + private List? operators; + + // KPIs + private int totalOperators = 0; + private int totalTransactions = 0; + private decimal totalValue = 0; + private decimal totalFees = 0; + private int totalSuccessful = 0; + private int totalFailed = 0; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + try + { + isLoading = true; + errorMessage = null; + StateHasChanged(); + + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var accessToken = "stubbed-token"; + + // Load filter options + var merchantsTask = Mediator.Send(new GetMerchantsQuery(correlationId, accessToken, estateId)); + var operatorsTask = Mediator.Send(new GetOperatorsQuery(correlationId, accessToken, estateId)); + + await Task.WhenAll(merchantsTask, operatorsTask); + + if (merchantsTask.Result.IsSuccess) + merchants = merchantsTask.Result.Data; + + if (operatorsTask.Result.IsSuccess) + operators = operatorsTask.Result.Data; + + // Load summary data + await LoadSummaryData(); + } + catch (Exception ex) + { + errorMessage = $"Failed to load data: {ex.Message}"; + Logger.LogError(ex, "Error loading operator transaction summary data"); + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private async Task LoadSummaryData() + { + try + { + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var accessToken = "stubbed-token"; + + var startDate = _startDate.ToDateTime(TimeOnly.MinValue); + var endDate = _endDate.ToDateTime(TimeOnly.MaxValue); + + Guid? merchantId = string.IsNullOrEmpty(_selectedMerchantId) ? null : Guid.Parse(_selectedMerchantId); + Guid? operatorId = string.IsNullOrEmpty(_selectedOperatorId) ? null : Guid.Parse(_selectedOperatorId); + + var result = await Mediator.Send(new GetOperatorTransactionSummaryQuery( + correlationId, + accessToken, + estateId, + startDate, + endDate, + merchantId, + operatorId + )); + + if (result.IsSuccess && result.Data != null) + { + summaryData = result.Data; + CalculateKPIs(); + } + else + { + errorMessage = result.Message ?? "Failed to load summary data"; + } + } + catch (Exception ex) + { + errorMessage = $"Failed to load summary data: {ex.Message}"; + Logger.LogError(ex, "Error loading summary data"); + } + } + + private void CalculateKPIs() + { + if (summaryData == null || !summaryData.Any()) + { + totalOperators = 0; + totalTransactions = 0; + totalValue = 0; + totalFees = 0; + totalSuccessful = 0; + totalFailed = 0; + return; + } + + totalOperators = summaryData.Count; + totalTransactions = summaryData.Sum(s => s.TotalTransactionCount); + totalValue = summaryData.Sum(s => s.TotalTransactionValue); + totalFees = summaryData.Sum(s => s.TotalFeesEarned); + totalSuccessful = summaryData.Sum(s => s.SuccessfulTransactionCount); + totalFailed = summaryData.Sum(s => s.FailedTransactionCount); + } + + private double CalculateFailureRate(int failed, int total) + { + return total > 0 ? (failed * 100.0 / total) : 0; + } + + private string GetFailureRateClass(double failureRate) + { + return failureRate <= 5 ? "text-green-600" : failureRate <= 10 ? "text-yellow-600" : "text-red-600"; + } + + private async Task ApplyFilters() + { + await LoadSummaryData(); + } + + private async Task ClearFilters() + { + _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); + _endDate = DateOnly.FromDateTime(DateTime.Now); + _selectedMerchantId = ""; + _selectedOperatorId = ""; + await LoadSummaryData(); + } +} + diff --git a/EstateManagementUI.BlazorServer/Models/Models.cs b/EstateManagementUI.BlazorServer/Models/Models.cs index 714d4d9f..de66d550 100644 --- a/EstateManagementUI.BlazorServer/Models/Models.cs +++ b/EstateManagementUI.BlazorServer/Models/Models.cs @@ -224,6 +224,18 @@ public class MerchantTransactionSummaryModel public int FailedTransactionCount { get; set; } } +public class OperatorTransactionSummaryModel +{ + public Guid OperatorId { get; set; } + public string? OperatorName { get; set; } + public int TotalTransactionCount { get; set; } + public decimal TotalTransactionValue { get; set; } + public decimal AverageTransactionValue { get; set; } + public int SuccessfulTransactionCount { get; set; } + public int FailedTransactionCount { get; set; } + public decimal TotalFeesEarned { get; set; } +} + public class ProductPerformanceModel { public string? ProductName { get; set; } diff --git a/EstateManagementUI.BlazorServer/Requests/Requests.cs b/EstateManagementUI.BlazorServer/Requests/Requests.cs index f9e5406c..e34c0fee 100644 --- a/EstateManagementUI.BlazorServer/Requests/Requests.cs +++ b/EstateManagementUI.BlazorServer/Requests/Requests.cs @@ -40,6 +40,7 @@ public record GetLastSettlementQuery(CorrelationId CorrelationId, string AccessT public record GetMerchantQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId) : IRequest>; public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest>>; public record GetProductPerformanceQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest>>; + public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null) : IRequest>>; public record GetMerchantSettlementHistoryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid? MerchantId, DateTime StartDate, DateTime EndDate) : IRequest>>; } diff --git a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs index 7e3aca66..43cd0773 100644 --- a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs +++ b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs @@ -58,6 +58,7 @@ public Task Send(IRequest request, Cancellation Queries.GetLastSettlementQuery => Task.FromResult((TResponse)(object)Result.Success(GetMockLastSettlement())), Queries.GetMerchantTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockMerchantTransactionSummary(query))), Queries.GetProductPerformanceQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockProductPerformance(query))), + Queries.GetOperatorTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockOperatorTransactionSummary(query))), Queries.GetMerchantSettlementHistoryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockMerchantSettlementHistory(query))), // Commands - execute against test data store @@ -732,4 +733,41 @@ private Result ExecuteRemoveOperatorFromEstate(Commands.RemoveOperatorFromEstate // For now, we just return success return Result.Success(); } + + private List GetMockOperatorTransactionSummary(Queries.GetOperatorTransactionSummaryQuery query) + { + var operators = _testDataStore.GetOperators(query.EstateId); + var summary = new List(); + var random = new Random(42); // Use seed for consistent test data + + const decimal DefaultSuccessRate = 0.92m; // 92% success rate for mock data + const decimal DefaultFeePercentage = 0.015m; // 1.5% fee rate + + foreach (var op in operators) + { + // Apply operator filter if specified + if (query.OperatorId.HasValue && op.OperatorId != query.OperatorId.Value) + continue; + + var totalCount = random.Next(500, 5000); + var successfulCount = (int)(totalCount * DefaultSuccessRate); + var failedCount = totalCount - successfulCount; + var totalValue = (decimal)(random.NextDouble() * 500000 + 50000); + var totalFees = Math.Round(totalValue * DefaultFeePercentage, 2); + + summary.Add(new OperatorTransactionSummaryModel + { + OperatorId = op.OperatorId, + OperatorName = op.Name, + TotalTransactionCount = totalCount, + TotalTransactionValue = Math.Round(totalValue, 2), + AverageTransactionValue = Math.Round(totalValue / totalCount, 2), + SuccessfulTransactionCount = successfulCount, + FailedTransactionCount = failedCount, + TotalFeesEarned = totalFees + }); + } + + return summary; + } } diff --git a/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs b/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs new file mode 100644 index 00000000..3524c07b --- /dev/null +++ b/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; + +namespace EstateManagementUI.BusinessLogic.Models; + +[ExcludeFromCodeCoverage] +public class OperatorTransactionSummaryModel +{ + public Guid OperatorId { get; set; } + public string? OperatorName { get; set; } + public int TotalTransactionCount { get; set; } + public decimal TotalTransactionValue { get; set; } + public decimal AverageTransactionValue { get; set; } + public int SuccessfulTransactionCount { get; set; } + public int FailedTransactionCount { get; set; } + public decimal TotalFeesEarned { get; set; } +} diff --git a/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs b/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs index 3736ea87..8230573b 100644 --- a/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs +++ b/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs @@ -24,7 +24,8 @@ public class ReportingRequestHandler : IRequestHandler>>, IRequestHandler>, IRequestHandler>>, -IRequestHandler>> { +IRequestHandler>>, +IRequestHandler>> { private readonly IApiClient ApiClient; public ReportingRequestHandler(IApiClient apiClient) @@ -236,4 +237,47 @@ public async Task>> Handle(GetProductPerfor return Result.Success(products); } + + public async Task>> Handle(GetOperatorTransactionSummaryQuery request, + CancellationToken cancellationToken) { + // TODO: Replace with actual API call when endpoint is available + // For now, return mock data for testing + var operators = await this.ApiClient.GetOperators(request.AccessToken, Guid.Empty, request.EstateId, cancellationToken); + + if (!operators.IsSuccess) { + return Result.Failure>(operators.Message); + } + + var summary = new List(); + var random = new Random(42); // Use seed for consistent test data + + const decimal DefaultSuccessRate = 0.92m; // 92% success rate for mock data + const decimal DefaultFeePercentage = 0.015m; // 1.5% fee rate + + foreach (var op in operators.Data) { + var totalCount = random.Next(500, 5000); + var successfulCount = (int)(totalCount * DefaultSuccessRate); + var failedCount = totalCount - successfulCount; + var totalValue = (decimal)(random.NextDouble() * 500000 + 50000); + var totalFees = Math.Round(totalValue * DefaultFeePercentage, 2); + + summary.Add(new OperatorTransactionSummaryModel { + OperatorId = op.Id, + OperatorName = op.Name, + TotalTransactionCount = totalCount, + TotalTransactionValue = Math.Round(totalValue, 2), + AverageTransactionValue = Math.Round(totalValue / totalCount, 2), + SuccessfulTransactionCount = successfulCount, + FailedTransactionCount = failedCount, + TotalFeesEarned = totalFees + }); + } + + // Apply filters if specified + if (request.OperatorId.HasValue) { + summary = summary.Where(s => s.OperatorId == request.OperatorId.Value).ToList(); + } + + return Result.Success(summary); + } } \ No newline at end of file diff --git a/EstateManagementUI.BusinessLogic/Requests/Queries.cs b/EstateManagementUI.BusinessLogic/Requests/Queries.cs index d54fe9ec..d8cf25b9 100644 --- a/EstateManagementUI.BusinessLogic/Requests/Queries.cs +++ b/EstateManagementUI.BusinessLogic/Requests/Queries.cs @@ -72,5 +72,7 @@ public record GetMerchantQuery(CorrelationId CorrelationId, String AccessToken, public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest>>; public record GetProductPerformanceQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest>>; + + public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null) : IRequest>>; } }