diff --git a/EstateManagementUI.BlazorServer/Components/App.razor b/EstateManagementUI.BlazorServer/Components/App.razor index 7a7d78a1..beac8bb7 100644 --- a/EstateManagementUI.BlazorServer/Components/App.razor +++ b/EstateManagementUI.BlazorServer/Components/App.razor @@ -16,10 +16,10 @@ - - + + diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor index 5979945d..ab3af6a2 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor @@ -3,10 +3,9 @@ @using EstateManagementUI.BusinessLogic.Requests @using global::Shared.General @rendermode InteractiveServer +@inherits AuthorizedComponentBase @inject IMediator Mediator @inject IJSRuntime JSRuntime -@inherits EstateManagementUI.BlazorServer.Components.Common.CustomComponentBase -@inject ILogger Logger @inject AuthenticationStateProvider AuthenticationStateProvider Analytical Charts (Volume & Value) @@ -161,227 +160,4 @@ } - - -@code { - private bool isLoading = true; - private string? errorMessage; - - private List? comparisonDates; - private string _selectedComparisonDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd"); - private string _selectedChartType = "line"; - - private decimal totalValue = 0; - private int totalCount = 0; - private decimal averageValue = 0; - private decimal netSettlement = 0; - - private List? salesCountData; - private List? salesValueData; - private TodaysSalesModel? todaysSales; - private TodaysSettlementModel? todaysSettlement; - - protected override async Task OnInitializedAsync() - { - await LoadDashboardData(); - await base.OnInitializedAsync(); - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - // Give Chart.js time to load from CDN - await this.WaitOnUIRefresh(); - } - - if (!isLoading && salesCountData != null && salesValueData != null) - { - try - { - // Check if Chart.js is available - var isChartJsLoaded = await JSRuntime.InvokeAsync("eval", "typeof Chart !== 'undefined'"); - if (!isChartJsLoaded) - { - Logger.LogWarning("Chart.js not loaded yet, will retry on next render"); - return; - } - - await UpdateCharts(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in OnAfterRenderAsync"); - } - } - } - - private async Task LoadDashboardData() - { - try - { - isLoading = true; - errorMessage = null; - StateHasChanged(); - - var correlationId = new CorrelationId(Guid.NewGuid()); - // Note: These are stubbed values used throughout the test environment - // In production, these would come from the authentication context - var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - var user = authState.User; - var estateIdClaim = ClaimsHelper.GetUserClaim(user, "estateId"); - if (estateIdClaim.IsFailed) - return; - Guid estateId = Guid.Parse(estateIdClaim.Data.Value); - var accessToken = "stubbed-token"; - - // Load comparison dates first (only if not already loaded) - if (comparisonDates == null || !comparisonDates.Any()) - { - var comparisonDatesResult = await Mediator.Send(new Queries.GetComparisonDatesQuery(correlationId, estateId)); - if (comparisonDatesResult.IsSuccess) - { - comparisonDates = ModelFactory.ConvertFrom(comparisonDatesResult.Data); - if (comparisonDates != null && comparisonDates.Any()) - { - _selectedComparisonDate = comparisonDates.First().Date.ToString("yyyy-MM-dd"); - } - } - } - - if (!DateTime.TryParse(_selectedComparisonDate, out var comparisonDate)) - { - comparisonDate = DateTime.Now.AddDays(-7); - } - - // Load all data in parallel - var salesCountTask = Mediator.Send(new Queries.GetTodaysSalesCountByHourQuery(correlationId, accessToken, estateId, comparisonDate)); - var salesValueTask = Mediator.Send(new Queries.GetTodaysSalesValueByHourQuery(correlationId, accessToken, estateId, comparisonDate)); - var todaysSalesTask = Mediator.Send(new TransactionQueries.GetTodaysSalesQuery(correlationId, estateId, comparisonDate)); - var settlementTask = Mediator.Send(new Queries.GetTodaysSettlementQuery(correlationId, accessToken, estateId, comparisonDate)); - - await Task.WhenAll(salesCountTask, salesValueTask, todaysSalesTask, settlementTask); - - // Process results - if (salesCountTask.Result.IsSuccess) - salesCountData = ModelFactory.ConvertFrom(salesCountTask.Result.Data); - - if (salesValueTask.Result.IsSuccess) - salesValueData = ModelFactory.ConvertFrom(salesValueTask.Result.Data); - - if (todaysSalesTask.Result.IsSuccess) - todaysSales = ModelFactory.ConvertFrom(todaysSalesTask.Result.Data); - - if (settlementTask.Result.IsSuccess) - todaysSettlement = ModelFactory.ConvertFrom(settlementTask.Result.Data); - - // Calculate KPIs - CalculateKPIs(); - } - catch (Exception ex) - { - errorMessage = $"Failed to load data: {ex.Message}"; - } - finally - { - isLoading = false; - StateHasChanged(); - } - } - - private void CalculateKPIs() - { - if (todaysSales != null) - { - totalValue = todaysSales.TodaysSalesValue; - totalCount = todaysSales.TodaysSalesCount; - averageValue = totalCount > 0 ? totalValue / totalCount : 0; - } - - if (todaysSettlement != null) - { - netSettlement = todaysSettlement.TodaysSettlementValue; - } - } - - private async Task OnFiltersChanged() - { - await LoadDashboardData(); - } - - private async Task UpdateCharts() - { - try - { - if (salesCountData == null || salesValueData == null) - { - Logger.LogWarning("Chart data is null - salesCountData: {SalesCountData}, salesValueData: {SalesValueData}", - salesCountData == null ? "null" : "not null", - salesValueData == null ? "null" : "not null"); - return; - } - - Logger.LogInformation("Updating charts with {CountRecords} count records and {ValueRecords} value records", - salesCountData.Count, salesValueData.Count); - - // Create labels with date and time context - var today = DateTime.Today; - var comparisonDateParsed = DateTime.TryParse(_selectedComparisonDate, out var compDate) ? compDate : DateTime.Today.AddDays(-7); - - var labels = salesCountData.Select(d => $"{d.Hour:00}:00").ToArray(); - var todaysCountData = salesCountData.Select(d => d.TodaysSalesCount).ToArray(); - var comparisonCountData = salesCountData.Select(d => d.ComparisonSalesCount).ToArray(); - - var todaysValueData = salesValueData.Select(d => (double)d.TodaysSalesValue).ToArray(); - var comparisonValueData = salesValueData.Select(d => (double)d.ComparisonSalesValue).ToArray(); - - var comparisonLabel = GetComparisonLabel(); - var todayLabel = today.ToString("MMM dd"); - var comparisonDateLabel = compDate.ToString("MMM dd"); - - Logger.LogInformation("Chart labels: {Labels}, Today data points: {TodayCount}, Comparison data points: {CompCount}", - string.Join(", ", labels), todaysCountData.Length, comparisonCountData.Length); - - // Update Volume Chart - await JSRuntime.InvokeVoidAsync("updateOrCreateChart", - "volumeChart", - _selectedChartType, - labels, - new object[] - { - new { label = $"Today ({todayLabel}) Volume", data = todaysCountData, borderColor = "rgb(59, 130, 246)", backgroundColor = "rgba(59, 130, 246, 0.1)", tension = 0.4 }, - new { label = $"{comparisonLabel} ({comparisonDateLabel}) Volume", data = comparisonCountData, borderColor = "rgb(156, 163, 175)", backgroundColor = "rgba(156, 163, 175, 0.1)", tension = 0.4 } - }, - "Transaction Count" - ); - - // Update Value Chart - await JSRuntime.InvokeVoidAsync("updateOrCreateChart", - "valueChart", - _selectedChartType, - labels, - new object[] - { - new { label = $"Today ({todayLabel}) Value", data = todaysValueData, borderColor = "rgb(16, 185, 129)", backgroundColor = "rgba(16, 185, 129, 0.1)", tension = 0.4 }, - new { label = $"{comparisonLabel} ({comparisonDateLabel}) Value", data = comparisonValueData, borderColor = "rgb(156, 163, 175)", backgroundColor = "rgba(156, 163, 175, 0.1)", tension = 0.4 } - }, - "Transaction Value ($)" - ); - - Logger.LogInformation("Charts updated successfully"); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error updating charts"); - } - } - - private string GetComparisonLabel() - { - if (comparisonDates == null) return "Comparison"; - if (!DateTime.TryParse(_selectedComparisonDate, out var date)) - return "Comparison"; - var comparisonDate = comparisonDates.FirstOrDefault(d => d.Date.Date == date.Date); - return comparisonDate?.Description ?? date.ToString("MMM dd"); - } -} + \ No newline at end of file diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor.cs new file mode 100644 index 00000000..dc0bd1e6 --- /dev/null +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/AnalyticalCharts.razor.cs @@ -0,0 +1,237 @@ +using EstateManagementUI.BlazorServer.Factories; +using EstateManagementUI.BlazorServer.Models; +using EstateManagementUI.BlazorServer.Permissions; +using EstateManagementUI.BusinessLogic.Requests; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Shared.General; +using SimpleResults; + +namespace EstateManagementUI.BlazorServer.Components.Pages.Reporting +{ + public partial class AnalyticalCharts + { + private bool isLoading = true; + + private List? comparisonDates; + private string _selectedComparisonDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd"); + private string _selectedChartType = "line"; + + private decimal totalValue = 0; + private int totalCount = 0; + private decimal averageValue = 0; + private decimal netSettlement = 0; + + private List? salesByHourData; + private TodaysSalesModel? todaysSales; + private TodaysSettlementModel? todaysSettlement; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + { + return; + } + + Result result = await OnAfterRender(PermissionSection.Reporting, PermissionFunction.AnalyticalChartsReport, this.LoadDashboardData); + if (result.IsFailed) + { + return; + } + + // Give Chart.js time to load from CDN + await this.WaitOnUIRefresh(); + + // TODO:: move to a function that can be called after data loads as well, to handle cases where Chart.js loads after first render + if (!isLoading && this.salesByHourData != null) + { + try + { + // Check if Chart.js is available + var isChartJsLoaded = await JSRuntime.InvokeAsync("eval", "typeof Chart !== 'undefined'"); + if (!isChartJsLoaded) + { + return; + } + + await UpdateCharts(); + } + catch (Exception ex) + { + return; + } + } + + + } + + private async Task LoadDashboardData() { + try + { + isLoading = true; + errorMessage = null; + StateHasChanged(); + + var correlationId = new CorrelationId(Guid.NewGuid()); + Guid estateId = await this.GetEstateId(); + + // Load comparison dates first (only if not already loaded) + if (comparisonDates == null || !comparisonDates.Any()) + { + var comparisonDatesResult = await Mediator.Send(new Queries.GetComparisonDatesQuery(correlationId, estateId)); + if (comparisonDatesResult.IsSuccess) + { + comparisonDates = ModelFactory.ConvertFrom(comparisonDatesResult.Data); + if (comparisonDates != null && comparisonDates.Any()) + { + _selectedComparisonDate = comparisonDates.First().Date.ToString("yyyy-MM-dd"); + } + } + } + + if (!DateTime.TryParse(_selectedComparisonDate, out var comparisonDate)) + { + comparisonDate = DateTime.Now.AddDays(-7); + } + + // Load all data in parallel + var salesByHourTask = Mediator.Send(new TransactionQueries.GetTodaysSalesByHourQuery(correlationId, estateId, comparisonDate)); + var todaysSalesTask = Mediator.Send(new TransactionQueries.GetTodaysSalesQuery(correlationId, estateId, comparisonDate)); + var settlementTask = Mediator.Send(new SettlementQueries.GetTodaysSettlementQuery(correlationId, estateId, comparisonDate)); + + await Task.WhenAll(salesByHourTask, todaysSalesTask, settlementTask); + + // Process results + if (salesByHourTask.Result.IsSuccess) + this.salesByHourData = ModelFactory.ConvertFrom(salesByHourTask.Result.Data); + + if (todaysSalesTask.Result.IsSuccess) + todaysSales = ModelFactory.ConvertFrom(todaysSalesTask.Result.Data); + + if (settlementTask.Result.IsSuccess) + todaysSettlement = ModelFactory.ConvertFrom(settlementTask.Result.Data); + + // Calculate KPIs + CalculateKPIs(); + return Result.Success(); + } + catch (Exception ex) + { + errorMessage = $"Failed to load data: {ex.Message}"; + return Result.Failure(this.errorMessage); + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private void CalculateKPIs() + { + if (todaysSales != null) + { + totalValue = todaysSales.TodaysSalesValue; + totalCount = todaysSales.TodaysSalesCount; + averageValue = totalCount > 0 ? totalValue / totalCount : 0; + } + + if (todaysSettlement != null) + { + netSettlement = todaysSettlement.TodaysSettlementValue; + } + } + + private async Task OnFiltersChanged() + { + await LoadDashboardData(); + await UpdateCharts(); + } + + private async Task UpdateCharts() + { + try + { + if (this.salesByHourData == null) + { + return; + } + + // Create labels with date and time context + var today = DateTime.Today; + var comparisonDateParsed = DateTime.TryParse(_selectedComparisonDate, out var compDate) ? compDate : DateTime.Today.AddDays(-7); + + var labels = salesByHourData.Select(d => $"{d.Hour:00}:00").ToArray(); + var todaysCountData = this.salesByHourData.Select(d => d.TodaysSalesCount).ToArray(); + var comparisonCountData = this.salesByHourData.Select(d => d.ComparisonSalesCount).ToArray(); + + var todaysValueData = this.salesByHourData.Select(d => (double)d.TodaysSalesValue).ToArray(); + var comparisonValueData = this.salesByHourData.Select(d => (double)d.ComparisonSalesValue).ToArray(); + + var comparisonLabel = GetComparisonLabel(); + var todayLabel = today.ToString("MMM dd"); + var comparisonDateLabel = compDate.ToString("MMM dd"); + + // Update Volume Chart + await JSRuntime.InvokeVoidAsync("updateOrCreateChart", + "volumeChart", + _selectedChartType, + labels, + new object[] + { + new { label = $"Today ({todayLabel}) Volume", data = todaysCountData, borderColor = "rgb(59, 130, 246)", backgroundColor = "rgba(59, 130, 246, 0.1)", tension = 0.4 }, + new { label = $"{comparisonLabel} ({comparisonDateLabel}) Volume", data = comparisonCountData, borderColor = "rgb(156, 163, 175)", backgroundColor = "rgba(156, 163, 175, 0.1)", tension = 0.4 } + }, + "Transaction Count" + ); + + // Update Value Chart + await JSRuntime.InvokeVoidAsync("updateOrCreateChart", + "valueChart", + _selectedChartType, + labels, + new object[] + { + new { label = $"Today ({todayLabel}) Value", data = todaysValueData, borderColor = "rgb(16, 185, 129)", backgroundColor = "rgba(16, 185, 129, 0.1)", tension = 0.4 }, + new { label = $"{comparisonLabel} ({comparisonDateLabel}) Value", data = comparisonValueData, borderColor = "rgb(156, 163, 175)", backgroundColor = "rgba(156, 163, 175, 0.1)", tension = 0.4 } + }, + "Transaction Value (KES)" + ); + } + catch (Exception ex) + { + + } + } + + // Helper to retry checking for element presence + private async Task EnsureCanvasExistsAsync(string elementId, int retries = 5, int delayMs = 200) + { + for (int i = 0; i < retries; i++) + { + try + { + var exists = await JSRuntime.InvokeAsync("elementExists", elementId); + if (exists) return true; + } + catch + { + // elementExists may not be available yet + } + + await Task.Delay(delayMs); + } + + return false; + } + + private string GetComparisonLabel() + { + if (comparisonDates == null) return "Comparison"; + if (!DateTime.TryParse(_selectedComparisonDate, out var date)) + return "Comparison"; + var comparisonDate = comparisonDates.FirstOrDefault(d => d.Date.Date == date.Date); + return comparisonDate?.Description ?? date.ToString("MMM dd"); + } + } +} diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/ProductPerformance.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/ProductPerformance.razor.cs index e0666e51..9ec6c567 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/ProductPerformance.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/ProductPerformance.razor.cs @@ -12,8 +12,7 @@ public partial class ProductPerformance private bool showChart = false; // Filter states - //private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-7)); - private DateOnly _startDate = DateOnly.FromDateTime(new DateTime(2025, 12, 10)); + private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-7)); private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now); // Data diff --git a/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs b/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs index f374b4c5..20790c79 100644 --- a/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs +++ b/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs @@ -25,9 +25,7 @@ using RecentContractModel = EstateManagementUI.BlazorServer.Models.RecentContractModel; using RecentMerchantsModel = EstateManagementUI.BlazorServer.Models.RecentMerchantsModel; using SettlementSummaryModel = EstateManagementUI.BlazorServer.Models.SettlementSummaryModel; -using TodaysSalesCountByHourModel = EstateManagementUI.BlazorServer.Models.TodaysSalesCountByHourModel; using TodaysSalesModel = EstateManagementUI.BlazorServer.Models.TodaysSalesModel; -using TodaysSalesValueByHourModel = EstateManagementUI.BlazorServer.Models.TodaysSalesValueByHourModel; using TodaysSettlementModel = EstateManagementUI.BlazorServer.Models.TodaysSettlementModel; using TransactionDetailModel = EstateManagementUI.BlazorServer.Models.TransactionDetailModel; using TransactionModels = EstateManagementUI.BlazorServer.Models.TransactionModels; @@ -202,29 +200,7 @@ public static TodaysSettlementModel ConvertFrom(BusinessLogic.Models.TodaysSettl TodaysPendingSettlementValue = model.TodaysPendingSettlementValue }; } - - public static List ConvertFrom(List models) - { - List result = new List(); - models.ForEach(m => result.Add(ConvertFrom(m))); - return result; - } - - public static List ConvertFrom(List models) - { - List result = new List(); - models.ForEach(m => result.Add(ConvertFrom(m))); - return result; - } - - public static TodaysSalesCountByHourModel ConvertFrom(BusinessLogic.Models.TodaysSalesCountByHourModel model) { - return new TodaysSalesCountByHourModel() { TodaysSalesCount = model.TodaysSalesCount, ComparisonSalesCount = model.ComparisonSalesCount, Hour = model.Hour }; - } - - public static TodaysSalesValueByHourModel ConvertFrom(BusinessLogic.Models.TodaysSalesValueByHourModel model) { - return new TodaysSalesValueByHourModel() { TodaysSalesValue = model.TodaysSalesValue, ComparisonSalesValue = model.ComparisonSalesValue, Hour = model.Hour }; - } - + public static MerchantKpiModel ConvertFrom(BusinessLogic.Models.MerchantModels.MerchantKpiModel model) { return new MerchantKpiModel() { MerchantsWithNoSaleInLast7Days = model.MerchantsWithNoSaleInLast7Days, MerchantsWithNoSaleToday = model.MerchantsWithNoSaleToday, MerchantsWithSaleInLastHour = model.MerchantsWithSaleInLastHour }; } @@ -598,4 +574,19 @@ public static ProductPerformanceResponse ConvertFrom(BusinessLogic.Models.Transa return model; } + + public static List? ConvertFrom(List resultData) { + List todaysSalesByHourModels = new(); + foreach (BusinessLogic.Models.TransactionModels.TodaysSalesByHourModel todaysSalesCountByHourModel in resultData) { + todaysSalesByHourModels.Add(new TodaysSalesByHourModel { + Hour = todaysSalesCountByHourModel.Hour, + TodaysSalesCount = todaysSalesCountByHourModel.TodaysSalesCount, + ComparisonSalesCount = todaysSalesCountByHourModel.ComparisonSalesCount, + ComparisonSalesValue = todaysSalesCountByHourModel.ComparisonSalesValue, + TodaysSalesValue = todaysSalesCountByHourModel.TodaysSalesValue + }); + } + + return todaysSalesByHourModels; + } } \ No newline at end of file diff --git a/EstateManagementUI.BlazorServer/Models/Models.cs b/EstateManagementUI.BlazorServer/Models/Models.cs index d48adcd5..0f9f91f5 100644 --- a/EstateManagementUI.BlazorServer/Models/Models.cs +++ b/EstateManagementUI.BlazorServer/Models/Models.cs @@ -65,16 +65,11 @@ public class TodaysSettlementModel public decimal TodaysPendingSettlementValue { get; set; } } -public class TodaysSalesCountByHourModel +public class TodaysSalesByHourModel { public int Hour { get; set; } public int TodaysSalesCount { get; set; } public int ComparisonSalesCount { get; set; } -} - -public class TodaysSalesValueByHourModel -{ - public int Hour { get; set; } public decimal TodaysSalesValue { get; set; } public decimal ComparisonSalesValue { get; set; } } diff --git a/EstateManagementUI.BlazorServer/Permissions/PermissionFunction.cs b/EstateManagementUI.BlazorServer/Permissions/PermissionFunction.cs index 7b4902ed..9d17e451 100644 --- a/EstateManagementUI.BlazorServer/Permissions/PermissionFunction.cs +++ b/EstateManagementUI.BlazorServer/Permissions/PermissionFunction.cs @@ -19,5 +19,6 @@ public enum PermissionFunction TransactionDetailReport, TransactionMerchantSummaryReport, TransactionOperatorSummaryReport, - ProductPerformanceReport + ProductPerformanceReport, + AnalyticalChartsReport, } diff --git a/EstateManagementUI.BlazorServer/wwwroot/js/site.js b/EstateManagementUI.BlazorServer/wwwroot/js/site.js index 42ff393f..611a246b 100644 --- a/EstateManagementUI.BlazorServer/wwwroot/js/site.js +++ b/EstateManagementUI.BlazorServer/wwwroot/js/site.js @@ -15,3 +15,17 @@ function downloadFile(filename, base64Content) { document.body.removeChild(link); window.URL.revokeObjectURL(url); } + +window.elementExists = function (id) { + try { return document.getElementById(id) !== null; } catch { return false; } +}; + +window.updateOrCreateChartElement = function (canvasElement, type, labels, datasets, title) { + try { + if (!canvasElement) return; + // existing chart logic but operate on canvasElement (not by id) + // e.g., const ctx = canvasElement.getContext('2d'); ... + } catch (e) { + console.error(e); + } +}; \ No newline at end of file diff --git a/EstateManagmentUI.BusinessLogic/BackendAPI/DataTransferObjects/TodaysSalesByHour.cs b/EstateManagmentUI.BusinessLogic/BackendAPI/DataTransferObjects/TodaysSalesByHour.cs new file mode 100644 index 00000000..09dc3912 --- /dev/null +++ b/EstateManagmentUI.BusinessLogic/BackendAPI/DataTransferObjects/TodaysSalesByHour.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace EstateManagementUI.BusinessLogic.BackendAPI.DataTransferObjects; + +public class TodaysSalesByHour +{ + [JsonProperty("hour")] + public Int32 Hour { get; set; } + [JsonProperty("todays_sales_value")] + public Decimal TodaysSalesValue { get; set; } + [JsonProperty("comparison_sales_value")] + public Decimal ComparisonSalesValue { get; set; } + [JsonProperty("todays_sales_count")] + public Int32 TodaysSalesCount { get; set; } + [JsonProperty("comparison_sales_count")] + public Int32 ComparisonSalesCount { get; set; } +} \ No newline at end of file diff --git a/EstateManagmentUI.BusinessLogic/BackendAPI/DataTransferObjects/TodaysSettlement.cs b/EstateManagmentUI.BusinessLogic/BackendAPI/DataTransferObjects/TodaysSettlement.cs new file mode 100644 index 00000000..61e27824 --- /dev/null +++ b/EstateManagmentUI.BusinessLogic/BackendAPI/DataTransferObjects/TodaysSettlement.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace EstateManagementUI.BusinessLogic.BackendAPI.DataTransferObjects; + +public class TodaysSettlement +{ + [JsonProperty("todays_settlement_value")] + public Decimal TodaysSettlementValue { get; set; } + [JsonProperty("todays_pending_settlement_value")] + public Decimal TodaysPendingSettlementValue { get; set; } + [JsonProperty("todays_settlement_count")] + public Int32 TodaysSettlementCount { get; set; } + [JsonProperty("todays_pending_settlement_count")] + public Int32 TodaysPendingSettlementCount { get; set; } + [JsonProperty("comparison_settlement_value")] + public Decimal ComparisonSettlementValue { get; set; } + [JsonProperty("comparison_pending_settlement_value")] + public Decimal ComparisonPendingSettlementValue { get; set; } + [JsonProperty("comparison_settlement_count")] + public Int32 ComparisonSettlementCount { get; set; } + [JsonProperty("comparison_pending_settlement_count")] + public Int32 ComparisonPendingSettlementCount { get; set; } +} \ No newline at end of file diff --git a/EstateManagmentUI.BusinessLogic/BackendAPI/EstateReportingApiClient.cs b/EstateManagmentUI.BusinessLogic/BackendAPI/EstateReportingApiClient.cs index 97e722f7..976baae3 100644 --- a/EstateManagmentUI.BusinessLogic/BackendAPI/EstateReportingApiClient.cs +++ b/EstateManagmentUI.BusinessLogic/BackendAPI/EstateReportingApiClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text; +using Newtonsoft.Json; using TransactionProcessor.DataTransferObjects.Responses.Merchant; namespace EstateManagementUI.BusinessLogic.BackendAPI @@ -90,6 +91,16 @@ Task> GetTransactionDetailReport(String Task> GetMerchantTransactionSummary(String accessToken, Guid estateId, TransactionSummaryByMerchantRequest request, CancellationToken cancellationToken); Task> GetOperatorTransactionSummary(String accessToken, Guid estateId, TransactionSummaryByOperatorRequest request, CancellationToken cancellationToken); Task> GetProductPerformance(String accessToken, Guid estateId, DateTime startDate, DateTime endDate, CancellationToken cancellationToken); + + Task>> GetTodaysSalesByHour(String accessToken, + Guid estateId, + DateTime comparisonDate, + CancellationToken cancellationToken); + + Task> GetTodaysSettlement(String accessToken, + Guid estateId, + DateTime comparisonDate, + CancellationToken cancellationToken); } public class EstateReportingApiClient : ClientProxyBase.ClientProxyBase, IEstateReportingApiClient { @@ -550,6 +561,68 @@ public async Task> GetProductPerformance(Stri } } + public async Task>> GetTodaysSalesByHour(String accessToken, + Guid estateId, + DateTime comparisonDate, + CancellationToken cancellationToken) { + QueryStringBuilder builder = new QueryStringBuilder(); + builder.AddParameter("comparisonDate", $"{comparisonDate.Date:yyyy-MM-dd}"); + + String requestUri = this.BuildRequestUrl($"/api/transactions/todayssalesbyhour?{builder.BuildQueryString()}"); + + try + { + List<(String headerName, String headerValue)> additionalHeaders = [ + (EstateIdHeaderName, estateId.ToString()) + ]; + + Result>? result = await this.SendHttpGetRequest>(requestUri, accessToken, additionalHeaders, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return result; + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception($"Error getting todays sales by hour for estate {estateId}.", ex); + + return Result.Failure(exception.Message); + } + } + + public async Task> GetTodaysSettlement(String accessToken, + Guid estateId, + DateTime comparisonDate, + CancellationToken cancellationToken) { + QueryStringBuilder builder = new QueryStringBuilder(); + builder.AddParameter("comparisonDate", $"{comparisonDate.Date:yyyy-MM-dd}"); + + String requestUri = this.BuildRequestUrl($"/api/settlements/todayssettlements?{builder.BuildQueryString()}"); + + try + { + List<(String headerName, String headerValue)> additionalHeaders = [ + (EstateIdHeaderName, estateId.ToString()) + ]; + + Result? result = await this.SendHttpGetRequest(requestUri, accessToken, additionalHeaders, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return result; + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception($"Error getting todays settlement for estate {estateId}.", ex); + + return Result.Failure(exception.Message); + } + } + public async Task>> GetMerchantDevices(String accessToken, Guid estateId, Guid merchantId, diff --git a/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs b/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs index b4a3198a..3aa5b91d 100644 --- a/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs +++ b/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs @@ -195,6 +195,35 @@ public static TransactionModels.ProductPerformanceResponse ConvertFrom(ProductPe return model; } + + public static List ConvertFrom(List apiResultData) { + List salesByHour = new(); + foreach (TodaysSalesByHour todaysSalesByHour in apiResultData) { + salesByHour.Add(new TransactionModels.TodaysSalesByHourModel() { + Hour = todaysSalesByHour.Hour, + ComparisonSalesCount = todaysSalesByHour.ComparisonSalesCount, + ComparisonSalesValue = todaysSalesByHour.ComparisonSalesValue, + TodaysSalesCount = todaysSalesByHour.TodaysSalesCount, + TodaysSalesValue = todaysSalesByHour.TodaysSalesValue + }); + } + return salesByHour; + + } + + public static TodaysSettlementModel ConvertFrom(TodaysSettlement apiResultData) { + TodaysSettlementModel model = new() { + ComparisonPendingSettlementCount = apiResultData.ComparisonPendingSettlementCount, + ComparisonPendingSettlementValue = apiResultData.ComparisonPendingSettlementValue, + ComparisonSettlementCount = apiResultData.ComparisonSettlementCount, + ComparisonSettlementValue = apiResultData.ComparisonSettlementValue, + TodaysPendingSettlementCount = apiResultData.TodaysPendingSettlementCount, + TodaysPendingSettlementValue = apiResultData.TodaysPendingSettlementValue, + TodaysSettlementCount = apiResultData.TodaysSettlementCount, + TodaysSettlementValue = apiResultData.TodaysSettlementValue + }; + return model; + } } public static class FactoryExtensions{ diff --git a/EstateManagmentUI.BusinessLogic/Client/SettlementMethods.cs b/EstateManagmentUI.BusinessLogic/Client/SettlementMethods.cs new file mode 100644 index 00000000..b2925472 --- /dev/null +++ b/EstateManagmentUI.BusinessLogic/Client/SettlementMethods.cs @@ -0,0 +1,31 @@ +using EstateManagementUI.BusinessLogic.Models; +using EstateManagementUI.BusinessLogic.Requests; +using Shared.Results; +using SimpleResults; + +namespace EstateManagementUI.BusinessLogic.Client +{ + public partial interface IApiClient + { + Task> GetTodaysSettlement(SettlementQueries.GetTodaysSettlementQuery request, CancellationToken cancellationToken); + } + + public partial class ApiClient { + public async Task> GetTodaysSettlement(SettlementQueries.GetTodaysSettlementQuery request, + CancellationToken cancellationToken) { + // Get a token here + var token = await this.GetToken(cancellationToken); + if (token.IsFailed) + return ResultHelpers.CreateFailure(token); + + var apiResult = await this.EstateReportingApiClient.GetTodaysSettlement(token.Data, request.EstateId, request.ComparisonDate, cancellationToken); + + if (apiResult.IsFailed) + return ResultHelpers.CreateFailure(apiResult); + + TodaysSettlementModel todaysSettlementModel = APIModelFactory.ConvertFrom(apiResult.Data); + + return Result.Success(todaysSettlementModel); + } + } +} diff --git a/EstateManagmentUI.BusinessLogic/Client/StubTestData.cs b/EstateManagmentUI.BusinessLogic/Client/StubTestData.cs index e78c2902..7d4910a3 100644 --- a/EstateManagmentUI.BusinessLogic/Client/StubTestData.cs +++ b/EstateManagmentUI.BusinessLogic/Client/StubTestData.cs @@ -389,27 +389,17 @@ public static class StubTestData { TodaysPendingSettlementValue = 14500.00m }; - public static List GetMockSalesCountByHour() => new() + public static List GetMockSalesCountByHour() => new() { - new TodaysSalesCountByHourModel { Hour = 9, TodaysSalesCount = 45, ComparisonSalesCount = 38 }, - new TodaysSalesCountByHourModel { Hour = 10, TodaysSalesCount = 67, ComparisonSalesCount = 54 }, - new TodaysSalesCountByHourModel { Hour = 11, TodaysSalesCount = 89, ComparisonSalesCount = 76 }, - new TodaysSalesCountByHourModel { Hour = 12, TodaysSalesCount = 102, ComparisonSalesCount = 95 }, - new TodaysSalesCountByHourModel { Hour = 13, TodaysSalesCount = 78, ComparisonSalesCount = 82 }, - new TodaysSalesCountByHourModel { Hour = 14, TodaysSalesCount = 65, ComparisonSalesCount = 71 }, - new TodaysSalesCountByHourModel { Hour = 15, TodaysSalesCount = 77, ComparisonSalesCount = 34 } + new TransactionModels.TodaysSalesByHourModel { Hour = 9, TodaysSalesCount = 45, ComparisonSalesCount = 38, TodaysSalesValue = 12500, ComparisonSalesValue = 10500 }, + new TransactionModels.TodaysSalesByHourModel { Hour = 10, TodaysSalesCount = 67, ComparisonSalesCount = 54, TodaysSalesValue = 18500, ComparisonSalesValue = 15000 }, + new TransactionModels.TodaysSalesByHourModel { Hour = 11, TodaysSalesCount = 89, ComparisonSalesCount = 76, TodaysSalesValue = 24500, ComparisonSalesValue = 21000 }, + new TransactionModels.TodaysSalesByHourModel { Hour = 12, TodaysSalesCount = 102, ComparisonSalesCount = 95, TodaysSalesValue = 28000, ComparisonSalesValue = 26000 }, + new TransactionModels.TodaysSalesByHourModel { Hour = 13, TodaysSalesCount = 78, ComparisonSalesCount = 82, TodaysSalesValue = 21500, ComparisonSalesValue = 22500 }, + new TransactionModels.TodaysSalesByHourModel { Hour = 14, TodaysSalesCount = 65, ComparisonSalesCount = 71 , TodaysSalesValue = 18000, ComparisonSalesValue = 19500}, + new TransactionModels.TodaysSalesByHourModel { Hour = 15, TodaysSalesCount = 77, ComparisonSalesCount = 34 , TodaysSalesValue = 21500, ComparisonSalesValue = 9500} }; - public static List GetMockSalesValueByHour() => new() - { - new TodaysSalesValueByHourModel { Hour = 9, TodaysSalesValue = 12500, ComparisonSalesValue = 10500 }, - new TodaysSalesValueByHourModel { Hour = 10, TodaysSalesValue = 18500, ComparisonSalesValue = 15000 }, - new TodaysSalesValueByHourModel { Hour = 11, TodaysSalesValue = 24500, ComparisonSalesValue = 21000 }, - new TodaysSalesValueByHourModel { Hour = 12, TodaysSalesValue = 28000, ComparisonSalesValue = 26000 }, - new TodaysSalesValueByHourModel { Hour = 13, TodaysSalesValue = 21500, ComparisonSalesValue = 22500 }, - new TodaysSalesValueByHourModel { Hour = 14, TodaysSalesValue = 18000, ComparisonSalesValue = 19500 }, - new TodaysSalesValueByHourModel { Hour = 15, TodaysSalesValue = 21500, ComparisonSalesValue = 9500 } - }; public static MerchantModels.MerchantKpiModel GetMockMerchantKpi() => new() { diff --git a/EstateManagmentUI.BusinessLogic/Client/TransactionMethods.cs b/EstateManagmentUI.BusinessLogic/Client/TransactionMethods.cs index 899a3b0a..e43e9a98 100644 --- a/EstateManagmentUI.BusinessLogic/Client/TransactionMethods.cs +++ b/EstateManagmentUI.BusinessLogic/Client/TransactionMethods.cs @@ -17,6 +17,7 @@ public partial interface IApiClient Task> GetMerchantTransactionSummary(TransactionQueries.GetMerchantTransactionSummaryQuery request, CancellationToken cancellationToken); Task> GetOperatorTransactionSummary(TransactionQueries.GetOperatorTransactionSummaryQuery request, CancellationToken cancellationToken); Task> GetProductPerformance(TransactionQueries.GetProductPerformanceQuery request, CancellationToken cancellationToken); + Task>> GetTodaysSalesByHour(TransactionQueries.GetTodaysSalesByHourQuery request, CancellationToken cancellationToken); } public partial class ApiClient : IApiClient { @@ -157,5 +158,22 @@ public async Task> GetTodaysFailedSales(TransactionQuer return Result.Success(productPerformanceResponseModel); } + + public async Task>> GetTodaysSalesByHour(TransactionQueries.GetTodaysSalesByHourQuery request, + CancellationToken cancellationToken) { + // Get a token here + var token = await this.GetToken(cancellationToken); + if (token.IsFailed) + return ResultHelpers.CreateFailure(token); + + var apiResult = await this.EstateReportingApiClient.GetTodaysSalesByHour(token.Data, request.EstateId, request.ComparisonDate, cancellationToken); + + if (apiResult.IsFailed) + return ResultHelpers.CreateFailure(apiResult); + + List todaysSalesByHourModel = APIModelFactory.ConvertFrom(apiResult.Data); + + return Result.Success(todaysSalesByHourModel); + } } } diff --git a/EstateManagmentUI.BusinessLogic/Models/Models.cs b/EstateManagmentUI.BusinessLogic/Models/Models.cs index 2de379b4..21393859 100644 --- a/EstateManagmentUI.BusinessLogic/Models/Models.cs +++ b/EstateManagmentUI.BusinessLogic/Models/Models.cs @@ -54,21 +54,6 @@ public class TodaysSettlementModel public decimal TodaysPendingSettlementValue { get; set; } } -public class TodaysSalesCountByHourModel -{ - public int Hour { get; set; } - public int TodaysSalesCount { get; set; } - public int ComparisonSalesCount { get; set; } -} - -public class TodaysSalesValueByHourModel -{ - public int Hour { get; set; } - public decimal TodaysSalesValue { get; set; } - public decimal ComparisonSalesValue { get; set; } -} - - public class TopBottomProductDataModel { diff --git a/EstateManagmentUI.BusinessLogic/Models/TransactionModels.cs b/EstateManagmentUI.BusinessLogic/Models/TransactionModels.cs index eccdfe55..cf619203 100644 --- a/EstateManagmentUI.BusinessLogic/Models/TransactionModels.cs +++ b/EstateManagmentUI.BusinessLogic/Models/TransactionModels.cs @@ -121,5 +121,14 @@ public class ProductPerformanceSummary public Decimal TotalValue { get; set; } public Decimal AveragePerProduct { get; set; } } + + public class TodaysSalesByHourModel + { + public int Hour { get; set; } + public int TodaysSalesCount { get; set; } + public int ComparisonSalesCount { get; set; } + public decimal TodaysSalesValue { get; set; } + public decimal ComparisonSalesValue { get; set; } + } } } diff --git a/EstateManagmentUI.BusinessLogic/RequestHandlers/DashboardRequestHandler.cs b/EstateManagmentUI.BusinessLogic/RequestHandlers/DashboardRequestHandler.cs index 7e9cc5ae..80ec31a1 100644 --- a/EstateManagmentUI.BusinessLogic/RequestHandlers/DashboardRequestHandler.cs +++ b/EstateManagmentUI.BusinessLogic/RequestHandlers/DashboardRequestHandler.cs @@ -7,9 +7,8 @@ namespace EstateManagementUI.BusinessLogic.RequestHandlers; public class DashboardRequestHandler : - IRequestHandler>, - IRequestHandler>>, - IRequestHandler>>, + + //IRequestHandler>>, IRequestHandler>>, IRequestHandler>>, @@ -29,21 +28,15 @@ public DashboardRequestHandler(IApiClient apiClient) { // Implementations similar to above handlers returning stub data + //public async Task>> Handle(TransactionQueries.GetTodaysSalesByHourQuery request, + // CancellationToken cancellationToken) { + // return Result.Success(StubTestData.GetMockSalesCountByHour()); + //} - public async Task> Handle(Queries.GetTodaysSettlementQuery request, - CancellationToken cancellationToken) { - return Result.Success(StubTestData.GetMockTodaysSettlement()); - } - - public async Task>> Handle(Queries.GetTodaysSalesCountByHourQuery request, - CancellationToken cancellationToken) { - return Result.Success(StubTestData.GetMockSalesCountByHour()); - } - - public async Task>> Handle(Queries.GetTodaysSalesValueByHourQuery request, - CancellationToken cancellationToken) { - return Result.Success(StubTestData.GetMockSalesValueByHour()); - } + //public async Task>> Handle(Queries.GetTodaysSalesValueByHourQuery request, + // CancellationToken cancellationToken) { + // return Result.Success(StubTestData.GetMockSalesValueByHour()); + //} public async Task>> Handle(Queries.GetTopProductDataQuery request, CancellationToken cancellationToken) { diff --git a/EstateManagmentUI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/EstateManagmentUI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index 27a0e165..c7219acb 100644 --- a/EstateManagmentUI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/EstateManagmentUI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -11,7 +11,8 @@ public class TransactionRequestHandler : IRequestHandler>, IRequestHandler>, IRequestHandler>, - IRequestHandler> + IRequestHandler>, + IRequestHandler>> { private readonly IApiClient ApiClient; public TransactionRequestHandler(IApiClient apiClient) { @@ -47,4 +48,23 @@ public async Task> Handle(TransactionQueries.GetTodaysF CancellationToken cancellationToken) { return await this.ApiClient.GetProductPerformance(request, cancellationToken); } + + public async Task>> Handle(TransactionQueries.GetTodaysSalesByHourQuery request, + CancellationToken cancellationToken) { + return await this.ApiClient.GetTodaysSalesByHour(request, cancellationToken); + } +} + +public class SettlementRequestHandler : IRequestHandler> { + + private readonly IApiClient ApiClient; + public SettlementRequestHandler(IApiClient apiClient) + { + this.ApiClient = apiClient; + } + + public async Task> Handle(SettlementQueries.GetTodaysSettlementQuery request, + CancellationToken cancellationToken) { + return await this.ApiClient.GetTodaysSettlement(request, cancellationToken); + } } \ No newline at end of file diff --git a/EstateManagmentUI.BusinessLogic/Requests/Queries.cs b/EstateManagmentUI.BusinessLogic/Requests/Queries.cs index 582553d0..7ee8366a 100644 --- a/EstateManagmentUI.BusinessLogic/Requests/Queries.cs +++ b/EstateManagmentUI.BusinessLogic/Requests/Queries.cs @@ -11,6 +11,11 @@ public record GetTransactionDetailQuery(CorrelationId CorrelationId, Guid Estate public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, Guid EstateId, DateTime StartDate, DateTime EndDate, Int32? MerchantId = null, Int32? OperatorId = null) : IRequest>; public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId,Guid EstateId, DateTime StartDate, DateTime EndDate, Int32? MerchantId = null, Int32? OperatorId = null) : IRequest>; public record GetProductPerformanceQuery(CorrelationId CorrelationId, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest>; + public record GetTodaysSalesByHourQuery(CorrelationId CorrelationId, Guid EstateId, DateTime ComparisonDate) : IRequest>>; +} + +public static class SettlementQueries { + public record GetTodaysSettlementQuery(CorrelationId CorrelationId, Guid EstateId, DateTime ComparisonDate) : IRequest>; } public static class Queries @@ -22,11 +27,6 @@ public record GetFileImportLogQuery(CorrelationId CorrelationId, string AccessTo public record GetFileDetailsQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid FileId) : IRequest>; public record GetComparisonDatesQuery(CorrelationId CorrelationId, Guid EstateId) : IRequest>>; - public record GetTodaysSettlementQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime ComparisonDate) : IRequest>; - public record GetTodaysSalesCountByHourQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime ComparisonDate) : IRequest>>; - public record GetTodaysSalesValueByHourQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime ComparisonDate) : IRequest>>; - - public record GetTopProductDataQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, int ResultCount) : IRequest>>; public record GetBottomProductDataQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, int ResultCount) : IRequest>>; public record GetTopMerchantDataQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, int ResultCount) : IRequest>>; @@ -34,7 +34,6 @@ public record GetBottomMerchantDataQuery(CorrelationId CorrelationId, string Acc public record GetTopOperatorDataQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, int ResultCount) : IRequest>>; public record GetBottomOperatorDataQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, int ResultCount) : IRequest>>; public record GetLastSettlementQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId) : 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 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>>; diff --git a/Pages/_Host.cshtml b/Pages/_Host.cshtml new file mode 100644 index 00000000..499127c4 --- /dev/null +++ b/Pages/_Host.cshtml @@ -0,0 +1,32 @@ +@page "/" +@namespace EstateManagementUI.BlazorServer.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = Layout.MainLayout; +} + + + + + + + + + + + EstateManagementUI + + + + + + + + + + + + + + + \ No newline at end of file