diff --git a/EstateManagementUI.BlazorServer/Components/App.razor b/EstateManagementUI.BlazorServer/Components/App.razor index 42610fbc..1d77537b 100644 --- a/EstateManagementUI.BlazorServer/Components/App.razor +++ b/EstateManagementUI.BlazorServer/Components/App.razor @@ -16,8 +16,9 @@ - + + diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionDetail.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionDetail.razor index 3b4cfe3e..0a75750a 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionDetail.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionDetail.razor @@ -1,6 +1,15 @@ @page "/reporting/transaction-detail" @rendermode InteractiveServer +@using MediatR +@using Microsoft.AspNetCore.Components.Forms +@using EstateManagementUI.BlazorServer.Requests +@using EstateManagementUI.BlazorServer.Models +@using System.Text +@using static EstateManagementUI.BlazorServer.Requests.Queries +@inject IMediator Mediator @inject NavigationManager Navigation +@inject ILogger Logger +@inject IJSRuntime JSRuntime Transaction Detail Report @@ -9,7 +18,7 @@

Transaction Detail Report

-

View detailed transaction information

+

View detailed, transaction-level audit report for all merchant activity

@@ -19,15 +28,648 @@
- -
-
-

Transaction detail report functionality will be implemented here.

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

Error Loading Data

+

@errorMessage

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

Filters

+
+
+
+ +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + @if (detailData != null && detailData.Any()) + { + + } +
+
+
+ + +
+
+
+ + + +
+
+ Total Transactions + @totalTransactions.ToString("N0") +
+
+ +
+
+ + + +
+
+ Gross Amount + @totalGrossAmount.ToString("C") +
+
+ +
+
+ + + +
+
+ Total Fees + @totalFees.ToString("C") +
+
+ +
+
+ + + +
+
+ Net Amount + @totalNetAmount.ToString("C") +
+
+
+ + +
+
+

Transaction Details

+ @if (detailData != null && detailData.Any()) + { + var pagedData = GetPagedData(); + var startRecord = (_currentPage - 1) * _pageSize + 1; + var endRecord = Math.Min(_currentPage * _pageSize, detailData.Count); + Showing @startRecord-@endRecord of @detailData.Count transaction(s) + } +
+
+ @if (detailData != null && detailData.Any()) + { + var pagedData = GetPagedData(); +
+ + + + + + + + + + + + + + + + + + @foreach (var item in pagedData) + { + + + + + + + + + + + + + + } + +
+ Transaction ID @GetSortIcon(nameof(TransactionDetailModel.TransactionId)) + + Date & Time @GetSortIcon(nameof(TransactionDetailModel.TransactionDateTime)) + + Merchant @GetSortIcon(nameof(TransactionDetailModel.MerchantName)) + + Operator @GetSortIcon(nameof(TransactionDetailModel.OperatorName)) + + Product @GetSortIcon(nameof(TransactionDetailModel.ProductName)) + + Type @GetSortIcon(nameof(TransactionDetailModel.TransactionType)) + + Status @GetSortIcon(nameof(TransactionDetailModel.TransactionStatus)) + + Gross Amount @GetSortIcon(nameof(TransactionDetailModel.GrossAmount)) + + Fees @GetSortIcon(nameof(TransactionDetailModel.FeesCommission)) + + Net Amount @GetSortIcon(nameof(TransactionDetailModel.NetAmount)) + Settlement Ref
@GetShortId(item.TransactionId)@item.TransactionDateTime.ToString("yyyy-MM-dd HH:mm:ss")@item.MerchantName@item.OperatorName@item.ProductName + + @item.TransactionType + + + + @item.TransactionStatus + + @item.GrossAmount.ToString("C")@item.FeesCommission.ToString("C")@item.NetAmount.ToString("C")@(item.SettlementReference ?? "-")
+
+ + + @if (_totalPages > 1) + { +
+
+ + +
+ +
+ + + + Page @_currentPage of @_totalPages + + + +
+
+ } + } + else + { +
+ + + +

No transaction data available for the selected period

+
+ } +
+
+ }
@code { + private const string DEFAULT_ESTATE_ID = "11111111-1111-1111-1111-111111111111"; + private const string DEFAULT_ACCESS_TOKEN = "stubbed-token"; + + private bool isLoading = true; + private string? errorMessage; + + // Filter states + private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-7)); + private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now); + private string _selectedMerchantId = ""; + private string _selectedOperatorId = ""; + private string _selectedProductId = ""; + + // Data + private List? detailData; + private List? merchants; + private List? operators; + private List? products; + + // KPIs + private int totalTransactions = 0; + private decimal totalGrossAmount = 0; + private decimal totalFees = 0; + private decimal totalNetAmount = 0; + + // Sorting + private string? _currentSortColumn; + private bool _sortAscending = true; + + // Paging + private int _currentPage = 1; + private int _pageSize = 25; + private int _totalPages = 0; + + private List GetPagedData() + { + if (detailData == null || !detailData.Any()) + return new List(); + + _totalPages = (int)Math.Ceiling(detailData.Count / (double)_pageSize); + + return detailData + .Skip((_currentPage - 1) * _pageSize) + .Take(_pageSize) + .ToList(); + } + + private void GoToPage(int page) + { + if (page < 1 || page > _totalPages) + return; + + _currentPage = page; + } + + private void NextPage() + { + if (_currentPage < _totalPages) + _currentPage++; + } + + private void PreviousPage() + { + if (_currentPage > 1) + _currentPage--; + } + + protected override async Task OnInitializedAsync() + { + // Check for query parameters (from drill-down) + var uri = new Uri(Navigation.Uri); + var query = System.Web.HttpUtility.ParseQueryString(uri.Query); + + if (!string.IsNullOrEmpty(query["merchantId"])) + { + _selectedMerchantId = query["merchantId"]; + } + + if (!string.IsNullOrEmpty(query["startDate"])) + { + if (DateOnly.TryParse(query["startDate"], out var startDate)) + { + _startDate = startDate; + } + } + + if (!string.IsNullOrEmpty(query["endDate"])) + { + if (DateOnly.TryParse(query["endDate"], out var endDate)) + { + _endDate = endDate; + } + } + + await LoadData(); + } + + private async Task LoadData() + { + try + { + isLoading = true; + errorMessage = null; + StateHasChanged(); + + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = Guid.Parse(DEFAULT_ESTATE_ID); + var accessToken = DEFAULT_ACCESS_TOKEN; + + // Load filter options + var merchantsTask = Mediator.Send(new GetMerchantsQuery(correlationId, accessToken, estateId)); + var operatorsTask = Mediator.Send(new GetOperatorsQuery(correlationId, accessToken, estateId)); + var contractsTask = Mediator.Send(new GetContractsQuery(correlationId, accessToken, estateId)); + + await Task.WhenAll(merchantsTask, operatorsTask, contractsTask); + + if (merchantsTask.Result.IsSuccess) + merchants = merchantsTask.Result.Data; + + if (operatorsTask.Result.IsSuccess) + operators = operatorsTask.Result.Data; + + if (contractsTask.Result.IsSuccess) + { + // Extract all products from all contracts + products = contractsTask.Result.Data + ?.SelectMany(c => c.Products ?? new List()) + .Where(p => !string.IsNullOrEmpty(p.ProductName)) + .DistinctBy(p => p.ProductName) + .ToList(); + } + + // Load detail data + await LoadDetailData(); + } + catch (Exception ex) + { + errorMessage = $"Failed to load data: {ex.Message}"; + Logger.LogError(ex, "Error loading transaction detail data"); + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private async Task LoadDetailData() + { + try + { + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = Guid.Parse(DEFAULT_ESTATE_ID); + var accessToken = DEFAULT_ACCESS_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); + Guid? productId = string.IsNullOrEmpty(_selectedProductId) ? null : Guid.Parse(_selectedProductId); + + var result = await Mediator.Send(new GetTransactionDetailQuery( + correlationId, + accessToken, + estateId, + startDate, + endDate, + merchantId, + operatorId, + productId + )); + + if (result.IsSuccess && result.Data != null) + { + detailData = result.Data; + CalculateKPIs(); + } + else + { + errorMessage = result.Message ?? "Failed to load transaction detail data"; + } + } + catch (Exception ex) + { + errorMessage = $"Failed to load transaction detail data: {ex.Message}"; + Logger.LogError(ex, "Error loading transaction detail data"); + } + } + + private void CalculateKPIs() + { + if (detailData == null || !detailData.Any()) + { + totalTransactions = 0; + totalGrossAmount = 0; + totalFees = 0; + totalNetAmount = 0; + return; + } + + totalTransactions = detailData.Count; + totalGrossAmount = detailData.Sum(t => t.GrossAmount); + totalFees = detailData.Sum(t => t.FeesCommission); + totalNetAmount = detailData.Sum(t => t.NetAmount); + } + + private async Task ApplyFilters() + { + _currentPage = 1; // Reset to first page when filters are applied + await LoadDetailData(); + } + + private async Task ClearFilters() + { + _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-7)); + _endDate = DateOnly.FromDateTime(DateTime.Now); + _selectedMerchantId = ""; + _selectedOperatorId = ""; + _selectedProductId = ""; + _currentPage = 1; // Reset to first page when filters are cleared + await LoadDetailData(); + } + + private void SortBy(string columnName) + { + if (detailData == null || !detailData.Any()) + return; + + if (_currentSortColumn == columnName) + { + _sortAscending = !_sortAscending; + } + else + { + _currentSortColumn = columnName; + _sortAscending = true; + } + + _currentPage = 1; // Reset to first page when sorting changes + + detailData = columnName switch + { + nameof(TransactionDetailModel.TransactionId) => _sortAscending + ? detailData.OrderBy(t => t.TransactionId).ToList() + : detailData.OrderByDescending(t => t.TransactionId).ToList(), + nameof(TransactionDetailModel.TransactionDateTime) => _sortAscending + ? detailData.OrderBy(t => t.TransactionDateTime).ToList() + : detailData.OrderByDescending(t => t.TransactionDateTime).ToList(), + nameof(TransactionDetailModel.MerchantName) => _sortAscending + ? detailData.OrderBy(t => t.MerchantName).ToList() + : detailData.OrderByDescending(t => t.MerchantName).ToList(), + nameof(TransactionDetailModel.OperatorName) => _sortAscending + ? detailData.OrderBy(t => t.OperatorName).ToList() + : detailData.OrderByDescending(t => t.OperatorName).ToList(), + nameof(TransactionDetailModel.ProductName) => _sortAscending + ? detailData.OrderBy(t => t.ProductName).ToList() + : detailData.OrderByDescending(t => t.ProductName).ToList(), + nameof(TransactionDetailModel.TransactionType) => _sortAscending + ? detailData.OrderBy(t => t.TransactionType).ToList() + : detailData.OrderByDescending(t => t.TransactionType).ToList(), + nameof(TransactionDetailModel.TransactionStatus) => _sortAscending + ? detailData.OrderBy(t => t.TransactionStatus).ToList() + : detailData.OrderByDescending(t => t.TransactionStatus).ToList(), + nameof(TransactionDetailModel.GrossAmount) => _sortAscending + ? detailData.OrderBy(t => t.GrossAmount).ToList() + : detailData.OrderByDescending(t => t.GrossAmount).ToList(), + nameof(TransactionDetailModel.FeesCommission) => _sortAscending + ? detailData.OrderBy(t => t.FeesCommission).ToList() + : detailData.OrderByDescending(t => t.FeesCommission).ToList(), + nameof(TransactionDetailModel.NetAmount) => _sortAscending + ? detailData.OrderBy(t => t.NetAmount).ToList() + : detailData.OrderByDescending(t => t.NetAmount).ToList(), + _ => detailData + }; + + CalculateKPIs(); + } + + private string GetSortIcon(string columnName) + { + if (_currentSortColumn != columnName) + return "↕"; + + return _sortAscending ? "↑" : "↓"; + } + + private string GetShortId(Guid transactionId) + { + var idString = transactionId.ToString(); + return idString.Length >= 8 ? $"{idString.Substring(0, 8)}..." : idString; + } + + private string GetTypeBadgeClass(string? type) + { + return type switch + { + "Sale" => "badge-primary", + "Refund" => "badge-warning", + "Reversal" => "badge-secondary", + _ => "badge-default" + }; + } + + private string GetStatusBadgeClass(string? status) + { + return status switch + { + "Successful" => "badge-success", + "Failed" => "badge-danger", + "Reversed" => "badge-warning", + _ => "badge-default" + }; + } + + private async Task ExportToCSV() + { + if (detailData == null || !detailData.Any()) + return; + + try + { + var csv = new StringBuilder(); + + // Header + csv.AppendLine("Transaction ID,Transaction Date & Time,Merchant,Operator,Product,Transaction Type,Transaction Status,Gross Amount,Fees/Commission,Net Amount,Settlement Reference"); + + // Data rows + foreach (var item in detailData) + { + csv.AppendLine($"\"{item.TransactionId}\",\"{item.TransactionDateTime:yyyy-MM-dd HH:mm:ss}\",\"{item.MerchantName}\",\"{item.OperatorName}\",\"{item.ProductName}\",\"{item.TransactionType}\",\"{item.TransactionStatus}\",{item.GrossAmount},{item.FeesCommission},{item.NetAmount},\"{item.SettlementReference ?? ""}\""); + } + + var fileName = $"transaction-detail-{DateTime.Now:yyyyMMdd-HHmmss}.csv"; + var bytes = Encoding.UTF8.GetBytes(csv.ToString()); + var base64 = Convert.ToBase64String(bytes); + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error exporting to CSV"); + errorMessage = "Failed to export data to CSV"; + } + } + private string GetBackUrl() { var uri = Navigation.Uri; diff --git a/EstateManagementUI.BlazorServer/Models/Models.cs b/EstateManagementUI.BlazorServer/Models/Models.cs index b61f472a..72b5f0e0 100644 --- a/EstateManagementUI.BlazorServer/Models/Models.cs +++ b/EstateManagementUI.BlazorServer/Models/Models.cs @@ -265,3 +265,22 @@ public class SettlementSummaryModel public decimal NetSettlementAmount { get; set; } public string? SettlementStatus { get; set; } } + +// Transaction Detail Models +public class TransactionDetailModel +{ + public Guid TransactionId { get; set; } + public DateTime TransactionDateTime { get; set; } + public string? MerchantName { get; set; } + public Guid MerchantId { get; set; } + public string? OperatorName { get; set; } + public Guid OperatorId { get; set; } + public string? ProductName { get; set; } + public Guid ProductId { get; set; } + public string? TransactionType { get; set; } // sale, refund, reversal + public string? TransactionStatus { get; set; } // successful, failed, reversed + public decimal GrossAmount { get; set; } + public decimal FeesCommission { get; set; } + public decimal NetAmount { get; set; } + public string? SettlementReference { get; set; } +} diff --git a/EstateManagementUI.BlazorServer/Requests/Requests.cs b/EstateManagementUI.BlazorServer/Requests/Requests.cs index 0c7e48e0..900a1b9f 100644 --- a/EstateManagementUI.BlazorServer/Requests/Requests.cs +++ b/EstateManagementUI.BlazorServer/Requests/Requests.cs @@ -43,6 +43,7 @@ public record GetProductPerformanceQuery(CorrelationId CorrelationId, string Acc 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>>; public record GetSettlementSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, string? Status = null) : IRequest>>; + public record GetTransactionDetailQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest>>; } public static class Commands diff --git a/EstateManagementUI.BlazorServer/Services/StubbedMediatorService.cs b/EstateManagementUI.BlazorServer/Services/StubbedMediatorService.cs index 8be3bb30..d0d7f777 100644 --- a/EstateManagementUI.BlazorServer/Services/StubbedMediatorService.cs +++ b/EstateManagementUI.BlazorServer/Services/StubbedMediatorService.cs @@ -43,6 +43,7 @@ public Task Send(IRequest request, Cancellation Queries.GetTopOperatorDataQuery => Task.FromResult((TResponse)(object)Result>.Success(GetMockTopOperators())), Queries.GetBottomOperatorDataQuery => Task.FromResult((TResponse)(object)Result>.Success(GetMockBottomOperators())), Queries.GetLastSettlementQuery => Task.FromResult((TResponse)(object)Result.Success(GetMockLastSettlement())), + Queries.GetTransactionDetailQuery q => Task.FromResult((TResponse)(object)Result>.Success(GetMockTransactionDetails(q))), // Commands - just return success Commands.AddMerchantDeviceCommand => Task.FromResult((TResponse)(object)Result.Success()), @@ -431,4 +432,117 @@ public Task Publish(TNotification notification, CancellationToken SalesValue = 125000.00m, SettlementValue = 123750.00m }; + + private static List GetMockTransactionDetails(Queries.GetTransactionDetailQuery query) + { + var merchants = GetMockMerchants(); + var operators = GetMockOperators(); + var contracts = GetMockContracts(); + + // Get all products with their IDs from contracts + var productList = contracts + .SelectMany(c => c.Products ?? new List()) + .Where(p => !string.IsNullOrEmpty(p.ProductName)) + .ToList(); + + var transactionTypes = new[] { "Sale", "Refund", "Reversal" }; + var transactionStatuses = new[] { "Successful", "Failed", "Reversed" }; + + var random = new Random(42); // Use seed for consistent data + var transactions = new List(); + + // Calculate days in date range + var daysInRange = (query.EndDate - query.StartDate).Days + 1; + var transactionsPerDay = 50; + var totalTransactions = daysInRange * transactionsPerDay; + + for (int i = 0; i < totalTransactions; i++) + { + // Random date within range + var randomDays = random.Next(0, daysInRange); + var randomHours = random.Next(0, 24); + var randomMinutes = random.Next(0, 60); + var transactionDate = query.StartDate.AddDays(randomDays) + .AddHours(randomHours) + .AddMinutes(randomMinutes); + + // Random merchant, operator, and product + var merchant = merchants[random.Next(merchants.Count)]; + var op = operators[random.Next(operators.Count)]; + var product = productList[random.Next(productList.Count)]; + + // Random type and status (90% successful sales) + var typeRoll = random.NextDouble(); + var statusRoll = random.NextDouble(); + + string transactionType; + string transactionStatus; + + if (typeRoll < 0.85) + { + transactionType = "Sale"; + transactionStatus = statusRoll < 0.95 ? "Successful" : "Failed"; + } + else if (typeRoll < 0.95) + { + transactionType = "Refund"; + transactionStatus = "Successful"; + } + else + { + transactionType = "Reversal"; + transactionStatus = "Reversed"; + } + + // Random amounts + var grossAmount = Math.Round((decimal)(random.NextDouble() * 200 + 10), 2); + var feePercentage = 0.015m; // 1.5% + var feesCommission = Math.Round(grossAmount * feePercentage, 2); + var netAmount = grossAmount - feesCommission; + + // Settlement reference (70% have one for successful transactions) + string? settlementReference = null; + if (transactionStatus == "Successful" && random.NextDouble() < 0.7) + { + settlementReference = $"STL-{transactionDate:yyyyMMdd}-{random.Next(1000, 9999)}"; + } + + transactions.Add(new TransactionDetailModel + { + TransactionId = Guid.NewGuid(), + TransactionDateTime = transactionDate, + MerchantName = merchant.MerchantName, + MerchantId = merchant.MerchantId, + OperatorName = op.Name, + OperatorId = op.OperatorId, + ProductName = product.ProductName, + ProductId = product.ContractProductId, + TransactionType = transactionType, + TransactionStatus = transactionStatus, + GrossAmount = grossAmount, + FeesCommission = feesCommission, + NetAmount = netAmount, + SettlementReference = settlementReference + }); + } + + // Apply filters + if (query.MerchantId.HasValue) + { + transactions = transactions.Where(t => t.MerchantId == query.MerchantId.Value).ToList(); + } + + if (query.OperatorId.HasValue) + { + transactions = transactions.Where(t => t.OperatorId == query.OperatorId.Value).ToList(); + } + + if (query.ProductId.HasValue) + { + transactions = transactions.Where(t => t.ProductId == query.ProductId.Value).ToList(); + } + + // Sort by transaction date descending (most recent first) + return transactions.OrderByDescending(t => t.TransactionDateTime).ToList(); + } } diff --git a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs index 057ec3e0..1bb2a4ff 100644 --- a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs +++ b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs @@ -61,6 +61,7 @@ public Task Send(IRequest request, Cancellation Queries.GetOperatorTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockOperatorTransactionSummary(query))), Queries.GetMerchantSettlementHistoryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockMerchantSettlementHistory(query))), Queries.GetSettlementSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockSettlementSummary(query))), + Queries.GetTransactionDetailQuery q => Task.FromResult((TResponse)(object)Result>.Success(GetMockTransactionDetails(q))), // Commands - execute against test data store Commands.CreateMerchantCommand cmd => Task.FromResult((TResponse)(object)ExecuteCreateMerchant(cmd)), @@ -819,4 +820,114 @@ private List GetMockOperatorTransactionSummary( return summary; } + + private List GetMockTransactionDetails(Queries.GetTransactionDetailQuery query) + { + var merchants = _testDataStore.GetMerchants(query.EstateId); + var operators = _testDataStore.GetOperators(query.EstateId); + var contracts = _testDataStore.GetContracts(query.EstateId); + + // Get all products with their IDs from contracts + var productList = contracts + .SelectMany(c => c.Products ?? new List()) + .Where(p => !string.IsNullOrEmpty(p.ProductName)) + .ToList(); + + var random = new Random(42); // Use seed for consistent data + var transactions = new List(); + + // Calculate days in date range + var daysInRange = (query.EndDate - query.StartDate).Days + 1; + var transactionsPerDay = 50; + var totalTransactions = daysInRange * transactionsPerDay; + + for (int i = 0; i < totalTransactions; i++) + { + // Random date within range + var randomDays = random.Next(0, daysInRange); + var randomHours = random.Next(0, 24); + var randomMinutes = random.Next(0, 60); + var transactionDate = query.StartDate.AddDays(randomDays) + .AddHours(randomHours) + .AddMinutes(randomMinutes); + + // Random merchant, operator, and product + var merchant = merchants[random.Next(merchants.Count)]; + var op = operators[random.Next(operators.Count)]; + var product = productList.Count > 0 ? productList[random.Next(productList.Count)] : null; + + // Random type and status (90% successful sales) + var typeRoll = random.NextDouble(); + var statusRoll = random.NextDouble(); + + string transactionType; + string transactionStatus; + + if (typeRoll < 0.85) + { + transactionType = "Sale"; + transactionStatus = statusRoll < 0.95 ? "Successful" : "Failed"; + } + else if (typeRoll < 0.95) + { + transactionType = "Refund"; + transactionStatus = "Successful"; + } + else + { + transactionType = "Reversal"; + transactionStatus = "Reversed"; + } + + // Random amounts + var grossAmount = Math.Round((decimal)(random.NextDouble() * 200 + 10), 2); + var feePercentage = 0.015m; // 1.5% + var feesCommission = Math.Round(grossAmount * feePercentage, 2); + var netAmount = grossAmount - feesCommission; + + // Settlement reference (70% have one for successful transactions) + string? settlementReference = null; + if (transactionStatus == "Successful" && random.NextDouble() < 0.7) + { + settlementReference = $"STL-{transactionDate:yyyyMMdd}-{random.Next(1000, 9999)}"; + } + + transactions.Add(new TransactionDetailModel + { + TransactionId = Guid.NewGuid(), + TransactionDateTime = transactionDate, + MerchantName = merchant.MerchantName, + MerchantId = merchant.MerchantId, + OperatorName = op.Name, + OperatorId = op.OperatorId, + ProductName = product?.ProductName ?? "Default Product", + ProductId = product?.ContractProductId ?? Guid.Empty, + TransactionType = transactionType, + TransactionStatus = transactionStatus, + GrossAmount = grossAmount, + FeesCommission = feesCommission, + NetAmount = netAmount, + SettlementReference = settlementReference + }); + } + + // Apply filters + if (query.MerchantId.HasValue) + { + transactions = transactions.Where(t => t.MerchantId == query.MerchantId.Value).ToList(); + } + + if (query.OperatorId.HasValue) + { + transactions = transactions.Where(t => t.OperatorId == query.OperatorId.Value).ToList(); + } + + if (query.ProductId.HasValue) + { + transactions = transactions.Where(t => t.ProductId == query.ProductId.Value).ToList(); + } + + // Sort by transaction date descending (most recent first) + return transactions.OrderByDescending(t => t.TransactionDateTime).ToList(); + } } diff --git a/EstateManagementUI.BlazorServer/Styles/app.css b/EstateManagementUI.BlazorServer/Styles/app.css index 0eef90e2..295bc55b 100644 --- a/EstateManagementUI.BlazorServer/Styles/app.css +++ b/EstateManagementUI.BlazorServer/Styles/app.css @@ -178,6 +178,10 @@ @apply bg-blue-100 text-blue-800; } + .badge-secondary { + @apply bg-gray-100 text-gray-800; + } + .badge-success { @apply bg-green-100 text-green-800; } @@ -193,6 +197,10 @@ .badge-info { @apply bg-teal-100 text-teal-800; } + + .badge-default { + @apply bg-gray-100 text-gray-600; + } } /* Base styles */ diff --git a/EstateManagementUI.BlazorServer/wwwroot/js/site.js b/EstateManagementUI.BlazorServer/wwwroot/js/site.js new file mode 100644 index 00000000..42ff393f --- /dev/null +++ b/EstateManagementUI.BlazorServer/wwwroot/js/site.js @@ -0,0 +1,17 @@ +// File download utility +function downloadFile(filename, base64Content) { + const binaryString = window.atob(base64Content); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const blob = new Blob([bytes], { type: 'application/octet-stream' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); +}