From 6a6b187d2fa87b5af8e04216f0fb52bb0998912c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:56:54 +0000 Subject: [PATCH 01/20] Initial plan From 3d6457b13cee09dd649891638d5db2ec5aa1790e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:14:30 +0000 Subject: [PATCH 02/20] feat: add merchant schedule maintenance screen Agent-Logs-Url: https://github.com/TransactionProcessing/EstateManagementUI/sessions/29ec46de-60e8-40bd-b189-413834e552ce Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../Merchants/MerchantSchedulePageTests.cs | 129 ++++++++ .../Pages/Merchants/MerchantsViewPageTests.cs | 19 ++ .../UIServices/MerchantUIServiceTests.cs | 89 ++++++ .../Common/Helpers.cs | 5 + .../Components/Pages/Merchants/Schedule.razor | 100 ++++++ .../Pages/Merchants/Schedule.razor.cs | 291 ++++++++++++++++++ .../Components/Pages/Merchants/View.razor | 3 + .../Components/Pages/Merchants/View.razor.cs | 2 + .../Factories/ModelFactory.cs | 12 + .../Models/MerchantModels.cs | 12 + .../UIServices/MerchantUIService.cs | 40 ++- .../Client/APIModelFactory.cs | 17 + .../Client/MerchantMethods.cs | 42 +++ .../Models/MerchantModels.cs | 12 + .../RequestHandlers/MerchantRequestHandler.cs | 12 + .../Requests/MerchantCommands.cs | 1 + .../Requests/MerchantQueries.cs | 3 +- 17 files changed, 787 insertions(+), 2 deletions(-) create mode 100644 EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs create mode 100644 EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor create mode 100644 EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs new file mode 100644 index 00000000..5ac18252 --- /dev/null +++ b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs @@ -0,0 +1,129 @@ +using Bunit; +using EstateManagementUI.BlazorServer.Components.Pages.Merchants; +using EstateManagementUI.BlazorServer.Models; +using EstateManagementUI.BusinessLogic.Requests; +using Moq; +using Shouldly; +using SimpleResults; +using Xunit; + +namespace EstateManagementUI.BlazorServer.Tests.Pages.Merchants; + +public class MerchantSchedulePageTests : BaseTest +{ + [Fact] + public void MerchantSchedule_LoadsMerchantAndSchedule() + { + var merchantId = Guid.NewGuid(); + var currentYear = DateTime.Today.Year; + SetupPageData(merchantId, currentYear, new MerchantModels.MerchantScheduleModel { Year = currentYear, Months = [] }); + + var cut = RenderComponent(parameters => parameters.Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + cut.Markup.ShouldContain("Merchant Schedule: Test Merchant"); + cut.Markup.ShouldContain("Selected Year Maintenance"); + cut.Find("#month-1-closed-days"); + } + + [Fact] + public void MerchantSchedule_ClonePreviousYear_CopiesEditableMonths() + { + var merchantId = Guid.NewGuid(); + var currentYear = DateTime.Today.Year; + var futureYear = currentYear + 1; + + SetupPageData(merchantId, currentYear, new MerchantModels.MerchantScheduleModel { Year = currentYear, Months = [] }); + SetupSchedule(futureYear, new MerchantModels.MerchantScheduleModel { Year = futureYear, Months = [] }); + SetupSchedule(currentYear, new MerchantModels.MerchantScheduleModel + { + Year = currentYear, + Months = new List + { + new() { Month = 1, ClosedDays = new List { 1, 2, 15 } } + } + }); + + var cut = RenderComponent(parameters => parameters.Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + cut.Find("#selectedYear").Change(futureYear.ToString()); + cut.Find("#loadYearButton").Click(); + cut.Find("#clonePreviousYearButton").Click(); + + cut.Find("#month-1-closed-days").GetAttribute("value").ShouldBe("1, 2, 15"); + } + + [Fact] + public void MerchantSchedule_SaveSelectedYear_SendsYearPayload() + { + var merchantId = Guid.NewGuid(); + var currentYear = DateTime.Today.Year; + var futureYear = currentYear + 1; + + SetupPageData(merchantId, currentYear, new MerchantModels.MerchantScheduleModel { Year = currentYear, Months = [] }); + SetupSchedule(futureYear, new MerchantModels.MerchantScheduleModel { Year = futureYear, Months = [] }); + this.MerchantUIService.Setup(m => m.SaveMerchantSchedule(It.IsAny(), It.IsAny(), merchantId, + It.IsAny())) + .ReturnsAsync(Result.Success()); + + var cut = RenderComponent(parameters => parameters.Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + cut.Find("#selectedYear").Change(futureYear.ToString()); + cut.Find("#loadYearButton").Click(); + cut.Find("#month-1-closed-days").Change("1, 2, 15"); + cut.Find("#saveScheduleButton").Click(); + + this.MerchantUIService.Verify(m => m.SaveMerchantSchedule( + It.IsAny(), + It.IsAny(), + merchantId, + It.Is(schedule => + schedule.Year == futureYear && + schedule.Months.Any(month => month.Month == 1 && month.ClosedDays.SequenceEqual(new[] { 1, 2, 15 })) + )), Times.Once); + } + + [Fact] + public void MerchantSchedule_PreviousYear_IsReadOnly() + { + var merchantId = Guid.NewGuid(); + var currentYear = DateTime.Today.Year; + var previousYear = currentYear - 1; + + SetupPageData(merchantId, currentYear, new MerchantModels.MerchantScheduleModel { Year = currentYear, Months = [] }); + SetupSchedule(previousYear, new MerchantModels.MerchantScheduleModel { Year = previousYear, Months = [] }); + + var cut = RenderComponent(parameters => parameters.Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + cut.Find("#selectedYear").Change(previousYear.ToString()); + cut.Find("#loadYearButton").Click(); + + cut.Find("#month-1-closed-days").HasAttribute("disabled").ShouldBeTrue(); + cut.Find("#saveScheduleButton").HasAttribute("disabled").ShouldBeTrue(); + } + + private void SetupPageData(Guid merchantId, + Int32 scheduleYear, + MerchantModels.MerchantScheduleModel schedule) + { + this.MerchantUIService.Setup(m => m.GetMerchant(It.IsAny(), It.IsAny(), merchantId)) + .ReturnsAsync(Result.Success(new MerchantModels.MerchantModel + { + MerchantId = merchantId, + MerchantName = "Test Merchant", + MerchantReference = "REF001" + })); + + SetupSchedule(scheduleYear, schedule); + } + + private void SetupSchedule(Int32 year, + MerchantModels.MerchantScheduleModel schedule) + { + this.MerchantUIService.Setup(m => m.GetMerchantSchedule(It.IsAny(), It.IsAny(), It.IsAny(), year)) + .ReturnsAsync(Result.Success(schedule)); + } +} diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsViewPageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsViewPageTests.cs index e3e0656b..0b4a3418 100644 --- a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsViewPageTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsViewPageTests.cs @@ -562,6 +562,25 @@ public void MerchantsView_BackButton_NavigatesToMerchantsList() _fakeNavigationManager.Uri.ShouldContain("/merchants"); } + [Fact] + public void MerchantsView_ManageScheduleButton_NavigatesToMerchantSchedule() + { + var merchantId = Guid.NewGuid(); + SetupSuccessfulDataLoad(new MerchantModels.MerchantModel + { + MerchantId = merchantId, + MerchantName = "Test Merchant", + MerchantReference = "REF001" + }); + + var cut = RenderComponent(parameters => parameters.Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + cut.Find("#manageScheduleButton").Click(); + + _fakeNavigationManager.Uri.ShouldContain($"/merchants/{merchantId}/schedule"); + } + // Helper methods private void SetupSuccessfulDataLoad( MerchantModels.MerchantModel? merchant = null, diff --git a/EstateManagementUI.BlazorServer.Tests/UIServices/MerchantUIServiceTests.cs b/EstateManagementUI.BlazorServer.Tests/UIServices/MerchantUIServiceTests.cs index a6ef52b9..ca57a3dc 100644 --- a/EstateManagementUI.BlazorServer.Tests/UIServices/MerchantUIServiceTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/UIServices/MerchantUIServiceTests.cs @@ -555,6 +555,95 @@ public async Task GetMerchantsForDropDown_ReturnsFailure_WhenMediatorFails() result.IsFailed.ShouldBeTrue(); } + [Fact] + public async Task GetMerchantSchedule_ReturnsMappedModel_WhenMediatorSucceeds() + { + var estateId = Guid.NewGuid(); + var merchantId = Guid.NewGuid(); + var year = DateTime.Today.Year; + var businessLogicSchedule = new BusinessLogic.Models.MerchantModels.MerchantScheduleModel + { + Year = year, + Months = new List + { + new() { Month = 1, ClosedDays = new List { 1, 2, 15 } } + } + }; + + _mockMediator + .Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(businessLogicSchedule)); + + var result = await _service.GetMerchantSchedule(CorrelationIdHelper.New(), estateId, merchantId, year); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ShouldNotBeNull(); + result.Data!.Year.ShouldBe(year); + result.Data.Months.Count.ShouldBe(1); + result.Data.Months[0].Month.ShouldBe(1); + result.Data.Months[0].ClosedDays.ShouldBe(new List { 1, 2, 15 }); + } + + [Fact] + public async Task GetMerchantSchedule_ReturnsFailure_WhenMediatorFails() + { + _mockMediator + .Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Failure("err")); + + var result = await _service.GetMerchantSchedule(CorrelationIdHelper.New(), Guid.NewGuid(), Guid.NewGuid(), DateTime.Today.Year); + + result.IsFailed.ShouldBeTrue(); + } + + [Fact] + public async Task SaveMerchantSchedule_SendsCreateCommand_AndReturnsSuccess() + { + var estateId = Guid.NewGuid(); + var merchantId = Guid.NewGuid(); + var schedule = new BlazorServer.Models.MerchantModels.MerchantScheduleModel + { + Year = DateTime.Today.Year + 1, + Months = new List + { + new() { Month = 1, ClosedDays = new List { 1, 2, 15 } }, + new() { Month = 2, ClosedDays = new List() } + } + }; + + _mockMediator + .Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success); + + var result = await _service.SaveMerchantSchedule(CorrelationIdHelper.New(), estateId, merchantId, schedule); + + result.IsSuccess.ShouldBeTrue(); + _mockMediator.Verify(m => m.Send(It.Is(c => + c.EstateId == estateId && + c.MerchantId == merchantId && + c.Schedule.Year == schedule.Year && + c.Schedule.Months.Count == 2 && + c.Schedule.Months[0].Month == 1 && + c.Schedule.Months[0].ClosedDays.SequenceEqual(new[] { 1, 2, 15 }) + ), It.IsAny()), Times.Once); + } + + [Fact] + public async Task SaveMerchantSchedule_ReturnsFailure_WhenMediatorFails() + { + _mockMediator + .Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Failure("err")); + + var result = await _service.SaveMerchantSchedule(CorrelationIdHelper.New(), Guid.NewGuid(), Guid.NewGuid(), new BlazorServer.Models.MerchantModels.MerchantScheduleModel + { + Year = DateTime.Today.Year, + Months = new List() + }); + + result.IsFailed.ShouldBeTrue(); + } + [Fact] public async Task AddOperatorToMerchant_ReturnsFailure_WhenMediatorFails() { diff --git a/EstateManagementUI.BlazorServer/Common/Helpers.cs b/EstateManagementUI.BlazorServer/Common/Helpers.cs index cfdd3ef7..55513a6a 100644 --- a/EstateManagementUI.BlazorServer/Common/Helpers.cs +++ b/EstateManagementUI.BlazorServer/Common/Helpers.cs @@ -210,6 +210,11 @@ public static void NavigateToMakeMerchantDeposit(this NavigationManager navigati navigationManager.NavigateTo($"/merchants/{merchantId}/deposit"); } + public static void NavigateToMerchantSchedule(this NavigationManager navigationManager, Guid merchantId) + { + navigationManager.NavigateTo($"/merchants/{merchantId}/schedule"); + } + public static void NavigateToNewMerchant(this NavigationManager navigationManager) { navigationManager.NavigateTo($"/merchants/new"); diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor new file mode 100644 index 00000000..70f3165b --- /dev/null +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor @@ -0,0 +1,100 @@ +@page "/merchants/{MerchantId:guid}/schedule" +@rendermode InteractiveServer +@using EstateManagementUI.BlazorServer.Models +@using EstateManagementUI.BlazorServer.UIServices +@using Microsoft.AspNetCore.Components.Forms +@inherits AuthorizedComponentBase +@inject IMerchantUIService MerchantUIService +@inject NavigationManager NavigationManager + +Merchant Schedule + +
+ @if (isLoading) + { +
+
+
+ } + else if (merchant != null) + { +
+
+

Merchant Schedule: @merchant.MerchantName

+

Reference: @merchant.MerchantReference

+
+
+ +
+
+ + @if (!string.IsNullOrWhiteSpace(successMessage)) + { +
+

Success

+

@successMessage

+
+ } + + @if (!string.IsNullOrWhiteSpace(errorMessage)) + { +
+

Error

+

@errorMessage

+
+ } + +
+
+
+

Selected Year Maintenance

+

Only the selected year can be updated. Past months become read only once they have passed.

+
+
+
+ + +
+ + + +
+
+ +
+

Enter closed days as comma-separated day numbers, for example 1, 2, 15. Saving only updates editable months in the year you currently have loaded.

+
+ +
+ @foreach (var month in monthEditors) + { +
+
+
+

@month.MonthName

+

@month.Description

+
+ + @(month.IsReadOnly ? "Read Only" : "Editable") + +
+ +
+ + +

Use comma-separated day numbers for this month.

+
+
+ } +
+
+ } +
diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs new file mode 100644 index 00000000..da80c6bc --- /dev/null +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs @@ -0,0 +1,291 @@ +using System.Globalization; +using EstateManagementUI.BlazorServer.Common; +using EstateManagementUI.BlazorServer.Models; +using EstateManagementUI.BlazorServer.Permissions; +using EstateManagementUI.BusinessLogic.Requests; +using Shared.Results; +using SimpleResults; + +namespace EstateManagementUI.BlazorServer.Components.Pages.Merchants +{ + public partial class Schedule + { + [Parameter] + public Guid MerchantId { get; set; } + + private readonly DateTime today = DateTime.Today; + private MerchantModels.MerchantModel? merchant; + private List monthEditors = []; + private bool isLoading = true; + private bool isSaving; + private int selectedYear = DateTime.Today.Year; + private int yearInput = DateTime.Today.Year; + private string? errorMessage; + private string? successMessage; + + private bool CanSave => this.isSaving == false && this.monthEditors.Any(month => month.IsReadOnly == false); + private bool CanClonePreviousYear => this.isSaving == false && this.selectedYear > 1900 && this.monthEditors.Any(month => month.IsReadOnly == false); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender == false) + { + return; + } + + Result result = await OnAfterRender(PermissionSection.Merchant, PermissionFunction.Edit, this.LoadPage); + if (result.IsFailed) + { + return; + } + + isLoading = false; + this.StateHasChanged(); + } + + private async Task LoadPage() + { + try + { + isLoading = true; + Guid estateId = await this.GetEstateId(); + CorrelationId correlationId = new(Guid.NewGuid()); + + Result merchantResult = await this.MerchantUIService.GetMerchant(correlationId, estateId, this.MerchantId); + if (merchantResult.IsFailed) + { + return ResultHelpers.CreateFailure(merchantResult); + } + + this.merchant = merchantResult.Data; + this.yearInput = this.selectedYear; + + return await this.LoadSchedule(estateId, this.selectedYear); + } + finally + { + isLoading = false; + } + } + + private async Task LoadSelectedYearAsync() + { + this.successMessage = null; + this.errorMessage = null; + + Int32 previousYear = this.selectedYear; + Int32 requestedYear = this.yearInput; + this.selectedYear = requestedYear; + Guid estateId = await this.GetEstateId(); + Result result = await this.LoadSchedule(estateId, this.selectedYear); + if (result.IsFailed) + { + this.selectedYear = previousYear; + this.errorMessage = result.Errors.SingleOrDefault() ?? $"Failed to load schedule for {requestedYear}."; + } + this.StateHasChanged(); + } + + private async Task ClonePreviousYearAsync() + { + if (this.CanClonePreviousYear == false) + { + return; + } + + this.successMessage = null; + this.errorMessage = null; + + Guid estateId = await this.GetEstateId(); + CorrelationId correlationId = new(Guid.NewGuid()); + Int32 sourceYear = this.selectedYear - 1; + + Result result = await this.MerchantUIService.GetMerchantSchedule(correlationId, estateId, this.MerchantId, sourceYear); + if (result.Status == ResultStatus.NotFound) + { + this.errorMessage = $"No schedule exists for {sourceYear} to clone."; + return; + } + + if (result.IsFailed) + { + this.errorMessage = result.Errors.SingleOrDefault() ?? $"Failed to load schedule for {sourceYear}."; + return; + } + + Dictionary sourceMonths = result.Data.Months.ToDictionary(month => month.Month); + foreach (ScheduleMonthEditor month in this.monthEditors.Where(month => month.IsReadOnly == false)) + { + List closedDays = sourceMonths.TryGetValue(month.Month, out MerchantModels.MerchantScheduleMonthModel? sourceMonth) + ? sourceMonth.ClosedDays.OrderBy(day => day).ToList() + : []; + + month.ClosedDaysInput = this.FormatClosedDays(closedDays); + } + + this.successMessage = $"Editable months were cloned from {sourceYear}."; + } + + private async Task SaveScheduleAsync() + { + if (this.CanSave == false) + { + return; + } + + this.successMessage = null; + this.errorMessage = null; + this.isSaving = true; + + try + { + Result scheduleResult = this.BuildScheduleToSave(); + if (scheduleResult.IsFailed) + { + this.errorMessage = scheduleResult.Errors.SingleOrDefault() ?? "Invalid schedule."; + return; + } + + Guid estateId = await this.GetEstateId(); + CorrelationId correlationId = new(Guid.NewGuid()); + Result result = await this.MerchantUIService.SaveMerchantSchedule(correlationId, estateId, this.MerchantId, scheduleResult.Data); + if (result.IsFailed) + { + this.errorMessage = result.Errors.SingleOrDefault() ?? "Failed to save merchant schedule."; + return; + } + + this.successMessage = $"Schedule saved for {this.selectedYear}."; + } + finally + { + this.isSaving = false; + } + } + + private async Task LoadSchedule(Guid estateId, + Int32 year) + { + CorrelationId correlationId = new(Guid.NewGuid()); + Result result = await this.MerchantUIService.GetMerchantSchedule(correlationId, estateId, this.MerchantId, year); + + if (result.Status == ResultStatus.NotFound) + { + this.BuildMonthEditors(null); + return Result.Success(); + } + + if (result.IsFailed) + { + return ResultHelpers.CreateFailure(result); + } + + this.BuildMonthEditors(result.Data); + return Result.Success(); + } + + private Result BuildScheduleToSave() + { + List editableMonths = []; + + foreach (ScheduleMonthEditor month in this.monthEditors.Where(month => month.IsReadOnly == false)) + { + Result> parseResult = this.ParseClosedDays(month.ClosedDaysInput, month.Month); + if (parseResult.IsFailed) + { + return Result.Failure(parseResult.Errors.SingleOrDefault() ?? $"Invalid closed days for {month.MonthName}."); + } + + month.ClosedDaysInput = this.FormatClosedDays(parseResult.Data); + editableMonths.Add(new MerchantModels.MerchantScheduleMonthModel + { + Month = month.Month, + ClosedDays = parseResult.Data + }); + } + + return Result.Success(new MerchantModels.MerchantScheduleModel + { + Year = this.selectedYear, + Months = editableMonths + }); + } + + private Result> ParseClosedDays(String? closedDaysInput, + Int32 month) + { + List days = []; + if (String.IsNullOrWhiteSpace(closedDaysInput)) + { + return Result.Success(days); + } + + foreach (String token in closedDaysInput.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + if (Int32.TryParse(token, out Int32 day) == false) + { + return Result.Failure($"'{token}' is not a valid day number for {CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month)}."); + } + + days.Add(day); + } + + Int32 maxDay = DateTime.DaysInMonth(this.selectedYear, month); + List normalisedDays = days.Distinct().OrderBy(day => day).ToList(); + if (normalisedDays.Any(day => day < 1 || day > maxDay)) + { + return Result.Failure($"Only days between 1 and {maxDay} can be supplied for {CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month)} {this.selectedYear}."); + } + + return Result.Success(normalisedDays); + } + + private void BuildMonthEditors(MerchantModels.MerchantScheduleModel? schedule) + { + Dictionary monthLookup = schedule?.Months.ToDictionary(month => month.Month) ?? []; + + this.monthEditors = Enumerable.Range(1, 12).Select(month => + { + List closedDays = monthLookup.TryGetValue(month, out MerchantModels.MerchantScheduleMonthModel? scheduleMonth) + ? scheduleMonth.ClosedDays.OrderBy(day => day).ToList() + : []; + + return new ScheduleMonthEditor + { + Month = month, + MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month), + ClosedDaysInput = this.FormatClosedDays(closedDays), + IsReadOnly = this.IsMonthReadOnly(this.selectedYear, month), + Description = this.IsMonthReadOnly(this.selectedYear, month) + ? "This month has passed and cannot be changed." + : "Closed days can still be updated." + }; + }).ToList(); + } + + private bool IsMonthReadOnly(Int32 year, + Int32 month) + { + if (year < this.today.Year) + { + return true; + } + + return year == this.today.Year && month < this.today.Month; + } + + private String FormatClosedDays(IEnumerable closedDays) => String.Join(", ", closedDays.OrderBy(day => day)); + + private void BackToMerchant() => this.NavigationManager.NavigateToMerchant(this.MerchantId); + + private sealed class ScheduleMonthEditor + { + public Int32 Month { get; init; } + public String MonthName { get; init; } = String.Empty; + public String ClosedDaysInput { get; set; } = String.Empty; + public Boolean IsReadOnly { get; init; } + public String Description { get; init; } = String.Empty; + public String InputId => $"month-{this.Month}-closed-days"; + } + } +} diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor index ba38c731..36b9c3d9 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor @@ -26,6 +26,9 @@

Reference: @merchant.MerchantReference

+ diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor.cs index 5da0dfec..1bd4f6a9 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor.cs @@ -220,6 +220,8 @@ private IReadOnlyList GetOpeningHoursRows() => new("Sunday", merchant?.OpeningHours.Sunday ?? new MerchantModels.DayOpeningHoursModel()) ]; + private void ManageSchedule() => NavigationManager.NavigateToMerchantSchedule(this.MerchantId); + private void BackToList() => NavigationManager.NavigateToMerchantList(); private sealed record OpeningHoursRow(string DayName, MerchantModels.DayOpeningHoursModel Hours); diff --git a/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs b/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs index 81c5c44f..563bfd2b 100644 --- a/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs +++ b/EstateManagementUI.BlazorServer/Factories/ModelFactory.cs @@ -391,6 +391,18 @@ private static RecentContractModel ConvertFrom(BusinessLogic.Models.ContractMode return merchantList; } + public static MerchantScheduleModel ConvertFrom(BusinessLogic.Models.MerchantModels.MerchantScheduleModel resultData) { + return new MerchantScheduleModel { + Year = resultData.Year, + Months = resultData.Months + .OrderBy(month => month.Month) + .Select(month => new MerchantScheduleMonthModel { + Month = month.Month, + ClosedDays = month.ClosedDays.OrderBy(day => day).ToList() + }).ToList() + }; + } + public static List ConvertFrom(List resultData) { List merchantOperators = new(); foreach (BusinessLogic.Models.MerchantModels.MerchantOperatorModel merchantOperatorModel in resultData) diff --git a/EstateManagementUI.BlazorServer/Models/MerchantModels.cs b/EstateManagementUI.BlazorServer/Models/MerchantModels.cs index 579bf88d..6cb21c84 100644 --- a/EstateManagementUI.BlazorServer/Models/MerchantModels.cs +++ b/EstateManagementUI.BlazorServer/Models/MerchantModels.cs @@ -93,6 +93,18 @@ public class MerchantListModel public DateTime CreatedDateTime { get; set; } } + public class MerchantScheduleModel + { + public Int32 Year { get; set; } + public List Months { get; set; } = []; + } + + public class MerchantScheduleMonthModel + { + public Int32 Month { get; set; } + public List ClosedDays { get; set; } = []; + } + public class CreateMerchantModel { [Required(ErrorMessage = "Merchant name is required")] diff --git a/EstateManagementUI.BlazorServer/UIServices/MerchantUIService.cs b/EstateManagementUI.BlazorServer/UIServices/MerchantUIService.cs index c1c70a7e..c5ca563d 100644 --- a/EstateManagementUI.BlazorServer/UIServices/MerchantUIService.cs +++ b/EstateManagementUI.BlazorServer/UIServices/MerchantUIService.cs @@ -94,6 +94,10 @@ Task MakeMerchantDeposit(CorrelationId correlationId, Task> GetMerchantKpis(CorrelationId correlationId, Guid estateId); Task>> GetMerchantsForDropDown(CorrelationId correlationId, Guid estateId); + + Task> GetMerchantSchedule(CorrelationId correlationId, Guid estateId, Guid merchantId, Int32 year); + + Task SaveMerchantSchedule(CorrelationId correlationId, Guid estateId, Guid merchantId, MerchantModels.MerchantScheduleModel scheduleModel); } public class MerchantUIService : IMerchantUIService { @@ -315,7 +319,7 @@ public async Task>> GetRecentMerchants(Correla } public async Task>> GetMerchantsForDropDown(CorrelationId correlationId, - Guid estateId) { + Guid estateId) { MerchantQueries.GetMerchantsForDropDownQuery query = new(correlationId, estateId); var result = await this.Mediator.Send(query); if (result.IsFailed) @@ -323,4 +327,38 @@ public async Task>> GetRecentMerchants(Correla var merchantList = ModelFactory.ConvertFrom(result.Data); return Result.Success(merchantList); } + + public async Task> GetMerchantSchedule(CorrelationId correlationId, + Guid estateId, + Guid merchantId, + Int32 year) { + MerchantQueries.GetMerchantScheduleQuery query = new(correlationId, estateId, merchantId, year); + var result = await this.Mediator.Send(query); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + var schedule = ModelFactory.ConvertFrom(result.Data); + return Result.Success(schedule); + } + + public async Task SaveMerchantSchedule(CorrelationId correlationId, + Guid estateId, + Guid merchantId, + MerchantModels.MerchantScheduleModel scheduleModel) { + BusinessLogic.Models.MerchantModels.MerchantScheduleModel businessLogicModel = new() { + Year = scheduleModel.Year, + Months = scheduleModel.Months + .OrderBy(month => month.Month) + .Select(month => new BusinessLogic.Models.MerchantModels.MerchantScheduleMonthModel { + Month = month.Month, + ClosedDays = month.ClosedDays.OrderBy(day => day).ToList() + }).ToList() + }; + + MerchantCommands.CreateMerchantScheduleCommand command = new(correlationId, estateId, merchantId, businessLogicModel); + var result = await this.Mediator.Send(command); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return Result.Success(); + } } diff --git a/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs b/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs index 409db552..0b56f339 100644 --- a/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs +++ b/EstateManagmentUI.BusinessLogic/Client/APIModelFactory.cs @@ -39,6 +39,23 @@ public static MerchantModels.MerchantKpiModel ConvertFrom(MerchantKpi apiResult) return model; } + public static MerchantModels.MerchantScheduleModel ConvertFrom(MerchantScheduleResponse apiResult) + { + MerchantModels.MerchantScheduleModel model = new() + { + Year = apiResult.Year, + Months = apiResult.Months + .OrderBy(month => month.Month) + .Select(month => new MerchantModels.MerchantScheduleMonthModel + { + Month = month.Month, + ClosedDays = month.ClosedDays.OrderBy(day => day).ToList() + }).ToList() + }; + + return model; + } + public static TodaysSalesModel ConvertFrom(TodaysSales apiResultData) { TodaysSalesModel model = new TodaysSalesModel { ComparisonAverageValue = apiResultData.ComparisonAverageSalesValue, diff --git a/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs b/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs index ed0fa4b7..9445226a 100644 --- a/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs +++ b/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs @@ -5,6 +5,7 @@ using Shared.Results; using SimpleResults; using TransactionProcessor.DataTransferObjects.Requests.Merchant; +using TransactionProcessor.DataTransferObjects.Requests.MerchantSchedule; using TransactionProcessor.DataTransferObjects.Responses.Merchant; namespace EstateManagementUI.BusinessLogic.Client @@ -16,10 +17,12 @@ public partial interface IApiClient Task>> GetMerchants(MerchantQueries.GetMerchantsQuery request, CancellationToken cancellationToken); Task>> GetMerchants(MerchantQueries.GetMerchantsForDropDownQuery request, CancellationToken cancellationToken); Task> GetMerchant(MerchantQueries.GetMerchantQuery request, CancellationToken cancellationToken); + Task> GetMerchantSchedule(MerchantQueries.GetMerchantScheduleQuery request, CancellationToken cancellationToken); Task>> GetMerchantOperators(MerchantQueries.GetMerchantOperatorsQuery request, CancellationToken cancellationToken); Task>> GetMerchantContracts(MerchantQueries.GetMerchantContractsQuery request, CancellationToken cancellationToken); Task>> GetMerchantDevices(MerchantQueries.GetMerchantDevicesQuery request, CancellationToken cancellationToken); Task UpdateMerchant(MerchantCommands.UpdateMerchantCommand request, CancellationToken cancellationToken); + Task CreateMerchantSchedule(MerchantCommands.CreateMerchantScheduleCommand request, CancellationToken cancellationToken); Task UpdateMerchantOpeningHours(MerchantCommands.UpdateMerchantOpeningHoursCommand request, CancellationToken cancellationToken); Task UpdateMerchantAddress(MerchantCommands.UpdateMerchantCommand request, CancellationToken cancellationToken); Task UpdateMerchantContact(MerchantCommands.UpdateMerchantCommand request, CancellationToken cancellationToken); @@ -152,6 +155,29 @@ public async Task CreateMerchant(MerchantCommands.CreateMerchantCommand return Result.Success(); } + public async Task CreateMerchantSchedule(MerchantCommands.CreateMerchantScheduleCommand request, + CancellationToken cancellationToken) { + var token = await this.GetToken(cancellationToken); + if (token.IsFailed) + return ResultHelpers.CreateFailure(token); + + CreateMerchantScheduleRequest apiRequest = new() { + Year = request.Schedule.Year, + Months = request.Schedule.Months + .OrderBy(month => month.Month) + .Select(month => new MerchantScheduleMonthRequest { + Month = month.Month, + ClosedDays = month.ClosedDays.OrderBy(day => day).ToList() + }).ToList() + }; + + Result apiResult = await this.TransactionProcessorClient.CreateMerchantSchedule(token.Data, request.EstateId, request.MerchantId, apiRequest, cancellationToken); + if (apiResult.IsFailed) + return ResultHelpers.CreateFailure(apiResult); + + return Result.Success(); + } + public async Task RemoveOperatorFromMerchant(MerchantCommands.RemoveOperatorFromMerchantCommand request, CancellationToken cancellationToken) { var token = await this.GetToken(cancellationToken); @@ -234,6 +260,22 @@ public async Task RemoveOperatorFromMerchant(MerchantCommands.RemoveOper return Result.Success(merchant); } + public async Task> GetMerchantSchedule(MerchantQueries.GetMerchantScheduleQuery request, + CancellationToken cancellationToken) { + Result token = await this.GetToken(cancellationToken); + if (token.IsFailed) + return ResultHelpers.CreateFailure(token); + + Result apiResult = await this.TransactionProcessorClient.GetMerchantSchedule(token.Data, request.EstateId, request.MerchantId, request.Year, cancellationToken); + + if (apiResult.IsFailed) + return ResultHelpers.CreateFailure(apiResult); + + MerchantModels.MerchantScheduleModel merchantSchedule = APIModelFactory.ConvertFrom(apiResult.Data); + + return Result.Success(merchantSchedule); + } + public async Task>> GetMerchantOperators(MerchantQueries.GetMerchantOperatorsQuery request, CancellationToken cancellationToken) { Result token = await this.GetToken(cancellationToken); diff --git a/EstateManagmentUI.BusinessLogic/Models/MerchantModels.cs b/EstateManagmentUI.BusinessLogic/Models/MerchantModels.cs index 57551b81..a2df661b 100644 --- a/EstateManagmentUI.BusinessLogic/Models/MerchantModels.cs +++ b/EstateManagmentUI.BusinessLogic/Models/MerchantModels.cs @@ -95,6 +95,18 @@ public class MerchantKpiModel public int MerchantsWithSaleInLastHour { get; set; } } + public class MerchantScheduleModel + { + public Int32 Year { get; set; } + public List Months { get; set; } = []; + } + + public class MerchantScheduleMonthModel + { + public Int32 Month { get; set; } + public List ClosedDays { get; set; } = []; + } + public class MerchantOpeningHoursModel { public DayOpeningHoursModel Sunday { get; set; } = new(); diff --git a/EstateManagmentUI.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs b/EstateManagmentUI.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs index 4a4757cc..fd716154 100644 --- a/EstateManagmentUI.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs +++ b/EstateManagmentUI.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs @@ -9,9 +9,11 @@ namespace EstateManagementUI.BusinessLogic.RequestHandlers; public class MerchantRequestHandler : IRequestHandler>>, IRequestHandler>, + IRequestHandler>, IRequestHandler, IRequestHandler, IRequestHandler, + IRequestHandler, IRequestHandler, IRequestHandler, IRequestHandler, @@ -101,6 +103,11 @@ public async Task Handle(MerchantCommands.UpdateMerchantOpeningHoursComm return await this.ApiClient.GetMerchant(request, cancellationToken); } + public async Task> Handle(MerchantQueries.GetMerchantScheduleQuery request, + CancellationToken cancellationToken) { + return await this.ApiClient.GetMerchantSchedule(request, cancellationToken); + } + public async Task Handle(MerchantCommands.AssignContractToMerchantCommand request, CancellationToken cancellationToken) { return await this.ApiClient.AddContractToMerchant(request, cancellationToken); @@ -111,6 +118,11 @@ public async Task Handle(MerchantCommands.AssignContractToMerchantComman return await this.ApiClient.GetRecentMerchants(request, cancellationToken); } + public async Task Handle(MerchantCommands.CreateMerchantScheduleCommand request, + CancellationToken cancellationToken) { + return await this.ApiClient.CreateMerchantSchedule(request, cancellationToken); + } + public async Task>> Handle(MerchantQueries.GetMerchantsForDropDownQuery request, CancellationToken cancellationToken) { return await this.ApiClient.GetMerchants(request, cancellationToken); diff --git a/EstateManagmentUI.BusinessLogic/Requests/MerchantCommands.cs b/EstateManagmentUI.BusinessLogic/Requests/MerchantCommands.cs index bbe86b52..05df112b 100644 --- a/EstateManagmentUI.BusinessLogic/Requests/MerchantCommands.cs +++ b/EstateManagmentUI.BusinessLogic/Requests/MerchantCommands.cs @@ -18,6 +18,7 @@ public record AddMerchantDeviceCommand(CorrelationId CorrelationId, Guid EstateI public record SwapMerchantDeviceCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, string OldDevice, string NewDevice) : IRequest; public record MakeMerchantDepositCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, decimal Amount, DateTime Date, string Reference) : IRequest; public record CreateMerchantCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, string Name, String SettlementSchedule, MerchantAddress MerchantAddress, MerchantContact MerchantContact) : IRequest; + public record CreateMerchantScheduleCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, MerchantModels.MerchantScheduleModel Schedule) : IRequest; public record MerchantOpeningHours(OpeningHours Sunday, OpeningHours Monday, OpeningHours Tuesday, OpeningHours Wednesday, OpeningHours Thursday, OpeningHours Friday, OpeningHours Saturday); diff --git a/EstateManagmentUI.BusinessLogic/Requests/MerchantQueries.cs b/EstateManagmentUI.BusinessLogic/Requests/MerchantQueries.cs index 819b3a79..6966e4f3 100644 --- a/EstateManagmentUI.BusinessLogic/Requests/MerchantQueries.cs +++ b/EstateManagmentUI.BusinessLogic/Requests/MerchantQueries.cs @@ -10,7 +10,8 @@ public record GetMerchantsForDropDownQuery(CorrelationId CorrelationId, Guid Est public record GetRecentMerchantsQuery(CorrelationId CorrelationId, Guid EstateId) : IRequest>>; public record GetMerchantKpiQuery(CorrelationId CorrelationId, Guid EstateId) : IRequest>; public record GetMerchantQuery(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId) : IRequest>; + public record GetMerchantScheduleQuery(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, Int32 Year) : IRequest>; public record GetMerchantOperatorsQuery(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId) : IRequest>>; public record GetMerchantContractsQuery(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId) : IRequest>>; public record GetMerchantDevicesQuery(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId) : IRequest>>; -} \ No newline at end of file +} From d5c590c90ee3c2ceaf8377ec3bfc5c1c078eab33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:19:44 +0000 Subject: [PATCH 03/20] chore: finalize merchant schedule screen Agent-Logs-Url: https://github.com/TransactionProcessing/EstateManagementUI/sessions/29ec46de-60e8-40bd-b189-413834e552ce Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../Components/Pages/Merchants/Schedule.razor.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs index da80c6bc..5b734ef4 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs @@ -249,14 +249,15 @@ private void BuildMonthEditors(MerchantModels.MerchantScheduleModel? schedule) List closedDays = monthLookup.TryGetValue(month, out MerchantModels.MerchantScheduleMonthModel? scheduleMonth) ? scheduleMonth.ClosedDays.OrderBy(day => day).ToList() : []; + Boolean isReadOnly = this.IsMonthReadOnly(this.selectedYear, month); return new ScheduleMonthEditor { Month = month, MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month), ClosedDaysInput = this.FormatClosedDays(closedDays), - IsReadOnly = this.IsMonthReadOnly(this.selectedYear, month), - Description = this.IsMonthReadOnly(this.selectedYear, month) + IsReadOnly = isReadOnly, + Description = isReadOnly ? "This month has passed and cannot be changed." : "Closed days can still be updated." }; From 9b5156aa370da292b6d1957c76785876ff50fd0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:25:30 +0000 Subject: [PATCH 04/20] feat: switch schedule year to dropdown Agent-Logs-Url: https://github.com/TransactionProcessing/EstateManagementUI/sessions/6fd7e708-c9e9-4e80-b2eb-3847e027b2dc Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../Merchants/MerchantSchedulePageTests.cs | 17 +++++++++++++++++ .../Components/Pages/Merchants/Schedule.razor | 7 ++++++- .../Pages/Merchants/Schedule.razor.cs | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs index 5ac18252..b2f396ed 100644 --- a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs @@ -26,6 +26,23 @@ public void MerchantSchedule_LoadsMerchantAndSchedule() cut.Find("#month-1-closed-days"); } + [Fact] + public void MerchantSchedule_YearSelector_DisplaysNextTenYears() + { + var merchantId = Guid.NewGuid(); + var currentYear = DateTime.Today.Year; + SetupPageData(merchantId, currentYear, new MerchantModels.MerchantScheduleModel { Year = currentYear, Months = [] }); + + var cut = RenderComponent(parameters => parameters.Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + var options = cut.FindAll("#selectedYear option"); + + options.Count.ShouldBe(10); + options.First().GetAttribute("value").ShouldBe(currentYear.ToString()); + options.Last().GetAttribute("value").ShouldBe((currentYear + 9).ToString()); + } + [Fact] public void MerchantSchedule_ClonePreviousYear_CopiesEditableMonths() { diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor index 70f3165b..7ebd9dd7 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor @@ -55,7 +55,12 @@
- + + @foreach (var year in availableYears) + { + + } +
+ diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs index 01bc2528..d1ccc5ba 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs @@ -554,5 +554,7 @@ private async Task SwapDeviceConfirm(string originalDevice) { private void BackToList() => NavigationManager.NavigateToMerchantList(); + private void EditSchedule() => NavigationManager.NavigateToMerchantSchedule(this.MerchantId); + private sealed record OpeningHoursRow(String DayName, MerchantModels.DayOpeningHoursModel Hours); } diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor index 7ebd9dd7..29bdabc4 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor @@ -25,7 +25,7 @@
@@ -49,8 +49,8 @@
-

Selected Year Maintenance

-

Only the selected year can be updated. Past months become read only once they have passed.

+

@(ReadOnly ? "Selected Year Schedule" : "Selected Year Maintenance")

+

@(ReadOnly ? "View the selected year's schedule. Months are shown read only from the merchant view screen." : "Only the selected year can be updated. Past months become read only once they have passed.")

@@ -65,17 +65,27 @@ - - + @if (ReadOnly == false) + { + + + }
-

Enter closed days as comma-separated day numbers, for example 1, 2, 15. Saving only updates editable months in the year you currently have loaded.

+ @if (ReadOnly) + { +

Closed days are shown as comma-separated day numbers for the selected year.

+ } + else + { +

Enter closed days as comma-separated day numbers, for example 1, 2, 15. Saving only updates editable months in the year you currently have loaded.

+ }
diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs index 6acc9eec..3d4c6a6c 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs @@ -3,6 +3,7 @@ using EstateManagementUI.BlazorServer.Models; using EstateManagementUI.BlazorServer.Permissions; using EstateManagementUI.BusinessLogic.Requests; +using Microsoft.AspNetCore.Components; using Shared.Results; using SimpleResults; @@ -13,6 +14,10 @@ public partial class Schedule [Parameter] public Guid MerchantId { get; set; } + [Parameter] + [SupplyParameterFromQuery(Name = "readOnly")] + public Boolean ReadOnly { get; set; } + private readonly DateTime today = DateTime.Today; private readonly IReadOnlyList availableYears = Enumerable.Range(DateTime.Today.Year, 10).ToList(); private MerchantModels.MerchantModel? merchant; @@ -24,8 +29,8 @@ public partial class Schedule private string? errorMessage; private string? successMessage; - private bool CanSave => this.isSaving == false && this.monthEditors.Any(month => month.IsReadOnly == false); - private bool CanClonePreviousYear => this.isSaving == false && this.selectedYear > 1900 && this.monthEditors.Any(month => month.IsReadOnly == false); + private bool CanSave => this.ReadOnly == false && this.isSaving == false && this.monthEditors.Any(month => month.IsReadOnly == false); + private bool CanClonePreviousYear => this.ReadOnly == false && this.isSaving == false && this.selectedYear > 1900 && this.monthEditors.Any(month => month.IsReadOnly == false); protected override async Task OnAfterRenderAsync(bool firstRender) { @@ -34,7 +39,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender) return; } - Result result = await OnAfterRender(PermissionSection.Merchant, PermissionFunction.Edit, this.LoadPage); + PermissionFunction permissionFunction = this.ReadOnly ? PermissionFunction.View : PermissionFunction.Edit; + Result result = await OnAfterRender(PermissionSection.Merchant, permissionFunction, this.LoadPage); if (result.IsFailed) { return; @@ -250,7 +256,7 @@ private void BuildMonthEditors(MerchantModels.MerchantScheduleModel? schedule) List closedDays = monthLookup.TryGetValue(month, out MerchantModels.MerchantScheduleMonthModel? scheduleMonth) ? scheduleMonth.ClosedDays.OrderBy(day => day).ToList() : []; - Boolean isReadOnly = this.IsMonthReadOnly(this.selectedYear, month); + Boolean isReadOnly = this.ReadOnly || this.IsMonthReadOnly(this.selectedYear, month); return new ScheduleMonthEditor { @@ -259,7 +265,9 @@ private void BuildMonthEditors(MerchantModels.MerchantScheduleModel? schedule) ClosedDaysInput = this.FormatClosedDays(closedDays), IsReadOnly = isReadOnly, Description = isReadOnly - ? "This month has passed and cannot be changed." + ? this.ReadOnly + ? "This schedule is read only from the merchant view screen." + : "This month has passed and cannot be changed." : "Closed days can still be updated." }; }).ToList(); @@ -278,7 +286,16 @@ private bool IsMonthReadOnly(Int32 year, private String FormatClosedDays(IEnumerable closedDays) => String.Join(", ", closedDays.OrderBy(day => day)); - private void BackToMerchant() => this.NavigationManager.NavigateToMerchant(this.MerchantId); + private void BackToMerchant() + { + if (this.ReadOnly) + { + this.NavigationManager.NavigateToMerchant(this.MerchantId); + return; + } + + this.NavigationManager.NavigateToEditMerchant(this.MerchantId); + } private sealed class ScheduleMonthEditor { diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor index 36b9c3d9..ae29881b 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor @@ -26,8 +26,8 @@

Reference: @merchant.MerchantReference

-